50

Elasticsearch 最佳实践系列之分片恢复并发故障

 5 years ago
source link: https://mp.weixin.qq.com/s/_Ug8x22eeXHZ4_CEhwcepA?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

大家好,今天为大家分享一次 ES 的填坑经验。主要是关于集群恢复过程中,分片恢复并发数调整过大导致集群 hang 住 的问题。

一、场景描述

废话不多说,先来描述场景。某日,腾讯云某 ES 集群,15个节点,2700+ 索引,15000+ 分片,数十 TB 数据。由于机器故障,某个节点被重启,此时集群有大量的 unassigned 分片,集群处于 yellow 状态。为了加快集群恢复的速度,调整分片恢复并发数,将默认值为2的 node_concurrent_recoveries 调整为100:

curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d'
{
    "persistent": {
        "cluster.routing.allocation.node_concurrent_recoveries": 100,
        "indices.recovery.max_bytes_per_sec": "40mb"
    }
}
'

二、基本概念

设定之后,观察集群 unassigned 分片,一开始下降的速度很快。大约几分钟后,数量维持在一个固定值不变了,然后,然后就没有然后了,集群所有节点 generic 线程池卡死,虽然已存在的索引读写没问题,但是新建索引以及所有涉及 generic 线程池的操作全部卡住。立马修改分片恢复并发数到10,通过管控平台一把重启了全部节点,约15分钟后集群恢复正常。接下来会先介绍一些基本的概念,然后再重现这个问题并做详细分析。

三、 ES 线程池(thread pool)

ES 中每个节点有多种线程池,各有用途。重要的有:

  • generic:通用线程池,后台的 node discovery,上述的分片恢复(node recovery)等等一些通用后台的操作都会用到该线程池。该线程池线程数量默认为配置的处理器数量(processors)* 4,最小128,最大512。

  • index:index/delete 等索引操作会用到该线程池,包括自动创建索引等。默认线程数量为配置的处理器数量,默认队列大小:200.

  • search:查询请求处理线程池。默认线程数量:int((# of available_processors * 3) / 2) + 1,默认队列大小:1000.

  • get:get 请求处理线程池。默认线程数量为配置的处理器数量,默认队列大小:1000.

  • write:单个文档的 index/delete/update 以及 bulk 请求处理线程。默认线程数量为配置的处理器数量,默认队列大小:200,在写多的日志场景我们一般会将队列调大。

    还有其它线程池,例如备份回档(snapshot)、analyze、refresh 等,这里就不一一介绍了。详细可参考官方文档:

    https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-threadpool.html

四、集群恢复之分片恢复

我们知道 ES 集群状态分为三种,green、yellow、red。green 状态表示所有分片包括主副本均正常被分配;yellow 状态表示所有主分片已分配,但是有部分副本分片未分配;red 表示有部分主分片未分配。

一般当集群中某个节点因故障失联或者重启之后,如果集群索引有副本的场景,集群将进入分片恢复阶段(recovery)。此时一般是 master 节点发起更新集群元数据任务,分片的分配策略由 master 决定,具体分配策略可以参考腾讯云+社区的这篇文章了解细节: https://cloud.tencent.com/developer/article/1334743 。各节点收到集群元数据更新请求,检查分片状态并触发分片恢复流程,根据分片数据所在的位置,有多种恢复的方式,主要有以下几种:

  • EXISTING_STORE: 数据在节点本地存在,从本地节点恢复。

  • PEER:本地数据不可用或不存在,从远端节点(源分片,一般是主分片)恢复。

  • SNAPSHOT: 数据从备份仓库恢复。

  • LOCAL_SHARDS: 分片合并(缩容)场景,从本地别的分片恢复。

PEER 场景分片恢复并发数主要由如下参数控制:

  • cluster.routing.allocation.node_concurrent_incoming_recoveries:节点上最大接受的分片恢复并发数。一般指分片从其它节点恢复至本节点。

  • cluster.routing.allocation.node_concurrent_outgoing_recoveries:节点上最大发送的分片恢复并发数。一般指分片从本节点恢复至其它节点。

  • cluster.routing.allocation.node_concurrent_recoveries:该参数同时设置上述接受发送分片恢复并发数为相同的值。

    详细参数可参考官方文档:

    https://www.elastic.co/guide/en/elasticsearch/reference/current/shards-allocation.html

集群卡住的主要原因就是从远端节点恢复(PEER)的并发数过多,导致 generic 线程池被用完。涉及目标节点(target)和源节点(source)的恢复交互流程,后面分析问题时我们再来详细讨论。

五、 问题复现与剖析

为了便于描述,我用 ES 6.4.3版本重新搭建了一个三节点的集群。单节点 1 core,2GB memory。新建了300个 index, 单个 index 5个分片一个副本,共 3000 个 shard。每个 index 插入大约100条数据。

curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d'
{
    "persistent": {
        "cluster.routing.allocation.node_concurrent_recoveries": 200 // 设定分片恢复并发数
    }
}
'

接下来停掉某节点,模拟机器挂掉场景。几分钟后,观察集群分片恢复数量,卡在固定数值不再变化:

a63mqyU.jpg!web

通过 allocation explain 查看分片分配状态,未分配的原因是受到最大恢复并发数的限制:

a63mqyU.jpg!web

观察线程池的数量,generic 线程池打满128.

a63mqyU.jpg!web

此时查询或写入已有索引不受影响,但是新建索引这种涉及到 generic 线程池的操作都会卡住。

通过堆栈分析,128 个 generic 线程全部卡在 PEER recovery 阶段。

a63mqyU.jpg!web

现象有了,我们来分析一下这种场景,远程分片恢复(PEER Recovery)流程为什么会导致集群卡住。

当集群中有分片的状态发生变更时,master 节点会发起集群元数据更新(cluster state update)请求给所有节点。其它节点收到该请求后,感知到分片状态的变更,启动分片恢复流程。部分分片需要从其它节点恢复,代码层面,涉及分片分配的目标节点(target)和源节点(source)的交互流程如下:

a63mqyU.jpg!web

6.x 版本之后引入了 seqNo,恢复会涉及到 seqNo+translog,这也是6.x提升恢复速度的一大改进。我们重点关注流程中第 2、4、5、7、10、12 步骤中的远程调用,它们的作用分别是:

  • 第2步:分片分配的目标节点向源节点(一般是主分片)发起分片恢复请求,携带起始 seqNo 和 syncId。

  • 第4步:发送数据文件信息,告知目标节点待接收的文件清单。

  • 第5步:发送 diff 数据文件给目标节点。

  • 第7步:源节点发送 prepare translog 请求给目标节点,等目标节点打开 shard level 引擎,准备接受 translog。

  • 第10步:源节点发送指定范围的 translog 快照给目标节点。

  • 第12步:结束恢复流程。

我们可以看到除第5步发送数据文件外,多次远程交互 submitRequest  都会调用 txGet,这个调用底层用的是基于 AQS 改造过的 sync 对象,是一个同步调用。 如果一端 generic 线程池被这些请求打满,发出的请求等待对端返回,而发出的这些请求由于对端 generic 线程池同样的原因被打满,只能 pending 在队列中,这样两边的线程池都满了而且相互等待对端队列中的线程返回,就出现了分布式死锁现象。

六、问题处理

为了避免改动太大带来不确定的 side effect,针对腾讯云 ES 集群我们目前先在 rest 层拒掉了并发数超过一定值的参数设定请求并提醒用户。与此同时,我们向官方提交了 issue:https://github.com/elastic/elasticsearch/issues/36195进行跟踪。

七、总结

本文旨在描述集群恢复过程出现的集群卡死场景,避免更多的 ES 用户踩坑,没有对整体分片恢复做详细的分析,大家想了解详细的分片恢复流程可以参考这篇文章:https://cloud.tencent.com/developer/article/1370385。

完结,谢谢!

U7FR7zI.jpg!web

C D B C T S D B C K V C M o n g o  


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK