Elasticsearch 近期问题汇总
目录
协调节点长时间 GC 导致查询延迟增长
很多朋友会使用 nginx 作为 elasticsearch的网关,需要注意一个问题, nginx 探活是 TCP 层面的,而非 http 层面。当其下挂的协调节点长时间 GC,甚至脱离集群,nginx不会将其摘除。导致一些无辜的查询被发到 GC 的节点,查询延迟激增。
解决方式:增加 http 层面的探活,扩容协调节点或增加其 JVM 内存。
在集群缩容后,需要考虑重新调整 total_shard_per_node
total_shard_per_node 的设置只能集群节点数量不变的情况下是正确的。当硬件故障等原因集群缩容时,需要考虑重新设置该值,否则可能会影响分片分配过程。例如 exlude 节点发现无法排除,分配不执行迁移。
7.7.0 动态模板导致大量 put mapping 问题的优化
动态模板会导致大量 put mapping,写入触发的 put mapping 通常大于 master 处理速度。该 pr 对数据节点发送 put mapping 的请求进行了限制,特定数量的请求未完成前,不会发起下一个。默认 indices.mapping.max_in_flight_updates=10
Block too many concurrent mapping updates #51038 (issue: #50670)
7.6 之前的版本 shard-started RPC 造成 Master pending_taks堆积问题
故障表现
在分片数量较多的集群,当调整分片副本数等操作时,集群无法响应 put mapping 等操作,导致业务数据无法写入
基本原理
调整分片副本数,reopen 索引等操作时,shard-started RPC。当 master 将分片指定给某个节点,节点正常打开该分片后,会通知 master “我已经正常打开”,master 就不会再考虑把他分给别人。在集群分片数量较多的集群(5w问题明显),该 RPC需要较长的处理时间(数十秒),他有次高优先级,当并行的 rebalance,reopen 操作较多时, 导致 shard-started RPC几乎完全占据了 master 的优先队列,导致其他较低优先级的任务没有机会执行,例如 put mapping
解决方案
有两个 pr 解决此问题。
- Defer reroute when starting shards(7.4)
应用此 pr 之前,Master 对 shard-started 的处理中会执行 reroute,这其实没有必要,因此将对 shard-started 处理过程中的 reroute 调用去掉,改到 shard-started 处理完成后,发起一个低优先级的 reroute。
https://github.com/elastic/elasticsearch/pull/44433
效果:shard-started task 的处理时间从原来的30-40秒降低到1秒内。reroute作为独立的正常优先级执行,比 put mapping 低一个优先级(shard-started 本身处理应该很快,只是 reroute 占据了大部分时间)
- 提升 reroute 的处理速度(7.6)
每次reroute 函数会计算所有涉及 shard routing 变更的逻辑,其中 BalancedShardsAllocator负责将分片数量在节点直接保持均匀。在此过程中需要考虑 DiskThresholdDecider,他会考虑 relocating 到节点的数据量,因此计算所有 INIT 和 RELOCATING 的分片,在此过程消耗了太多时间
下面的 pr 优化了此处理过程:
https://github.com/elastic/elasticsearch/pull/47817
效果:reroute task处理时间从原来的10-20秒降低到 1秒内(不计publish 时间)
整体效果:单个分片的 shard started 处理时间从 30 秒降低到 3 秒左右,大量 shard started 处理期间可以正在执行 put mapping,延迟为 十几秒。
例外情况:如果正常优先级的 reroute 执行时间超过 30 秒,put mapping 仍然可能会有失败的情况,但不是完全没有机会执行。reroute 的执行时间取决于同时 init 的分片数量。
磁盘空间到达 flood 水位线,客户端写入报错 read-only
故障现象
客户端写入 es 时报错失败:
1 2 3 |
Caused by: org.elasticsearch.ElasticsearchException: Elasticsearch exception [type=cluster_block_exception, reason=blocked by: [FORBIDDEN/12/index read-only / allow delete (api)];] |
es 有如下日志
1 2 3 |
flood stage disk watermark [95%] exceeded on [X-MC5Vp_QWWaVYpo0Z65yw][10.18.25.152(1)][/data00/es_data/nodes/0] free: 38.7gb[4.9%], all indices on this node will be marked read-only |
故障原因
磁盘剩余空间达到 watermark.flood_stage,索引写入被 block。需要做 2 个操恢复。
解决方式
elasticsearch 会自己执行分片迁移到正常水平,你也可以自己迁移或删除数据。当节点的磁盘可用空间恢复到正常值之后,7.4及之后的版本会将相关索引自己恢复可写。
https://github.com/elastic/elasticsearch/pull/42559
但是在较旧的版本,即使磁盘空间恢复到正常水平,相关索引仍然时只读状态,你需要手工介入,将其设置为可写:
对 _all 索引设置:
1 2 3 |
{"index.blocks.read_only_allow_delete": null} |
一般不合理的水位线设置才会出现这种情况。如果你的集群有不同规格的机器混部,建议水位线设置绝对值,而非百分比,百分比在各个节点上的空间是不一样的,会有不一样的结果。设置磁盘水位:
1 2 3 4 5 6 7 8 9 10 11 12 |
PUT _cluster/settings { "transient": { "cluster.routing.allocation.disk.watermark.low": "80gb", "cluster.routing.allocation.disk.watermark.high": "150gb", "cluster.routing.allocation.disk.watermark.flood_stage": "40gb", "cluster.info.update.interval": "1m" } } |
7.6 版本 update 后 refresh 慢,性能问题导致稳定性问题
故障现象
主分片自我恢复非常慢,或者 refresh 慢。refresh 期间的堆栈:
move 分片,或者重启节点,关闭索引等,应用集群状态时也需要 refresh,但是一直拿不到锁,从而导致节点无法处理集群状态:
故障原因
引入 softdelete 后导致的 update 性能问题,该问题触发条件是执行大量更新文档的操作,然后执行 refresh。该问题影响 7.7.0之前所有开启了 softdelete的版本。临时解决方式可以加大 refresh 频率。
解决方式
这是 Lucene 的问题:
https://github.com/elastic/elasticsearch/issues/52146
https://issues.apache.org/jira/browse/LUCENE-9228
这两个 issues 所说的”对字段更新为相同值“,在 Elasticsearch 的场景中指的就是一个 es 里的 update 操作,即:put /index/_doc/1,在 lucene 中,更新为同一个值的意思是将 _ id 字段总是更新为 1;而 lucene 执行这个更新的主要消耗,在于将 __soft_deletes 字段设置为 1
假设 update 10w 次
最内层 applyDocValuesUpdates 函数被调用的次数为 shard 中 searcable=true && doc.count>0的分段个数+1,加 1 是因为最后一次为当前要刷下去的段,因此实际上 内层的applyDocValuesUpdates会被调用 2 次,第一次为已经 refresh 下去的,已存在的 segment,第二次为当前 refresh,还没落盘的 segment
因此对于上述每个 segment,执行 applyDocValuesUpdates,该函数的目的是记录哪些 doc 的值要被更新为指定值,保存在 docIdConsumer(dvUpdates)
applyDocValuesUpdates内部有多个循环嵌套,问题版本会产生 10w*10w 次迭代,而 770 只迭代 10w 次。
由两个主要循环组成,下面的循环用于遍历所有的更新操作,对于 762版本,会迭代 10w 次,而优化后的 770版本 只循环 1 次:
1 2 3 |
while ((bufferedUpdate = iterator.next()) != null) |
下列循环迭代 10w 次,遍历倒排链:
1 2 3 |
while ((doc = docIdSetIterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { |
770 的优化方式是,在 FieldUpdatesBuffer.BufferedUpdateIterator#nextTerm 进行过滤,如果 刚刚处理的lastTerm和这次迭代到的相同,则跳过,避免后面再去调用 10w 次对倒排链的循环:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
BytesRef nextTerm() throws IOException { if (lookAheadTermIterator != null) { //nextTerm之后会将bufferedUpdate设置为nextTerm返回的值 final BytesRef lastTerm = bufferedUpdate.termValue; BytesRef lookAheadTerm; //在排序的情况下,lookAheadTermIterator和termValuesIterator是相同 while ((lookAheadTerm = lookAheadTermIterator.next()) != null && lookAheadTerm.equals(lastTerm)) { //进入这里代表这次遍历到的值和上次处理过的相同,直接跳过处理 BytesRef discardedTerm = termValuesIterator.next(); // discard as the docUpTo of the previous update is higher assert discardedTerm.equals(lookAheadTerm) : "[" + discardedTerm + "] != [" + lookAheadTerm + "]"; assert docsUpTo[getArrayIndex(docsUpTo.length, termValuesIterator.ord())] <= bufferedUpdate.docUpTo : docsUpTo[getArrayIndex(docsUpTo.length, termValuesIterator.ord())] + ">" + bufferedUpdate.docUpTo; } } return termValuesIterator.next(); |
(转载请注明作者和出处 easyice.cn ,请勿用于任何商业用途)