在运维 ES 集群的过程中,我们一般使用 node stats 接口获取关键指标, node-stats
API 可以通过如下命令执行:
node-stats
API 可以通过如下命令执行:
GET _nodes/stats
从输出的顶部开始,我们看到集群名称和我们的第一个节点:
{
"cluster_name": "elasticsearch_zach",
"nodes": {
"UNr6ZMf5Qk-YCPA_L18BOQ": {
"timestamp": 1408474151742,
"name": "Zach",
"transport_address": "inet[zacharys-air/192.168.1.131:9300]",
"host": "zacharys-air",
"ip": [
"inet[zacharys-air/192.168.1.131:9300]",
"NONE"
],
...
节点以哈希表列出,key 为节点的UUID。 显示有关节点网络属性的一些信息(如传输地址和主机)。 当节点没有正确加入集群的时候,这些值对于调试发现问题非常有用:通常会看到使用了错误的端口,或者该节点绑定到错误的IP地址/接口。
indices 信息块
indices
信息块列出该节点上所有 indices 的聚合信息:
"indices": {
"docs": {
"count": 6163666,
"deleted": 0
},
"store": {
"size_in_bytes": 2301398179,
"throttle_time_in_millis": 122850
},
返回结果可以分为 2 块:
-
docs
表示存储在该节点的 documents 数量,包括标记删除但是实际没有从 segment 清除的。 -
store
表示磁盘占用空间大小,包括 primary 和 replica shards。如果 throttle time 过大,可能表示磁盘限制过低。
"indexing": {
"index_total": 803441,
"index_time_in_millis": 367654,
"index_current": 99,
"delete_total": 0,
"delete_time_in_millis": 0,
"delete_current": 0
},
"get": {
"total": 6,
"time_in_millis": 2,
"exists_total": 5,
"exists_time_in_millis": 2,
"missing_total": 1,
"missing_time_in_millis": 0,
"current": 0
},
"search": {
"open_contexts": 0,
"query_total": 123,
"query_time_in_millis": 531,
"query_current": 0,
"fetch_total": 3,
"fetch_time_in_millis": 55,
"fetch_current": 0
},
"merges": {
"current": 0,
"current_docs": 0,
"current_size_in_bytes": 0,
"total": 1128,
"total_time_in_millis": 21338523,
"total_docs": 7241313,
"total_size_in_bytes": 5724869463
},
indexing
表示曾经被索引过的 docs。 这个值是单调递增的,当 docs 被删除时,它不会减少。 还要注意,在内部执行索引操作的任何时候它都会增加,包括更新操作。一起被列出的指标还有 indexing 耗时,当前被 indexing 的 docs 数量,以及类似的删除操作对应的统计指标。get
get-by-ID 的统计信息,包括针对单个 doc 的GET
和HEAD
请求。-
search
描述了活跃 search 数量 (open_contexts
),总 query 的数量, 以及自节点启动后 query 的累计耗时。query_time_in_millis / query_total
的比率可以作为查询效率的粗略指标。比例越大,每个查询的时间越多,应该考虑调优。fetch 详细统计的是查询的后半段 (query-then-fetch 中的 fetch )。如果 fetch 的时间比 query 更多,这是慢盘或大查询的指示符,或可能 search 请求涉及的分页过大 (比如,
size: 10000
). -
merges
包含有关Lucene segment 合并的信息。 包括当前有效的 merge 数量,涉及的 doc 数量,已 merge 的 segment 的累积大小以及 merge 所花费的时间。如果写负载很高,则合并统计信息非常重要,因为合并消耗大量的磁盘 I/O 和 CPU 资源。
注意:update 和 delete 也会带来 merge 操作,因为它们会导致片段碎片需要最终被合并。
"filter_cache": {
"memory_size_in_bytes": 48,
"evictions": 0
},
"fielddata": {
"memory_size_in_bytes": 0,
"evictions": 0
},
"segments": {
"count": 319,
"memory_in_bytes": 65812120
},
...
-
filter_cache
描述 cache filter 的内存使用大小,filter 被淘汰的次数。 大量的淘汰意味着需要增加 filter cache,或filter 没有被正确缓存。然而,淘汰是一个非常难以评估的指标。 filter 以 segment 为单元进行缓存,这意味着淘汰一个 small segment 代价相对较小。 如果是大量的 small segment 淘汰,这意味着它们对查询性能几乎没有影响。
field_data
描述了 field_data 使用的内存,它会被用作 聚合(aggregation),排序(sorting)等,这块内存指标也包括淘汰次数。 和filter_cache
不一样,这里的淘汰次数必须为 0 或者逼近 0 才合理。 因为 field data 不是 cache,所有任何淘汰都是代价昂贵且需要避免的。如果发现这里有淘汰,就需要重新评估包括内存、 field data 限制、query。-
segments
描述的是该节点目前服务的 Lucene segment 的数量,非常重要。 大多数 indices 应该有大约 50-150 个 segment,即使是 TB 大小,包含十亿级别 documents 的。 大量的 segment 可以体现出合并的问题(例如,合并赶不上 segment 的创建)。 请注意,此统计信息是节点上所有 indices 的总和。内存统计信息让我们了解 Lucene segment 本身使用的内存量,包括 low-level 数据结构,例如发布列表,字典和布隆过滤器。
OS 和 Process 信息块
OS
和 Process
信息块描述的指标比较简单通用,无需详细解释。 OS
信息块描述的是整个 OS
,而 Process
信息块 展示的是 Elasticsearch JVM 进程相关指标。
这些都是非常有价值的指标,但是我们通常都会在其他监控平台进行采集和展示:
- CPU
- Load
- Memory usage
- Swap usage
- Open file descriptors
JVM 信息块
jvm
信息块包括一些 ElasticSearch JVM 进程的重要指标。更重要的是包含了 GC 相关的详细指标,这对观察集群稳定性非常必要。
Garbage Collection Primer
在描述指标之前,非常有必要插入下 GC 相关的知识和对 ES 本身的影响。如果您非常了解 JVM GC,可以直接跳过。
Java 是一种 garbage-collected 语言,意味着开发者不需要关心内存的分配和回收。
当内存被分配至 JVM 进程,会被分配到叫做 heap 的大 chunk。JVM 会把 heap 划分为两大块,根据 generations:
-
Young (or Eden)
用于分配新建对象的空间。新生代的空间往往较小,经常是 100 MB–500 MB。新生代往往包含两块 survivor 空间。
-
Old
用于存储老对象的空间。这些对象预计会长期存在, 老生代通常比新生代大得多,而 ES 节点往往会有大于 30 GB 的老生代空间分配。
当对象被实例化时,它被分配至新生代。 当新生代空间满时,会发生 YGC。 仍然需要存在的对象会被移动到其中的一个 suvive 空间中,并且"dead"对象会在这个过程被移除。 如果一个对象在若干个 YGC 中幸存下来,那么它就会晋升到老生代。
在老生代中也会发生类似的过程:当空间变满时,垃圾回收开始,"dead"对象被删除。
然而,GC 过程是有代价的。 不论是 YGC 还是 FGC 都有 "Stop-the-World" 的阶段。 在此期间,JVM会直接停止执行程序,以便跟踪对象 graph 并收集 "dead" 对象。 在 "Stop-the-World" 阶段任何事情都不会发生。 请求不得到服务,ping 不受到响应,shards 不被重定位。 对于 ES 来说,世界确实停止了。
这对新生代来说不是什么大问题;其小尺寸意味着 GC 会被快速执行。 但是老生代的问题还是比较大的,而这里如果存在缓慢的 GC 可能意味着1秒甚至150秒的暂停,这对服务器软件是不可接受的。
JVM中的垃圾回收器是非常复杂的算法,并且可以很好地缩短停顿时间。 和 ES 在 GC 方面做了很深的优化,通过智能地内部重用对象,重用网络缓冲区,并默认启用[docvalues]。 但是最终,GC 频率和持续时间仍是需要关注的指标,因为它是群集不稳定性的首要原因。
经常遇到长 GC 的集群将是一个负载较重的内存不足的集群。 这些长时间的 GC 将使节点短暂地离开集群。 这种不稳定性导致 shards 频繁被 realocate,因为 Elasticsearch 尝试保持集群平衡和足够可用的副本。 这反过来又增加了网络流量和磁盘 I /O,而同时集群也在尝试维护正常的 indexing 和 query 负载。
总而言之, 长时间的 GC 应该被重视并尽可能的优化。
因为 GC 对 ES 集群至关重要,我们应该密切关注 node-stats
API 的 GC 信息块:
"jvm": {
"timestamp": 1408556438203,
"uptime_in_millis": 14457,
"mem": {
"heap_used_in_bytes": 457252160,
"heap_used_percent": 44,
"heap_committed_in_bytes": 1038876672,
"heap_max_in_bytes": 1038876672,
"non_heap_used_in_bytes": 38680680,
"non_heap_committed_in_bytes": 38993920,
-
jvm
信息块首先展示了堆内存的信息。我们可以看到堆内存被使用的情况,内存之余进程分配的情况以及最大的可用堆内存。 理想情况下,heap_committed_in_bytes
需要和heap_max_in_bytes
相同。如果 committed size 较小,JVM 将必须在某些情况下 resize heap — 这是一个代价很大的过程。如果数值不同,请参看 [heap-sizing] 进行合理配置。heap_used_percent
是一个十分有用的指标。Elasticsearch 默认情况会在 75% 的水位线进行 GC。 如果节点的堆内存持续 >= 75%,节点会处于 memory pressure。这意味着接下来可能会处发长时间的 GC。
如果节点的堆内存长时间 >=85%,那就更严重。水位线处于 90–95%会非常恐怖的触发 10–30 秒的 GC,甚至是 OOM。
"pools": {
"young": {
"used_in_bytes": 138467752,
"max_in_bytes": 279183360,
"peak_used_in_bytes": 279183360,
"peak_max_in_bytes": 279183360
},
"survivor": {
"used_in_bytes": 34865152,
"max_in_bytes": 34865152,
"peak_used_in_bytes": 34865152,
"peak_max_in_bytes": 34865152
},
"old": {
"used_in_bytes": 283919256,
"max_in_bytes": 724828160,
"peak_used_in_bytes": 283919256,
"peak_max_in_bytes": 724828160
}
}
},
-
young
,survivor
,以及old
信息块将会给到不同空间的指标。但是并没有上述指标那么重要。
"gc": {
"collectors": {
"young": {
"collection_count": 13,
"collection_time_in_millis": 923
},
"old": {
"collection_count": 0,
"collection_time_in_millis": 0
}
}
}
-
gc
信息块显示了 YGC 和 FGC 的次数和累积时间。一般情况下我们可以忽略 YGC 的次数:这个数字往往很大。相反的 FGC 次数应该较小,并且有较小的
collection_time_in_millis
。这些都是累积指标,所以参考价值不是很大。 (比如,一个启动很久的节点可能会有很大的值)。这就是为什么第三方监控工具很重要的原因,GC 次数 over time are 是非常有必要考虑的指标。GC 持续时间也很重要。比如,在进行 indexing 的时候,会有触发一系列的 GC。 这些 GC 往往很快并且对节点的影响很小:YGC 大概是 1-2 ms,FGC 大概是数百 ms。
最好的官方建议是周期性的抓取 GC 次数和持续时间(或者用第三方监控工具),并且关注频繁的 GC。我们也可以 enable slow-GC logging[logging]。
Threadpool 信息块
Elasticsearch 在内部维护了线程池。这些线程池进行协作完成工作,在必要的时候传递工作。一般情况下,无需对其进行配置,但是某些情况下获取他们的状态去观察集群的行为是有用的。
ES 大概有一打线程池,具有相同的状态信息输出:
"index": {
"threads": 1,
"queue": 0,
"active": 0,
"rejected": 0,
"largest": 1,
"completed": 1
}
每一个线程池都列出了它们被配置的线程数(threads
),其中正在处理工作的线程数 (active
),和有多少工作单元在队列中排队 (queue
)。
如果队列达到了极限,新的工作单元会被拒绝,我们会在 rejected
看到统计信息。这表示集群因为某些资源达到了瓶颈,因为队列满了意味着节点或者集群的处理速度赶不上工作单元的生成速度。
Bulk Rejections(批处理拒绝)
如果遇到了队列拒绝,很大可能是因为 bulk indexing 请求造成的。一旦阈值达到,队列会迅速被填满,新的 bulk 请求会被拒绝。队列拒绝是一种集群友好的策略,避免被打爆。他告诉了运维同学集群的最大能力在哪里,绝对比把数据持续塞进内存队列好的多。增加队列大小并不能带来性能提升,而是隐藏了问题。如果集群只能处理 10K docs/s,不论队列多大,那也只能处理那么多。队列会简单的隐藏性能问题并且加重数据丢失的风险。任何被塞进队列的都是被定义为未被处理的。如果节点宕机,这些请求都会被永久丢失。进一步来说,队列也会吃掉大量的内存所以不建议开太大。
所以在开发应用的时候也需要优雅的处理 queue rejected 导致的问题,当接收到 bulk rejections 相关异常时,必须这么做:
- 停止核心线程 3-5 秒;
- 从bulk response 中解析出被拒绝的 actions,因为可能 bulk 操作中有大量 action 是成功的;
- 再次发送新的 bulk request,去处理被拒绝的 actions;
- 如果再次遇到 rejected 重复第1步;
Rejections 并不是 error:他意味着你必须要再次重试;
我们需要关注如下的线程池信息:
-
indexing
普通 indexing requests 的线程池;
-
bulk
Bulk requests, 区别于 nonbulk indexing 请求;
-
get
Get-by-ID 操作;
-
search
所有的 search 和 query 请求;
-
merging
Lucene merge 的请求;
FS 和 Network 信息块
往下拉,可以看到 node-stats
API 还附带了这些输出信息:free space,data directory paths,disk I/O stats 等等。如果你没有在第三方监控到,那么可以走这里获取。
同样的对于网络信息块,输出如下:
"transport": {
"server_open": 13,
"rx_count": 11696,
"rx_size_in_bytes": 1525774,
"tx_count": 10282,
"tx_size_in_bytes": 1440101928
},
"http": {
"current_open": 4,
"total_opened": 23
},
-
transport
描述的是 transport address 相关基础信息。这和集群内部通信有关(9300端口)。如果看到很多连接请不要担心,因为 ES 在集群内部维护了很多连接。 -
http
汇报的是外部通信端口信心 (often 9200)。如果total_opened
非常大并且持续增长,这说明有 HTTP 客户端没有使用 keep-alive 连接。对于性能来说,keep-alive 模式很重要,因为构建和销毁 socket 的代价很大。
Circuit Breaker 信息块
最后介绍下 Circuit Breaker 信息块:fielddata circuit-breaker 相关统计指标:
"fielddata_breaker": {
"maximum_size_in_bytes": 623326003,
"maximum_size": "594.4mb",
"estimated_size_in_bytes": 0,
"estimated_size": "0b",
"overhead": 1.03,
"tripped": 0
}
这段信息告诉了我们 circuit-breaker 的大小(例如,如果 query 尝试使用更多内存,circuit breaker 会在什么阈值跳闸,阻断 query),还告诉了我们 circuit-breaker 阻断的次数以及当前配置的开销。 开销用于弥补估计值的偏差,因为某些 query 比其他 query 更难估计。最重要的指标是tripped
,如果该值很大或者持续增加,说明 query 需要被优化或者需要更多内存去支撑 query。