Elasticsearch集群性能优化实践

2021-11-19 15:26:10 浏览数 (1)

简介

典型的几种集群规划问题:

  • 节点规格规划问题:集群数量很大,但是每个节点的配置很低;
  • 索引分片规划问题:索引很小,但是设置了几十个分片,或者索引很大,只设置了两三个分片;
  • 分片数量规划问题:集群中包含 10万 的分片。

集群规模及索引规划

集群规模评估

集群规模的评估主要评估以下三个方面:

  • 计算资源评估:计算资源的评估主要是评估单节点的CPU和内存。ES的计算资源一般消耗在写入和查询过程,经过总结大量ES集群的运维经验,2C8G 的配置大概能支持 5k doc/s 的写入,32C64G 的配置大概能支撑 5w doc/s的写入能力;
  • 存储资源评估存储资源的评估主要是评估磁盘的类型及容量大小。例如ES集群使用什么类型的磁盘,SSD或者高性能云盘,以及每块盘的容量大小,是选择单盘多容量,还是多盘少容量。而对于冷热分离的集群,则默认使用SSD作为热节点,高性能云盘作为温节点。另外腾讯云ES支持单节点挂载多块云硬盘,且经过性能压测,3块盘相比于1块盘,吞吐量大约有2.8倍的提升。因此如果对写入速度及IO性能要求较高,可选择挂载多块 SSD 磁盘;
ES冷热分离多盘集群示意图ES冷热分离多盘集群示意图
  • 节点数量评估:节点数量的评估主要是评估集群数据节点的数量。在同等集群性能的情况下,建议优先选择高配置少节点的集群。例如 32C64G*3 节点的集群相比于 8C16G*12 节点的集群,在集群稳定性和扩容的便捷性上都有一定的优势。因为高配置的集群如果遇到性能瓶颈需要扩容,则只需要横向扩容,即向集群中加入更多同等配置的节点即可;而低配置的集群在扩容节点配置时,则需要纵向扩容。目前云上的纵向扩容方式有两种:第一种是滚动重启方式扩容,这对集群稳定性会有一定的影响。第二种是数据迁移方式扩容,其原理是先向集群中加入同等数量的高配置节点,然后将低配置节点上的数据迁移到新节点上,最后再将低配置节点剔除集群,所以这种扩容流程时间会比较长,且成本较高。
数据迁移方式纵向扩容示意图数据迁移方式纵向扩容示意图

评估标准

集群规模评估主要根据以下三点来评估:

  • 具体的业务场景,如日志分析、指标监控、搜索业务;
  • 业务预计的查询及写入QPS;
  • 索引的数据总量。

集群规模评估准则

这里结合我们的运维经验,给出集群规模评估的几点参考建议:

  • 32C64G单节点配置通常可承载5W次/s的写入;
  • 写入量和数据量较大时,优先选择32C64G的节点配置;
  • 1T的数据量预计需消耗2-4GB的内存空间;
  • 搜索场景优先选择大内存节点配置
  • 存储容量 = 源数据 * (1 副本数量) * 1.45 * (1 预留空间) ≈ 源数据 * (1 副本数量) * 2.2

索引配置评估

索引配置的评估主要评估两点:

  • 如何划分索引在使用 index 时,建议做好定期切换索引的计划。对于日志场景来说,写入不大的情况下建议按天创建索引,而写入较大的情况下,则建议按小时创建索引。定期滚动索引的好处主要包括:能够控制单个索引的大小,提升读写性能;同时能够防止单个索引太大,影响故障恢复的时间;另外也能避免热索引过大,从而影响快照备份恢复的时间。
  • 如何设置索引主分片数:云上的索引主分片数默认是5个,具体的大小则需要业务根据具体的场景及数据量来优化。

索引配置评估准则

索引配置的评估同样也要结合具体的业务场景及索引的数据量来评估,尤其是单日新增的数据量。

索引配置的评估可根据下面几点准则进行评估:

  • 单个分片大小控制在 30-50GB
  • 集群总分片数量控制在 3w 以内
  • 1GB 的内存空间支持 20-30 个分片为佳
  • 一个节点建议不超过 1000 个分片
  • 索引分片数量建议和节点数量保持一致
  • 集群规模较大时建议设置专用主节点
  • 专用主节点配置建议在 8C16G 以上
  • 如果是时序数据,建议结合冷热分离 ILM 索引生命周期管理

特别需要说明的是集群分片总数的大小控制上,我们经过一些性能测试发现,当集群的总分片数超过 10w 个以后,创建索引时间会增长到分钟级。尤其是对于写入量在百万 qps 以上的集群。如果总分片数在 10W ,且索引是自动创建的,那么就经常会在每次切换新索引时候,出现写入陡降、集群不可用的情况。下面这张图是云上一个 100个节点,总分片数在 11W 的集群。每天 8点切换新索引时,写入直接掉0,集群不可用时间在数小时不等。

对于这种问题,我们腾讯云ES团队也有一些非常成熟的优化方案。其中对于每天八点切换新索引时写入陡降的问题,可通过提前创建索引来解决,且建议使用固定的 index mapping。避免大量的 put-mapping 元数据更新操作。因为对于这种节点数量和总分片数量都很大的集群来说,更新元数据是一个非常消耗性能的操作;对于总分片数超过 10W 的问题,这种一般在日志分析场景中较为常见,如果历史数据不是很重要,则可定期删除历史索引即可。而对于历史数据较为重要,任何数据都不能删除的场景,则可通过冷热分离架构 索引生命周期管理功能,将7天之前的数据存储到温节点,且在索引数据从热节点迁移到温节点时,通过 Shrink 来将主分片个数降低到一个较小的值,并且可将温节点数据通过快照方式备份到腾讯云COS中,然后将温节点上索引的副本设置为0,这样便可进一步降低集群中的总分片数量。

ES写入性能优化

ES集群的写入性能受到很多因素的影响,下面是一些写入性能方面的优化建议:

doc_id优化

索引中每一个 doc 都有一个全局唯一的 doc_id,这个 doc_id 可自定义,也可让ES自动生成。如果自定义的话,则ES在写入过程中会多一步判断的过程,即先Get下该 doc_id 是否已经存在,如果存在的话则执行 Update 操作,不存在则创建新的 doc。因此如果我们对索引 doc_id 没有特别要求,则建议让ES自动生成 doc_id,这样可提升一定的写入性能;

提前创建索引

对于规模较大的集群,建议提前创建好索引,且使用固定的 Index mapping

这一条优化建议在上面也提到了,因为创建索引及新加字段都是更新元数据操作,需要 master 节点将新版本的元数据同步到所有节点,因此在集群规模比较大,写入qps较高的场景下,特别容易出现master更新元数据超时的问题,这可导致 master 节点中有大量的 pending_tasks 任务堆积,从而造成集群不可用,甚至出现集群无主的情况。

更新集群元数据超时更新集群元数据超时
集群大量pending_tasks任务堆积集群大量pending_tasks任务堆积

增加 refresh_interval

对于数据实时性要求不高的场景,适当增加 refresh_interval 时间

ES默认的 refresh_interval 是1s,即 doc 写入1s后即可被搜索到,如果业务对数据实时性要求不高的话,如日志场景,可将索引模版的 refresh_interval 设置成30s,这能够避免过多的小 segment 文件的生成及段合并的操作。

设置单副本

对于追求写入效率的场景,可以将正在写入的索引设置为单副本,写入完成后打开副本

越来越多的外部客户正选择将自建的ES集群迁移到腾讯云上来,客户通常是使用 logstash 来迁移数据,由于自建集群中完整保留了数据,因此这时候可以将云上的正在写入的索引副本设置为0, 这样可最快完成集群迁移工作。数据迁移完成后再将副本打开即可。

Bulk 批量写入

使用 Bulk 接口批量写入数据,每次 bulk 数据量大小控制在 10M 左右

ES为了提升写入性能,提供了 Bulk 批量写入的API,通常客户端会准备好一批数据往ES中写入,ES收到 Bulk 请求后则根据routing 值进行分发,将该批数据组装成若干分子集,然后异步得发送给各分片所在的节点,这样能够大大降低写入请求时的网络交互和延迟。通常我们建议一次Bulk的数据量控制在10M以下,一次Bulk的doc数在 10000 上下浮动。

ES Bulk请求示意图ES Bulk请求示意图

自定义 routing

使用自定义 routing 功能,尽量将请求转发到较少的分片

上面我们提到ES提供了Bulk接口支持将数据批量写入到索引。虽然协调节点是异步得将数据发送给所有的分片,但是却需要等待所有的分片响应后才能返回给客户端,因此一次Bulk的延迟则取决于响应最慢的那个分片所在的节点。这就是分布式系统的长尾效应。因此,我们可以自定义 routing 值,将一次Bulk尽量转发到较少的分片上。

代码语言:javascript复制
POST _bulk?routing=user_id
自定义routing自定义routing

磁盘类型

尽量选择 SSD 磁盘类型,并且可选择挂载多块云硬盘

云上目前提供多种类型的磁盘可用选择,其中1T的 SSD 云盘吞吐量为 260M/s,高性能云盘为 150M/s。因此使用SSD磁盘对于写入性能和IO性能都会有一定的提升,另外腾讯云现在也提供了多盘的能力,相对于单盘节点来说,3块盘的吞吐量大约有2.8倍的提升。

冻结历史索引

冻结历史索引,释放更多的内存空间

我们知道ES的索引有三种状态,分别是 Open状态、Frozen状态和 Close状态。如下图所示:

ES索引的三种状态ES索引的三种状态

Open状态的索引由于是通过将倒排索引以FST数据结构的方式加载进内存中,因此索引是能够被快速搜索的,且搜索速度也是最快的。但是需要消耗大量的内存空间,且这部分内存为常驻内存,不会被GC的。1T的索引预计需要消耗2-4GB的JVM堆内存空间;

Frozen状态的索引特点是可被搜索,但是由于它不占用内存,只是存储在磁盘上,因此冻结索引的搜索速度是相对比较慢的。如果我们集群中的数据量比较大,历史数据也不能被删除,则可以考虑使用下面的API将历史索引冻结起来,这样便可释放出较多的内存空间。

代码语言:javascript复制
POST /index_name/_freeze

对于冻结索引的搜索,可以在API中指定 ignore_throttled=false 参数:

代码语言:javascript复制
GET /index_name/_search?ignore_throttled=false
{
 "query": {
   "match": {
     "name": "wurong"
   }
 }
}

上面介绍了一些较为常见的写入性能优化的建议和经验,但是更为高效的优化还需要结合具体的业务场景和集群规模。

ES集群常规运维经验总结

查看集群健康状态

ES集群的健康状态分为三种,分别是Green、Yellow和Red。

  • Green(绿色):全部主&副本分片分配成功;
  • Yellow(黄色):至少有一个副本分片未分配成功;
  • Red(红色):至少有一个主分片未分配成功。

我们可以通过下面的API来查询集群的健康状态及未分配的分片个数。

代码语言:javascript复制
GET _cluster/health
{
  "cluster_name": "es-xxxxxxx",
  "status": "yellow",
  "timed_out": false,
  "number_of_nodes": 103,
  "number_of_data_nodes": 100,
  "active_primary_shards": 4610,
  "active_shards": 9212,
  "relocating_shards": 0,
  "initializing_shards": 0,
  "unassigned_shards": 8,
  "delayed_unassigned_shards": 0,
  "number_of_pending_tasks": 0,
  "number_of_in_flight_fetch": 0,
  "task_max_waiting_in_queue_millis": 0,
  "active_shards_percent_as_number": 99.91323210412148
}

其中需要重点关注的几个字段有 status、number_of_nodes、unassigned_shards 和 number_of_pending_tasks。

number_of_pending_tasks 这个字段的如果很高的话,通常是由于 master 节点触发的元数据更新操作,部分节点响应超时导致的大量的任务堆积。

我们可以通过下面的API来查看具体有那些 task 需要执行:

代码语言:javascript复制
GET /_cat/pending_tasks
insertOrder timeInQueue priority source
       1685       855ms HIGH     update-mapping [foo][t]
       1686       843ms HIGH     update-mapping [foo][t]
       1693       753ms HIGH     refresh-mapping [foo][[t]]
       1688       816ms HIGH     update-mapping [foo][t]

其中 priority 字段则表示该 task 的优先级,翻看 ES 的源码可以看到一共有六种优先级。

代码语言:javascript复制
IMMEDIATE((byte) 0),
URGENT((byte) 1),
HIGH((byte) 2),
NORMAL((byte) 3),
LOW((byte) 4),
LANGUID((byte) 5);

查看分片未分配原因

当集群Red时候,我们可以通过下面的API来查看分片未分配的原因。

代码语言:javascript复制
GET _cluster/allocation/explain
查看分片未分配的原因查看分片未分配的原因

其中 index和shard 列出了具体哪个索引的哪个分片未分配成功。reason 字段则列出了哪种原因导致的分片未分配。这里也将所有可能的原因列出来:

代码语言:javascript复制
1)INDEX_CREATED:由于创建索引的API导致未分配。

2)CLUSTER_RECOVERED :由于完全集群恢复导致未分配。

3)INDEX_REOPENED :由于打开open或关闭close一个索引导致未分配。

4)DANGLING_INDEX_IMPORTED :由于导入dangling索引的结果导致未分配。

5)NEW_INDEX_RESTORED :由于恢复到新索引导致未分配。

6)EXISTING_INDEX_RESTORED :由于恢复到已关闭的索引导致未分配。

7)REPLICA_ADDED:由于显式添加副本分片导致未分配。

8)ALLOCATION_FAILED :由于分片分配失败导致未分配。

9)NODE_LEFT :由于承载该分片的节点离开集群导致未分配。

10)REINITIALIZED :由于当分片从开始移动到初始化时导致未分配(例如,使用影子shadow副本分片)。

11)REROUTE_CANCELLED :作为显式取消重新路由命令的结果取消分配。

12)REALLOCATED_REPLICA :确定更好的副本位置被标定使用,导致现有的副本分配被取消,出现未分配。

detail 字段则列出了更为详细的未分配的原因。下面我会总结下在日常运维工作中常见的几种原因。

如果未分配的分片比较多的话,我们也可以通过下面的API来列出所有未分配的索引和主分片:

代码语言:javascript复制
GET /_cat/indices?v&health=red

常见分片未分配原因总结

磁盘满了

代码语言:javascript复制
the node is above the high watermark cluster setting [cluster.routing.allocation.disk.watermark.high=95%], using more disk space than the maximum allowed [95.0%], actual free: [4.055101177689788%]

当我们执行 _cluster/allocation/explain 命令后看到上面的一行语句的话,则可以判断是该索引主分片所在的节点磁盘满了。

解决方法:

扩容磁盘提升磁盘容量或者删除历史数据释放磁盘空间。

通常如果磁盘满了,ES为了保证集群的稳定性,会将该节点上所有的索引设置为只读。ES 7.x版本之后当磁盘空间提升后可自动解除,但是7.x版本之前则需要手动执行下面的API来解除只读模式:

代码语言:javascript复制
PUT index_name/_settings
{
 "index": {
   "blocks": {
     "read_only_allow_delete": null
    }
  }
}

分片的文档数超过了21亿条限制

代码语言:javascript复制
failure IllegalArgumentException[number of documents in the index cannot exceed 2147483519

该限制是分片维度而不是索引维度的。因此出现这种异常,通常是由于我们的索引分片设置的不是很合理。

解决方法:

切换写入到新索引,并修改索引模版,合理设置主分片数

主分片所在节点掉线

代码语言:javascript复制
cannot allocate because a previous copy of the primary shard existed but can no longer be found on the nodes in the cluster

解决方法:

找到节点掉线原因并重新启动节点加入集群,等待分片恢复。

索引所需属性和节点属性不匹配

代码语言:javascript复制
node does not match index setting [index.routing.allocation.require] filters [temperature:"warm",_id:"comdNq4ZSd2Y6ycB9Oubsg"]

解决方法:

重新设置索引所需的属性,和节点保持一致。因为如果重新设置节点属性,则需要重启节点,代价较高。例如通过下面的API来修改索引所需要分配节点的温度属性:

代码语言:javascript复制
PUT /index_name/_settings
{
  "index": {
    "routing": {
      "allocation": {
        "require": {
          "temperature": "warm"
        }
      }
    }
  }
}

节点长时间掉线后重新加入集群,引入了脏数据

代码语言:javascript复制
cannot allocate because all found copies of the shard are either stale or corrupt

解决方法:

通过reroute API来重新分配一个主分片:

代码语言:javascript复制
POST _cluster/reroute?pretty" -d '{
    "commands" : [
        {
          "allocate_stale_primary" : {
              "index" : "article", 
              "shard" : 1,
              "node" : "98365000222032",
              "accept_data_loss": true
          }
        }
    ]
}

未分配分片太多,达到了分片恢复的阈值,其他分片排队等待

代码语言:javascript复制
reached the limit of incoming shard recoveries [2], cluster setting [cluster.routing.allocation.node_concurrent_incoming_recoveries=2] (can also be set via [cluster.routing.allocation.node_concurrent_recoveries])

这种情况通常出现在集群重启,或者某一个节点重启后。且由于设置的分片并发恢复的值较低导致。为了尽快恢复集群健康状态。解决方法:

可以通过调用下面的API来提升分片恢复的速度和并发度:

代码语言:javascript复制
PUT /_cluster/settings
{
    "transient" : {
        "cluster.routing.allocation.node_concurrent_recoveries": "20",
        "indices.recovery.max_bytes_per_sec": "100mb"
    }
}

总结

本文介绍了集群规模和索引配置规划的评估准则,依据这些准则提前规划集群,可以保证集群的稳定性和可用性,简化复杂的运维工作。另外介绍了一些常见的写入性能优化的建议和方法。能够进一步提升集群的写入性能和稳定性。最后介绍了日常运维工作中常见的排查集群问题的方法和思路。希望本文能够帮助到腾讯云的每一个ES客户。

0 人点赞