es里的_update_by_query原理 es update性能_迭代

1.概述

7.6 版本 update 后 refresh 慢,性能问题导致稳定性问题

故障现象 主分片自我恢复非常慢,或者 refresh 慢。refresh 期间的堆栈:

es里的_update_by_query原理 es update性能_字段_02

move 分片,或者重启节点,关闭索引等,应用集群状态时也需要 refresh,但是一直拿不到锁,从而导致节点无法处理集群状态:

es里的_update_by_query原理 es update性能_lucene_03

故障原因

引入 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 次:

while ((bufferedUpdate = iterator.next()) != null)

下列循环迭代 10w 次,遍历倒排链:

while ((doc = docIdSetIterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {

770 的优化方式是,在 FieldUpdatesBuffer.BufferedUpdateIterator#nextTerm 进行过滤,如果 刚刚处理的lastTerm和这次迭代到的相同,则跳过,避免后面再去调用 10w 次对倒排链的循环:

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();