ES数据扩容与索引设计

数据扩容

当存储达到上限的时候,就需要考虑扩容,一般有两种情况:

  1. 单分片的数据量达到了上限(文档数量上限 2^31 或者容量上限 50GB)
  2. 单节点数据容量超载,存储吃紧

第 1 种情况,需考虑增加分片的数量,减小单个分片的数据存储量。
第 2 种情况,需要增加新节点,减小单节点上的数据量,缓解单节点数据容量吃紧的压力。当有新的节点加入集群,Elasticsearch 会自动移动分片,且在分片移动过程中,所有的索引搜索请求均在正常运行。

Elasticsearch 在索引创建时需要指定分片数量,分片的数量一旦确定,就不能进行修改了,因为分片的数量是文档路由算法的计算元素:

shard = hash(routing) % number_of_primary_shards

所以,在索引创建之初,就需要根据业务实际情况,对分片的数量做一个预分配。提前预分配合适的分片数量。

海量分片

分片的预分配不是随意的,海量的分片数量给将来的扩容带来一定的便利,但是会有大量的没有实际意义的分片存在,而每个分片也是有一定的成本的:

  • 一个分片的底层即为一个 Lucene 索引,会消耗一定文件句柄、内存、以及 CPU 运转。
  • 每一个搜索请求都需要命中索引中的每一个分片,海量的分片性能比较差。

所以要避免海量分片,应该根据业务发展情况,对数据量做评估,来预分配更合适的分片数量。

容量评估

1 个分片太少而 1000 个又太多,那么怎么知道需要多少分片? 这是一个不好回答的问题。因为实在有太多相关的因素:使用的硬件、文档的大小和复杂度、文档的索引分析方式、运行的查询类型、执行的聚合以及数据模型等。

单分片的容量评估:

  1. 基于准备用于生产环境的硬件创建一个拥有单个节点的集群。
  2. 创建一个和准备用于生产环境相同配置和分析器的索引,并设置只有一个分片无副本。
  3. 索引实际的文档(或者尽可能接近实际)。
  4. 运行实际的查询和聚合(或者尽可能接近实际)。

复制真实环境的使用方式并将数据全部压缩到单个分片上直到它“挂掉”。
“挂掉”的定义:请求无响应或者超出忍耐。这样就可以定义好了单个分片的合适容量大小。

通常将 50GB 作为分片上限,但还是有必要进行实际场景评估,评估是为了找到一个界限,实际场景中,可能数据量未触达 50GB 上限,但是过了界限就已经不太能够满足快速响应速度需求了。

数据总量(GB)的评估:
通过上面单分片容量的评估可以计算出相对准确的单个文档占用多大存储空间,然后按照业务未来某个阶段发展的预期,评估出来总的文档数据量,计算出来数据总量(GB):
数据总量(GB)= 单文档存储空间 x 文档数据量

分片数量确定:
分片数 = 数据总量(GB)/ 单分片的容量
另外,结合每 1GB 内存对应最多 20 个分片

磁盘空间(GB)的评估:
磁盘空间(BG)= 数据总量(GB)x(副本数量 + 1)x 1.2

数据节点数量评估:
数据节点数量 = 向上取整(磁盘空间(GB)/ 单节点的数据量(GB))+ 1

扩容分类

增加分片

当分片容量快要达到上限,容量达到上限 50GB,或者文档数量达到上限,报错:number of documents in the index can not exceed 2147483519。就需要扩大分片数量以减小单个分片内的数据量。

大规模流行论坛都是从小论坛起步的,随着时间的推移,论坛的数据量激增到超过了当前分片的上限,就需要扩增分片数量。

扩增分片的步骤:
第一步、为论坛创建一个新的索引 baking_new,并为其分配合理的分片数,可以满足当前和未来一定预期的数据增长:

PUT /baking_new
{
 "settings": {
 "number_of_shards": 10
 }
}

第二步、将旧索引中的数据迁移到新的索引 baking_new 中,可以通过 scroll 查询和 bulk API 来实现,当迁移完成时,可以更新索引别名指向那个新的索引:

POST /_aliases
{
 "actions": [
 { "remove": { "alias": "baking", "index": "baking_old" }},
 { "add": { "alias": "baking", "index": "baking_new" }}
 ]
}

更新索引别名的操作是原子性的,就像在拨动一个开关。应用程序还是在与 baking API 交互并且对于它已经指向 baking_new 索引毫无感知。

增加容量

数据节点上磁盘容量吃紧的时候,Elasticsearch 会有一些表现,例如:数据不能写入,不能查询最新的数据等。这背后可能是触发 Elasticsearch 的保护机制:

  • ES cluster.routing.allocation.disk.watermark.low:控制磁盘使用的低水位线(watermark) 默认值 85%,超过后,Elasticsearch 不会再为该节点分配分片;
  • ES cluster.routing.allocation.disk.watermark.high:控制高水位线,默认值 90%,超过后,将尝试将分片重定位到其他节点;
  • ES cluster.routing.allocation.disk.watermark.flood_stage:控制洪泛水位线。默认值 95%,超过后,Elasticsearch 集群将强制将所有索引都标记为只读。如需解除只读,只能手动将 index.blocks.read_only_allow_delete 改成 false。

磁盘的扩容相对比较容易,一般可以通过下面两种方案解决:增加磁盘容量或者增加数据节点。

索引设计

基于时序的数据

时序数据的特点:文档数量增长迅速,通常随时间加速;文档几乎不会更新,基本以最近文档为搜索目标;随着时间推移,旧文档逐渐失去价值。典型的时序数据如:日志。

应对时序数据的索引设计一般按照时间范围索引数据,比如:按年的索引 (logs_2022) 或按月的索引 (logs_2022-06)。如果数据量比较大,甚至可以考虑按天索引 (logs_2022_06_29)。

按照时间范围索引数据,可以比较方便的在需要的时候进行分片调整,按照数据增长情况调整索引时间范围来适应当前需求。删除旧数据也十分简单:只需要删除旧的索引即可。

按照时间范围索引数据,索引的数量就不止一个,Elasticsearch 提供的别名,可以实现透明地在索引间切换。例如:当创建索引时,可以将 logs_current 指向当前索引来接收新的日志事件,当检索时,将 last_3_months 指向所有最近三个月的索引:

POST /_aliases
{
 "actions": [
 { "add": { "alias": "logs_current", "index": "logs_2021-10" }}, 
 { "remove": { "alias": "logs_current", "index": "logs_2021-09" }}, 
 { "add": { "alias": "last_3_months", "index": "logs_2021-10" }}, 
 { "remove": { "alias": "last_3_months", "index": "logs_2021-07" }} 
 ]
}

最新的索引是 10 月,将 logs_current 由 9 月切换至 10 月。将 10 月添加到 last_3_months 并删掉 7 月。应用中使用的索引别名,这种变化对应用没有任何影响。

基于时序数据的索引创建应该是自动进行的,系统自动创建索引就需要指定满足需求的 settings 设置和 mapping 定义。
索引模板可以用于控制何种设置(settings)应当被应用于新创建的索引:

PUT /_template/my_logs 
{
 "template": "logs_*", 
 "order": 1, 
 "settings": {
 "number_of_shards": 1 
 },
 "mappings": {
 "_default_": { 
 "_all": {
 "enabled": false
 }
 }
 }
}

这个模板指定了所有名字以 logs- 为起始的索引的默认设置,不论它是手动还是自动创建的。

基于用户的数据

比较典型的基于用户的数据:用户邮件。用户拥有自己的邮箱,各用户的邮件数量有差异,一些用户有着比其用户更多的邮件数据,一些用户可能有比其他用户更多的搜索次数。这种对索引的分片和副本数量以及使用情况的差异化需求,适合使用 “一个用户一个索引” 的模式。每个用户只在自己的索引上进行读写,即使有查看所有用户邮件数据的场景,也可以通过搜索所有用户的索引实现。

使用共享索引

一个例子可能是为一些拥仅有几千个邮箱账户的论坛提供搜索服务。一些论坛可能有巨大的流量,但大多数都很小。将一个有着单个分片的索引用于一个小规模论坛已经是足够的了 —— 一个分片可以承载很多个论坛的数据。

这种场景,可以为这些小论坛使用一个大的共享的索引,将论坛标识索引进一个字段并且将它用作一个过滤器:

PUT /forums
{
 "settings": {
 "number_of_shards": 10 
 },
 "mappings": {
 "post": {
 "properties": {
 "forum_id": { 
 "type": "string",
 "index": "not_analyzed"
 }
 }
 }
 }
}
PUT /forums/post/1
{
 "forum_id": "baking", 
 "title": "Easy recipe for ginger nuts",
 ...
}

当对单个论坛进行搜索,可以把 forum_id 用作一个过滤器来针对单个论坛进行搜索。这个过滤器可以排除索引中绝大部分的数据(属于其它论坛的数据),缓存(节点级别的缓存,节点上的所有分片共享此缓存,是 Lucene 层面的实现。缓存的是某个 filter 子查询语句在一个 segment 上的查询结果)会保证快速的响应:

GET /forums/post/_search
{
 "query": {
 "bool": {
 "must": {
 "match": {
 "title": "ginger nuts"
 }
 },
 "filter": {
 "term": {
 "forum_id": {
 "baking"
 }
 }
 }
 }
 }
}

可以进一步优化,将来自于同一个论坛的帖子可以简单地容纳于单个分片,这样可以提高查询性能。按照文档的路由公式,可以将文档分配到一个指定分片:

shard = hash(routing) % number_of_primary_shards

routing 的值默认为文档的 _id ,但可以覆盖它并且提供自定义的路由值,例如 forum_id 。 所有有着相同 routing 值的文档都将被存储于相同的分片:

PUT /forums/post/1?routing=baking 
{
 "forum_id": "baking", 
 "title": "Easy recipe for ginger nuts",
 ...
}

搜索一个指定论坛的帖子时,可以传递相同的 routing 值来保证搜索请求仅在存有我们文档的分片上执行:

GET /forums/post/_search?routing=baking 
{
 "query": {
 "bool": {
 "must": {
 "match": {
 "title": "ginger nuts"
 }
 },
 "filter": {
 "term": { 
 "forum_id": {
 "baking"
 }
 }
 }
 }
 }
}

参考

https://www.elastic.co/cn/blo...
https://www.elastic.co/guide/...
https://www.elastic.co/guide/...
https://www.elastic.co/guide/...

作者:阿白原文地址:https://segmentfault.com/a/1190000042054012

%s 个评论

要回复文章请先登录注册