关于重建索引 API 使用和故障排查的 3 个最佳实践

2024-05-09 16:24:53 浏览数 (1)

重建索引API功能:

  • 在集群之间传输数据 
  • 重新定义、更改和/或更新映射
  • 通过采集管道进行处理和编制索引
  • 通过清除已删除的文档回收存储空间
  • 通过查询筛选器将大型索引拆分成较小的索引组

常见问题处理

症状:Kibana 开发工具中显示“backend closed connection”(后端已关闭连接)

问题

您的客户端将在 N 秒后关闭非活动套接字;以 Kibana 为例,如果重建索引操作无法在 120 秒内(v7.13 中默认的 server.socketTimeout 值)完成,您将看到“backend closed connection”(后端已关闭连接)消息。

解决方案 #1 - 获取在集群上运行的任务列表

其实这并不是问题,即使您在 Kibana 中看到这条消息,Elasticsearch 也会在后台运行重建索引 API。

您可以使用 _task API 跟踪重建索引 API 的执行情况,并查看所有指标:

代码语言:javascript复制
GET _tasks?actions=*reindex&wait_for_completion=false&detailed

这个 API 将向您显示当前在 Elasticsearch 集群中运行的所有重建索引 API,如果您在此列表中没有看到您的重建索引 API,即表明它已完成。

解决方案 #2 - 将重建索引结果存储在 _tasks 上

如果已知重建索引操作需要的时间超过 120 秒(120 秒是 Kibana 开发工具的超时时间),可以使用查询参数 wait_for_completion= false 来存储重建索引 API 的结果,这样您就能使用 _task API 来获取重建索引 API 结束时的状态(也可以从“.tasks”索引获取文档,如 wait_for_completion=false 的文档中所述)。

代码语言:javascript复制
POST _reindex?wait_for_completion=false
{
  "conflicts": "proceed",
  "source": {
    "index": "<source-index-name>"
  },
  "dest": {
    "index": "<dest-index-name>"
  }
}

当您使用“wait_for_completion=false”执行重建索引时,响应将类似于以下内容:

代码语言:javascript复制
{
  "task" : "a9Aa_I_ZSl-4bjR5vZLnSA:247906"
}

您需要保留这里提供的任务,搜索重建索引的结果时会用到(您将看到已创建的文档数、冲突甚至是错误;完成后,您将看到所花费的时间、批次数等等):

代码语言:javascript复制
GET _tasks/a9Aa_I_ZSl-4bjR5vZLnSA:247906

症状:_task API 列表中没有您的重建索引 API。

如果使用上文提到的 API 无法找到重建索引 API 操作,可能这又是另一个问题,下面我们一个一个地解决。

问题

如果重建索引 API 不在列表中,即表明操作已完成,因为没有更多的文档需要重建索引,或者是因为出现了错误。

我们将使用 _cat count API 来查看存储在两个索引中的文档数量,如果两个数值不同,则表明您的重建索引 API 执行已失败。

代码语言:javascript复制
GET _cat/count/<source-index-name>?h=count
GET _cat/count/<dest-index-name>?h=count

您需要将 / 替换为您在重建索引 API 中使用的索引名称。

解决方案 #1 - 这是一个冲突问题

最常见的错误之一是存在冲突,默认情况下,如果有冲突,重建索引 API 将中止。

现在,我们有两个选择:

  1. 将“conflicts”设置为“proceed”,这样重建索引 API 将忽略无法索引的文档,转而索引其他文档。
  2. 或者,我们也可以选择修复冲突,这样就可以为所有文档重建索引。

第一个选择中的冲突设置如下所示:

代码语言:javascript复制
POST _reindex
{
  "conflicts": "proceed",
  "source": {
    "index": "<source-index-name>"
  },
  "dest": {
    "index": "<dest-index-name>"
  }
}

或者,在第二个选择中,我们将搜索并修复产生冲突的错误:

  • 避免这一问题的最佳实践是在目标索引上定义映射或模板。这些错误中 99% 是源索引和目标索引之间的字段类型不匹配。
  • 如果在定义了映射或模板后,问题仍然存在,则表明某些文档可能无法建立索引,并且默认情况下不会记录错误。我们需要启用记录器,以便在 Elasticsearch 日志中查看错误。
代码语言:javascript复制
PUT /_cluster/settings
{
  "transient": {
    "logger.org.elasticsearch.action.bulk.TransportShardBulkAction":"DEBUG"
  }
}
  • 启用记录器后,我们需要再执行一次重建索引 API,如果可能的话,将“conflicts”设置为“proceed”,因为很多时候源索引和目标索引之间会有多个字段发生冲突。
  • 现在,重建索引 API 正在运行,我们将在日志中使用 grep 命令查找/搜索“failed to execute bulk item”或“MapperParsingException”
代码语言:javascript复制
failed to execute bulk item (index) index {[my-dest-index-00001][_doc][11], source[{
  "test-field": "ABC"
}

代码语言:javascript复制
 "org.elasticsearch.index.mapper.MapperParsingException: failed to parse field [test-field] of type [long] in document with id '11'. Preview of field's value: 'ABC'",
                "at org.elasticsearch.index.mapper.FieldMapper.parse(FieldMapper.java:216) ~[elasticsearch-7.13.4.jar:7.13.4]",

通过这个堆栈跟踪,我们已经有足够的信息来理解冲突是什么。在我的重建索引 API 中,目标索引有一个名为 [test-field] 的字段,类型为 [long],重建索引 API 尝试将该字段设置为字符串“ABC”(您可以用自己的内容字段替换“ABC”)。

在 Elasticsearch 中,字段数据类型是可以定义的,您可以在索引创建期间或使用模板设置这些类型。索引创建完成后,类型便不能更改,您需要先删除目标索引,然后使用之前提供的选项来设置新的固定映射。

  • 修复了错误之后,请记得将记录器转换为没那么冗长的模式:
代码语言:javascript复制
PUT /_cluster/settings
{
  "transient": {
    "logger.org.elasticsearch.action.bulk.TransportShardBulkAction":NULL
  }
}

解决方案 #2 - 没有冲突错误,但重建索引仍然失败

如果在重建索引执行期间,您在 Elasticsearch 日志中发现以下跟踪:

代码语言:javascript复制
'search_phase_execution_exception', 'No search context found for id [....]')

可能有很多种原因:

  1. 集群存在一些不稳定问题,并且在重建索引执行过程中,一些数据节点进行了重启或不可用。 如果是这个原因,在运行重建索引之前,请确保集群是稳定的,且所有数据节点都运行良好。
  2. 如果您是远程执行重建索引操作,并且已知节点之间的网络不可靠:
    • 建议选择快照 API(如本文结尾处所述)。
  3. 我们可以尝试对重建索引 API 执行手动切片,该操作可以将请求过程分割成较小的部分(当我们在同一集群中使用重建索引 API 时,可以使用这个选项)。
  4. 如果您的 Elasticsearch 集群存在过度分片、资源利用率高或垃圾收集问题,可能会在滚动搜索查询过程中出现超时。默认的滚动超时值为 5 分钟,因此,您可以尝试将重建索引 API 上的滚动设置为一个更高的值。
代码语言:javascript复制
POST _reindex?scroll=2h
{
 "source": {
"index": "<source-index-name>"
  },
  "dest": {
"index": "<dest-index-name>"
  }
}

症状:Elasticsearch 日志中显示“节点未连接”

我们始终建议在集群稳定且状态为绿色的情况下运行重建索引 API,集群需要足够的容量才能运行搜索和索引操作。

问题

代码语言:javascript复制
NodeNotConnectedException[[node-name][inet[ip:9300]] Node not connected]; ]

如果您的日志中出现这类错误,表明您的集群存在稳定性和连接问题,而不仅仅是重建索引 API 出现了问题。

但不妨设想一下,已知存在连接问题,但是需要运行重建索引 API,该怎么办。

解决方案

修复连接问题。

但是,假设我们知道有连接问题,可是需要运行重建索引 API,我们可以减少失败的可能性,不过这不是修复操作,并不是在所有情况下都有效。

  • 将源索引或目标索引(主索引或副本)的分片移出存在连接问题的节点。使用分配筛选 API 移动分片。
  • 您也可以移除目标索引上的副本(仅针对目标索引),这将加快重建索引 API 的执行速度,毕竟重建索引的运行速度越快,出现故障的可能性就越小。
代码语言:javascript复制
PUT /<dest-index-name>/_settings
{
  "index" : {
    "number_of_replicas" : 0
  }
}

如果这两种操作都无法让您成功执行重建索引 API,表明您需要先修复稳定性问题。 

症状:日志中没有错误,但两个索引的文档计数不一致

有时,重建索引 API 已经完成,但是源索引与目标索引中的文档计数不一致。

问题

如果我们尝试在一个目标中从多个源重建索引(即在一个目标中合并多个索引),问题可能源自您为这些文档分配的 _id。

假设我们有 2 个源索引:

  • 索引 A,_id:1,信息:“Hello A”
  • 索引 B,_id:1,信息:“Hello B”

两个索引在 C 中合并后:

  • 索引 C,_id:1,信息:“Hello B”

两个文档的 _id 相同,因此,最后索引的文档将覆盖前一个索引的文档。

解决方案

您可以选择不同的采集管道,也可以在重建索引 API 中使用 Painless。在这篇博文中,我们将使用脚本选项,在请求正文中使用“Painless”。

操作起来非常简单,只需使用原始 _id 并添加源索引名称:

代码语言:javascript复制
POST _reindex
{
  "source": {
    "index": "<source-index-name>"
  },
  "dest": {
    "index": "<dest-index-name>"
  },
  "script": {
    "source": "ctx._id = ctx._id   '-'   ctx._index;"
  }
}

还是前面的示例:

  • 索引 A,_id:1,信息:“Hello A”
  • 索引 B,_id:1,信息:“Hello B”

两个索引在 C 中合并后:

  • 索引 C,_id:1-A,信息:“Hello A”
  • 索引 C,_id:1-B,信息:“Hello B”

最佳实践

并发切片与size设置

Reindex支持切片滚动,以并行重建进程。这种并行化可以提高效率,并提供一种方便的方式将请求分解为更小的部分。默认reindex 读取源索引的 batch_size 为1000,这个size的使用限制于系统资源的分配,可以调整到10000增加速度.并发数主要通过url后的slices参数控制,下面的例子里 size设置为1w,并发数为5 .

代码语言:javascript复制
POST _reindex?slices=5&refresh
{
  "conflicts": "proceed",
  "source": {
    "index": "my-index-000001",
    "size": 10000,
    "query":{
       "range":{
         "FIELD": {
            "gte": 10,
            "lte": 20
          }
       }
     }
  },
  "dest": {
    "index": "my-new-index-000001"
  }
}

怎么选择切片的数量

  1. 使用自动切片,将切片设置为auto, ES 会自行设置一个合理的数字。
  2. 设置slice数量与索引中的分片数量相等时,查询性能是最有效的。通常情况下,将slice的数量设置为高于shard的数量并不会提高效率,反而会增加开销。

在生产使用中,请通过测试设置合理的slice数量提高reindex的效率。

实际测试中,一个1.5tb的24分片索引(集群配置32c64g,24节点,索引1副本,目标索引未设置副本),使用48 slice 需要2小时完成,24 slice 则需要3小时。

reindex 减索引字段

在 source 的 query 中限定 includes 的字段,这样可以在 reindex 时去除原索引不需要的字段

代码语言:javascript复制
POST _reindex?slices=5&refresh
{
  "conflicts": "proceed",
  "source": {
    "index": "my-index-000001",
    "size": 10000,
    "query":{
       "range":{
         "FIELD": {
            "gte": 10,
            "lte": 20
          }
       }
     },
     "_source":{
     "includes":["_id","aaa","bbb"],
     "excludes":["xxx","yyy","zzz"]
     }
  },
  "dest": {
    "index": "my-new-index-000001",
    "version_type":"external"
  }
}

5、关于写入的版本操作

此处引用携程wood大叔在社区上的一个例子:

version_type 的internal, external都是从dest索引的视角看过去的,internal表示使用dest内部版本, external表示使用source的版本。

假设source索引有3条文档:

id: 1 text: "a" version: 4

id: 2 text: "b" version: 3

id: 3 text: "c" version: 2

dest索引有2条文档:

id: 1 text: "e" version: 1

id: 2 text: "f" version: 5

如果将source reindex到dest,文档1和2会产生冲突。

使用不同的version type做reindex,dest里的文档内容和版本变化的结果如下:

version type = internal

id: 1 text: "a" version: 2 # 覆盖 & 内部版本号 1

id: 2 text: "b" version: 6 # 覆盖 & 内部版本号 1

id: 3 text: "c" version: 1 # 新文档,创建,版本号为1

version type = external:

id: 1 text: "a" version: 4 # 外部版本号> 内部,因此覆盖文档 & 保留外部版本号4

id: 2 text: "f" version: 5 # 外部版本号小于内部,版本冲突,保留文档和内部版本号5

id: 3 text: "c" version: 2 # 新文档,创建,并保留外部版本号2

op_type = create:

id: 1 text: "e" version: 1 # dest已有文档忽略不处理

id: 2 text: "f" version: 5 # dest已有文档忽略不处理

id: 3 text: "c" version: 1 # dest没有此文档,新创建(create),版本号为1

6、reindex 生产操作流程

前提条件:有时间戳字段,保证数据查询的范围有效。使用时间戳字段来分批执行reindex,这样来减少因reindex导致的数据停写时间。如没有时间戳字段,则整个reindex需要在源索引停止写入后操作

主要操作流程:

1、新建新索引,设置好新的字段mapping和setting;

2、根据时间字段进行reindex,将大部分数据写入新索引;

3、如果步骤2耗时漫长,比如3小时,则根据时间进行第二轮数据reindex;

4、停止数据写入,进行最后一轮的reindex。为了减少停服时间,query的时间窗口控制在半小时内,数据量控制在整体数据集的10分之一以下。

5、比对新旧索引数据量,正确后进行索引别名切换,无索引别名则应用程序切换至新索引。

7、对于大索引reindex的异步处理方案

方案:利用镜像把数据复制出来在非生产环境处理

具体操作:

1.设置临时镜像路径,提前一天把索引先镜像copy出来

代码语言:javascript复制
PUT _snapshot/ops-es-snapshot-ol
备份索引

2.在单独的非生产集群reindex,注意新索引不需要带别名

3.第二天晚上把结果索引镜像复制回生产环境

4.利用时间戳把新旧索引的数据差补上。注意:delete操作不可回放

5.数据校验无误后,需要别名切换的进行别名切换,再应用切换

6.删除生产的临时镜像路径

8、磁盘空间不足问题

增加副本导致索引数据成倍增长,磁盘空间需要评估能否满足需求,最好提前扩容。

如果创建副本期间磁盘空间告警:

1. 空间完全不足以容纳新增的副本,快速动态调整副本数量设置为0

PUT /shimo_v4/_settings { "number_of_replicas": 0 }

2. 空间勉强容纳新增副本,需要调高磁盘水位线,防止触发水位限制导致限流,数据写入失败

cluster.routing.allocation.disk.watermark.low cluster.routing.allocation.disk.watermark.high

9、reindex期间双写问题

问题:因业务不能停止写入,服务滚动重启期间存在新旧索引双写的问题,部分数据可能双边更新,应用切换完成后,不能简单覆盖。

解决方法:多次reindex操作(全量 增量 补数据),version_type统一使用external模式。以source版本为准,dest不存在的doc直接插入,version版本比dest大的覆盖更新,version版本比dest小的版本冲突,同时需设置conflicts="proceed",确保冲突不会中断reindex操作。

优势:

  1. reindex对生产的资源使用减少,影响时间也更小
  2. reindex的时间窗口和资源配置更加灵活,成功率也更高
  3. 大索引下(500g以上)镜像备份的时间远远小于reindex的耗时,这样能有效减少生产变更的耗时

建议:

鉴于 reindex 在 1TB 以上数据量糟糕的表现(时间长,速度衰迭严重,任务完成无保障),大索引的重刷还是使用logstash 分段任务来处理更合适。

结论

当您需要更改某些字段的格式时,重建索引 API 是一个不错的选择。下面我们将列出一些关键方面,确保重建索引 API 尽可能顺利地运行:

  1. 为目标索引创建并定义映射(或模板)。
  2. 调整目标索引,使重建索引 API 尽可能快地索引文档。我们有一个文档页面,其中提供了用于调整和加快索引速度的所有选项。 https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-indexing-speed.html
  3. 如果源索引的大小为中型或大型,请定义“wait_for_completion=false”设置,以便重建索引 API 结果存储 在 _tasks API 上。
  4. 将索引分成更小的组,您可以使用查询(范围、术语等)定义不同的组,或者使用切片功能将请求分成较小的部分。
  5. 运行重建索引 API 时,稳定性是关键因素,参与重建索引 API 的索引需要处于绿色状态(最糟糕的情况是黄色状态),然后确保我们的数据节点中没有很长的 GarbageCollections,并且 CPU 和磁盘使用率为正常值。

从 v7.11 开始,我们发布了一项新功能,让您无需为数据重建索引,这项功能称为“运行时字段”。使用这个 API 可以修复错误,而无需为数据重建索引,因为您可以在索引映射搜索请求中定义运行时字段。您可以通过这两种方式在采集数据后灵活地更改文档的模式,并生成只作为搜索查询的一部分存在的字段。

0 人点赞