故障诊断

当集群出现故障时,不必担心,我们有许多方法和工具来分析问题。在大部分情况下,我们遇到的问题都是由一些简单的原因导致的。但由于分布式系统的复杂性,有时候故障现象只出现了一次,并且难以复现,这就需要采取一些措施来缩小可疑的问题范围,虽然不能立刻解决问题,但是可以向前迈进一步。

1. Profile API

有时在发起一个查询时,查询会被延迟执行或者响应时间很慢。Elasticsearch 从 2.2 版本开始就提供了 Profile API 以供用户检查查询执行的时间和其他详细信息。

可以通过在 query 部分上方提供 “profile”:true 来启用 Profile API,示例如下:

  1. curl -XPOST "http://localhost:9200/twitter/_search" -H 'Content-Type: application/json' -d'
  2. {
  3. "profile": true,
  4. "query": {
  5. "term": {
  6. "user":"kimchy"
  7. }
  8. }
  9. }'

Profile API 的结果是基于每个分片计算的。由于在该示例索引中只有一个分片,所以在 Profile API 响应的分片数组中只有一个数组元素,如下所示。

  1. {
  2. "profile": {
  3. "shards": [
  4. {
  5. "id": "[7f-bO8KZQj-bjxa6Wxlt0A][twitter][0]",
  6. "searches": [
  7. {
  8. "query": [
  9. {
  10. "type": "TermQuery",
  11. "description": "user:kimchy",
  12. "time_in_nanos": 67306,
  13. "breakdown": {
  14. "set_min_competitive_score_count": 0,
  15. "match_count": 0,
  16. "shallow_advance_count": 0,
  17. "set_min_competitive_score": 0,
  18. "next_doc": 1556,
  19. "match": 0,
  20. "next_doc_count": 2,
  21. "score_count": 2,
  22. "compute_max_score_count": 0,
  23. "compute_max_score": 0,
  24. "advance": 1599,
  25. "advance_count": 1,
  26. "score": 11516,
  27. "build_scorer_count": 5,
  28. "create_weight": 32826,
  29. "shallow_advance": 0,
  30. "create_weight_count": 1,
  31. "build_scorer": 19809
  32. }
  33. }
  34. ],
  35. "rewrite_time": 1002,
  36. "collector": [
  37. {
  38. "name": "SimpleTopScoreDocCollector",
  39. "reason": "search_top_hits",
  40. "time_in_nanos": 23000
  41. }
  42. ]
  43. }
  44. ],
  45. "aggregations": []
  46. }
  47. ]
  48. }
  49. }

在 searches 数组项里有三个元素:query、rewrite_time、collector。下面解释每个元素的意义。

  • query:查询过程
    • type:它向我们显示了哪种类型的查询被触发
    • time_in_nanos:Lucene 执行此查询所用的时间
    • breakdown:有关查询的更详细的细节,主要与 Lucene 参数有关


  • rewrite_time:由于多个关键字会分解以创建个别查询,所以在这个过程中肯定会花费一些时间。将查询重写一个或多个组合查询的时间被称为重写时间,单位为纳秒。


  • collector:在 Lucene 中,收集器负责收集原始结果,并对它们进行组合、过滤、排序等处理。在示例执行的查询中,默认返回评分最高的前十个文档,所以使用的收集器是 SimpleTopScoreDocCollector。并且也同时返回了使用该收集器的原因和耗时。

Profile API 非常有用,它让我们清楚地看到查询时间。通过向我们提供有关子查询的详细信息,我们可以清楚地知道在哪个环节查询慢,这是非常有用的。另外,在 API 返回的结果中,关于 Lucene 的详细信息也让我们深入了解到 ES 是如何执行查询的。

2. Explain API

一个 ES 索引由多个分片组成,由于某些原因,某些分片可能会处于未分配状态(Unassigned)状态导致集群健康处于 Yellow 或 Red 状态,这是一种比较常见的错误信息,导致分片处于未分配的原因可能是节点离线、分片数量设置错误等原因,使用 Explain API 可以很容易分析当前的分片的分配情况。

这个 API 主要为了解决下面两个问题:

  • 对于未分配的分片,给出为什么没有分配的具体原因。
  • 对于已分配的分片,给出为什么将分片分配给特定节点的理由。

使用示例如下:

curl -XGET "http://localhost:9200/_cluster/allocation/explain"

curl -XGET "http://localhost:9200/_cluster/allocation/explain" -H 'Content-Type: application/json' -d'
{
  "index": "twitter",
  "shard": 0,
  "primary": false
}'

返回结果如下:

{
  "index" : "my_index",
  "shard" : 0,
  "primary" : false,
  "current_state" : "unassigned",
  "unassigned_info" : {
    "reason" : "CLUSTER_RECOVERED",
    "at" : "2021-07-25T09:52:53.930Z",
    "last_allocation_status" : "no_attempt"
  },
  "can_allocate" : "no",
  "allocate_explanation" : "cannot allocate because allocation is not permitted to any of the nodes",
  "node_allocation_decisions" : [
    {
      "node_id" : "7f-bO8KZQj-bjxa6Wxlt0A",
      "node_name" : "TD-lei.xul",
      "transport_address" : "127.0.0.1:9300",
      "node_attributes" : {
        "ml.machine_memory" : "17179869184",
        "xpack.installed" : "true",
        "transform.node" : "true",
        "ml.max_open_jobs" : "512",
        "ml.max_jvm_size" : "1037959168"
      },
      "node_decision" : "no",
      "deciders" : [
        {
          "decider" : "same_shard",
          "decision" : "NO",
          "explanation" : "a copy of this shard is already allocated to this node [[my_index][0], node[7f-bO8KZQj-bjxa6Wxlt0A], [P], s[STARTED], a[id=YNYQ7R_nTkmF6L3TH3N1aQ]]"
        }
      ]
    }
  ]
}

从返回结果中可以看到,my_index 索引的第 0 号分片的副分片当前处于未分配状态,因为当前集群中只有一个节点,而 ES 会避免将主副分片分配到同一个节点上去,主要是为了防止当节点失效时所有副本都不可用,以及可能的数据丢失。

3. hot threads

当发现节点进程占用 CPU 非常高时,我们想知道是哪些线程导致的,这些线程具体在执行什么任务,常规做法是通过 top 命令配合 jstack 来定位线程:top 获取线程级 cpu 占用率,再根据线程 id,配合 jstack 定位 cpu 占用率在特定比例之上的线程堆栈。

现在 ES 提供了更便捷的方式,通过 hot threads API 可以直接返回这些信息。该 API 可以返回集群中一个或全部节点的热点线程,使用示例如下:

curl -XGET "http://localhost:9200/_nodes/hot_threads"

curl -XGET "http://localhost:9200/_nodes/node1,node2/hot_threads"

该命令支持以下参数:

  • threads:返回的热点线程数,默认为 3 个。
  • interval:ES 通过对线程做两次检查来计算某个操作上花费时间的百分比,此参数定义这个间隔时间。默认为 500 ms。
  • type:定义要检查的线程状态类型,默认为 CPU。还可以检查线程的 CPU 占用时间、阻塞(block)时间和等待(wait)时间。
  • ignore_idle_threads:如果设置为 true 则空闲线程将被过滤。默认值为 true。

4. 内存使用率高

当节点内存使用率高时,我们想要知道内存是被哪些数据结构占据的,通用方式是用 jmap 导一个堆出来,加载到 MAT 中进行分析,这样可以精确定位数据结构占用内存的大小,以及被哪些对象引用。这是定位此类问题最直接的方式。但 jmap 导出的堆可能非常大,操作比较耗时,我们可以先通过一些命令简单看下 ES 中几个占用内存比较大的数据结构,进行粗略评估。

ELasticsearch 的 JVM 内存主要由以下几个部分使用:
image.png
indexing buffer
索引写入缓冲用于存储索引好的文档数据,当缓冲满时生成一个新的 Lucene 分段。在一个节点上,该节点的全部分片共享 indexing buffer。该缓冲区的默认大小为堆内存的 10%,加大该缓冲需要考虑到对 GC 的压力。

Segment 占用内存
一个 Lucene 分段就是一个完整的倒排索引,倒排索引由单词词典和倒排列表组成。在 Lucene 中,单词词典中的 FST 结构会被加载到内存。因此每个分段都会占用一定的内存空间。可以通过下面的 API 来查看某个节点上的所有分段占用的内存总量:

curl -XGET "http://localhost:9200/_cat/nodes?v&h=segments.memory"

也可以单独查看每个分段的内存占用量:

curl -XGET "http://localhost:9200/_cat/segments?v&h=segments.memory&h=index,shard,segment,size.memory"

Fielddata Cache
除了 text 类型字段外,其余字段类型默认都采用 doc values 建立一个列式存储用于排序,节约了内存。如果想要对 text 类型的字段进行排序和聚合,需要开启该字段的 fielddata,默认是关闭的。如果开启了 fielddata 则其可使用的内存大小默认没有上限,通过如下配置可设置一个百分比来控制其使用的堆内存上限,内部采用 LRU 算法淘汰最久未使用的缓存信息。

# 默认值为-1,表示不限制
indices.fielddata.cache.size:

当 Segment 被合并后,该缓存会失效。可以通过下面的命令查看节点上的 fielddata 使用情况:

curl -XGET "http://localhost:9200/_cat/nodes?v&h=fielddata.memory_size"
curl -XGET "http://localhost:9200/_nodes/stats/indices/fielddata?fields=field1,field2&pretty"
curl -XGET "http://localhost:9200/_stats/fielddata?fields=field1,field2&pretty"

Shard Request Cache
分片级别的请求缓存,其使用 LRU 淘汰策略,每个分片独立地缓存查询结果,会将整个 JSON 查询字符串作为缓存键。默认开启,最大占用内存为堆大小的 1%,可通过如下配置项静态配置在每个 Data Node 上:

indices.request.cache.size: 1%

默认情况下,该缓存只会缓存设置了 size=0 的查询对应的结果,它并不会缓存命中的结果(hits),但是会缓存 hits.total、Aggregations 和 Suggestions 信息。当分片 refresh 的时候,Shard Request Cache 会失效。如果 Shard 对应的数据频繁发生变化(即需要经常进行 refresh 操作)则缓存效果会很差。

是否开启该缓冲可以动态调整:


curl -XPUT "http://localhost:9200/my_index/_settings" -H 'Content-Type: application/json' -d'
{
  "index.requests.cache.enable": true
}'

也可以在某个请求中指定不使用缓存:

curl -XGET "http://localhost:9200/my_index/_search?request_cache=false"

可以使用下面的 API 来获取该缓存的使用量:

curl -XGET "http://localhost:9200/_cat/nodes?v&h=request_cache.memory_size"
curl -XGET "http://localhost:9200/_stats/request_cache?pretty"
curl -XGET "http://localhost:9200/_nodes/stats/indices/request_cache?pretty"

Node Query Cache
节点查询缓存由该节点上的所有分片共享,也是一个 LRU 缓存,用于缓存查询结果,只缓存在过滤器上下文(Filter Context)中使用的查询。该缓存默认开启,大小为堆大小的 10%。可通过如下配置项静态配置在每个 Data Node 上:

# 默认开启
index.queries.cache.enabled: true

# 默认占用总内存的 10%
indices.queries.cache.size: 10%

该缓存保存的是 Segment 级缓存命中的结果,当 Segment 被合并后缓存会失效。该缓存的使用量可通过下面的命令来获取:

curl -XGET "http://localhost:9200/_cat/nodes?v&h=query_cache.memory_size"
curl -XGET "http://localhost:9200/_nodes/stats/indices/query_cache?pretty"
curl -XGET "http://localhost:9200/_stats/query_cache?pretty"

分片调整

1. Shrink API

需要密切注意集群分片总数,分片数越多集群压力越大。在创建索引时,为索引分配了较多的分片,但可能实际数据量并没有多大。例如,按日期轮询生成的索引,可能有些日子里数据量并不大,对这种索引可以执行 Shrink 操作来降低索引分片数量。

Shrink API 是 ES 5.x 版本后推出的一个新功能,它会使用和源索引相同的配置创建一个新的索引,目的仅仅是为了减少主分片数。在使用 Shrink API 时有如下几个限制:

  • 分片必须只读,且所有分片必须在同一个节点上
  • 集群健康状态为 Green
  • 源分片数必须是目标分片数的倍数,如果源分片数是素数,则目标分片数只能为 1
  • 如果文件系统支持硬链接(hard link),则会将 Segment 直接硬链接到目标索引,性能好

使用示例如下:

# 首先将my_source_index设为只读
curl -X PUT "localhost:9200/my_source_index/_settings" -H 'Content-Type: application/json' -d'
{
  "settings": {
    "index.blocks.write": true
  }
}'

curl -X PUT "localhost:9200/my_source_index/_shrink/my_target_index?pretty" -H 'Content-Type: application/json' -d'
{
  "settings": {
    "index.number_of_replicas": 0,
    "index.number_of_shards": 2,
    "index.codec": "best_compression"
  },
  "aliases": {
    "my_search_indices": {}
  }
}'

在 Shrink 操作执行完成后,就可以删除源索引了。注意,目标索引此时也是只读的,我们需要手动进行修改。

2. Split API

内存熔断器 Circuit Breaker

ES 内置了多种内存断路器,主要用于避免不合理操作引发的 OOM,每个断路器可以指定内存使用的限制:

  • Parent circuit breaker:设置所有的熔断器可以使用的内存的总量
  • Fielddata circuit breaker:加载 fielddata 所需要的内存
  • Request circuit breaker:防止每个请求级数据结构超过一定的内存(过深的聚合)
  • In flight circuit breaker:Request 中的断路器
  • Accounting request circuit breaker:请求结束后不能释放的对象所占用的内存

我们可以通过 /_nodes/stats/breaker 查看 Circuit Breaker 的统计信息:


{
  "request": {
    "limit_size_in_bytes": 3823632384,
    "limit_size": "3.5gb",
    "estimated_size_in_bytes": 16440,
    "estimated_size": "16kb",
    "overhead": 1,
    "tripped": 0
  },
  "fielddata": {
    "limit_size_in_bytes": 3823632384,
    "limit_size": "3.5gb",
    "estimated_size_in_bytes": 475216,
    "estimated_size": "464kb",
    "overhead": 1.03,
    "tripped": 0
  },
  "in_flight_requests": {
    "limit_size_in_bytes": 6372720640,
    "limit_size": "5.9gb",
    "estimated_size_in_bytes": 0,
    "estimated_size": "0b",
    "overhead": 1,
    "tripped": 0
  },
  "accounting": {
    "limit_size_in_bytes": 1037959168,
    "limit_size": "989.8mb",
    "estimated_size_in_bytes": 0,
    "estimated_size": "0b",
    "overhead": 1,
    "tripped": 0
  },
  "parent": {
    "limit_size_in_bytes": 4460904448,
    "limit_size": "4.1gb",
    "estimated_size_in_bytes": 491656,
    "estimated_size": "480.1kb",
    "overhead": 1,
    "tripped": 0
  }
}

如果 tripped 大于 0 则说明有过熔断,当 limit_size 与 estimated_size 越接近,越可能引发熔断。但千万不要触发了熔断就盲目调大熔断阈值,有可能导致集群出问题;也不应该盲目调小,而是应该进行合理评估。

ES 在 7.0 版本之后相比之前版本提供了一套更好的 Circuit Breaker 实现机制,增加了如下配置项,可以更加精准的分析内存情况,避免 OOM。

indices.breaker.total.use_real_memory

常用运维命令

1. 分片移动

此外,我们可以通过 _cluster/reroute 从一个节点移动分片到另一个节点。比如,当一个节点上有过多的 Hot Shard 时,可以通过手动分配分片到特定节点来解决。

使用示例:

curl -X POST "localhost:9200/_cluster/reroute?pretty" -H 'Content-Type: application/json' -d'
{
    "commands" : [
        {
            "move" : {
                "index" : "test", 
                "shard" : 0,
                "from_node" : "node1", 
                "to_node" : "node2"
            }
        }
    ]
}'

2. 移除节点

当我们想从集群中移除一个节点或对一个机器进行维护,又不希望因此导致集群的颜色变红或者变黄。我们需要先将该节点的数据迁移,这可以通过分配过滤器实现。通过如下配置:

curl -X PUT "localhost:9200/_cluster/settings?pretty" -H 'Content-Type: application/json' -d'
{
    "transient" : {
        "cluster.routing.allocation.exclude._ip" : "the_ip_of_your_node"
    }
}'

或者

curl -X PUT "localhost:9200/_cluster/settings?pretty" -H 'Content-Type: application/json' -d'
{
    "transient" : {
        "cluster.routing.allocation.exclude._name" : "the_name_of_your_node"
    }
}'

执行命令后,Elasticsearch 会自动将该节点上的数据迁移到其他节点上。我们可以通过 _cat/shard 来查看该节点的分片是否迁移完毕。等移动完成,我们就可以安全下线该节点了。

如果被下线的这个节点维护完毕,想要重新上线时,需要取消 exclude 配置,以便后续的分片还可以分配到该节点上去:

curl -X PUT "localhost:9200/_cluster/settings?pretty" -H 'Content-Type: application/json' -d'
{
    "transient" : {
        "cluster.routing.allocation.exclude._name" : ""
    }
}'

3. 清空缓存

当节点上出现了高内存占用时,可以执行清除缓存的操作。该操作会影响集群的性能但是会避免你的集群出现 OOM 的问题。

curl -X PUT "localhost:9200/_cache/clear?pretty"