监控单个节点
集群健康
就像是光谱的一端——对集群的所有信息进行高度概述。而 节点统计值
API 则是在另一端。它提供一个让人眼花缭乱的统计数据的数组,包含集群的每一个节点统计值。
节点统计值
提供的统计值如此之多,在完全熟悉它之前,你可能都搞不清楚哪些指标是最值得关注的。我们将会高亮那些最重要的监控指标(但是我们鼓励你记录接口提供的所有指标——或者用 Marvel ——因为你永远不知道何时需要某个或者另一个值)。
节点统计值
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"
],
...
节点是排列在一个哈希里,以节点的 UUID 作为键名。还显示了节点网络属性的一些信息(比如传输层地址和主机名)。这些值对调试诸如节点未加入集群这类自动发现问题很有用。通常你会发现是端口用错了,或者节点绑定在错误的 IP 地址/网络接口上了。
索引部分
索引(indices)
部分列出了这个节点上所有索引的聚合过的统计值:
"indices": {
"docs": {
"count": 6163666,
"deleted": 0
},
"store": {
"size_in_bytes": 2301398179,
"throttle_time_in_millis": 122850
},
返回的统计值被归入以下部分:
docs
展示节点内存有多少文档,包括还没有从段里清除的已删除文档数量。store
部分显示节点耗用了多少物理存储。这个指标包括主分片和副本分片在内。如果限流时间很大,那可能表明你的磁盘限流设置得过低(在segments-and-merging里讨论过)。
"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
显示已经索引了多少文档。这个值是一个累加计数器。在文档被删除的时候,数值不会下降。还要注意的是,在发生内部 索引 操作的时候,这个值也会增加,比如说文档更新。 + 还列出了索引操作耗费的时间,正在索引的文档数量,以及删除操作的类似统计值。get
显示通过 ID 获取文档的接口相关的统计值。包括对单个文档的GET
和HEAD
请求。search
描述在活跃中的搜索(open_contexts
)数量、查询的总数量、以及自节点启动以来在查询上消耗的总时间。用query_time_in_millis / query_total
计算的比值,可以用来粗略的评价你的查询有多高效。比值越大,每个查询花费的时间越多,你应该要考虑调优了。 + fetch 统计值展示了查询处理的后一半流程(query-then-fetch 里的 fetch )。如果 fetch 耗时比 query 还多,说明磁盘较慢,或者获取了太多文档,或者可能搜索请求设置了太大的分页(比如,size: 10000
)。merges
包括了 Lucene 段合并相关的信息。它会告诉你目前在运行几个合并,合并涉及的文档数量,正在合并的段的总大小,以及在合并操作上消耗的总时间。 + 在你的集群写入压力很大时,合并统计值非常重要。合并要消耗大量的磁盘 I/O 和 CPU 资源。如果你的索引有大量的写入,同时又发现大量的合并数,一定要去阅读indexing-performance。 + 注意:文档更新和删除也会导致大量的合并数,因为它们会产生最终需要被合并的段 碎片 。
"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
展示了已缓存的过滤器位集合所用的内存数量,以及过滤器被驱逐出内存的次数。过多的驱逐数 可能 说明你需要加大过滤器缓存的大小,或者你的过滤器不太适合缓存(比如它们因为高基数而在大量产生,就像是缓存一个now
时间表达式)。 + 不过,驱逐数是一个很难评定的指标。过滤器是在每个段的基础上缓存的,而从一个小的段里驱逐过滤器,代价比从一个大的段里要廉价的多。有可能你有很大的驱逐数,但是它们都发生在小段上,也就意味着这些对查询性能只有很小的影响。 + 把驱逐数指标作为一个粗略的参考。如果你看到数字很大,检查一下你的过滤器,确保他们都是正常缓存的。不断驱逐着的过滤器,哪怕都发生在很小的段上,效果也比正确缓存住了的过滤器差很多。field_data
显示 fielddata 使用的内存,用以聚合、排序等等。这里也有一个驱逐计数。和filter_cache
不同的是,这里的驱逐计数是很有用的:这个数应该或者至少是接近于 0。因为 fielddata 不是缓存,任何驱逐都消耗巨大,应该避免掉。如果你在这里看到驱逐数,你需要重新评估你的内存情况,fielddata 限制,请求语句,或者这三者。segments
会展示这个节点目前正在服务中的 Lucene 段的数量。这是一个重要的数字。大多数索引会有大概 50–150 个段,哪怕它们存有 TB 级别的数十亿条文档。段数量过大表明合并出现了问题(比如,合并速度跟不上段的创建)。注意这个统计值是节点上所有索引的汇聚总数。记住这点。memory
统计值展示了 Lucene 段自己用掉的内存大小。(((“memory”, “statistics on”)))这里包括底层数据结构,比如倒排表,字典,和布隆过滤器等。太大的段数量会增加这些数据结构带来的开销,这个内存使用量就是一个方便用来衡量开销的度量值。
操作系统和进程部分
OS
和 Process
部分基本是自描述的,不会在细节中展开讲解。(((“operating system (OS), statistics on”)))它们列出来基础的资源统计值,比如 CPU 和负载。(((“process (Elasticsearch JVM), statistics on”)))OS
部分描述了整个操作系统,而 Process
部分只显示 Elasticsearch 的 JVM 进程使用的资源情况。
这些都是非常有用的指标,不过通常在你的监控技术栈里已经都测量好了。统计值包括下面这些:
- CPU
- 负载
- 内存使用率
- Swap 使用率
- 打开的文件描述符
JVM 部分
jvm
部分包括了运行 Elasticsearch 的 JVM 进程一些很关键的信息。最重要的,它包括了垃圾回收的细节,这对你的 Elasticsearch 集群的稳定性有着重大影响。
垃圾回收入门
在我们描述统计值之前,先上一门速成课程讲解垃圾回收以及它对 Elasticsearch 的影响是非常有用的。(((“garbage collection”)))如果你对 JVM 的垃圾回收很熟悉,请跳过这段。
Java 是一门 垃圾回收 语言,也就是说程序员不用手动管理内存分配和回收。程序员只管写代码,然后 Java 虚拟机(JVM)按需分配内存,然后在稍后不再需要的时候清理这部分内存。
当内存分配给一个 JVM 进程,它是分配到一个大块里,这个块叫做 堆 。JVM 把堆分成两组,用 代 来表示:
新生代(或者伊甸园):: 新实例化的对象分配的空间。新生代空间通常都非常小,一般在 100 MB–500 MB。新生代也包含两个 幸存 空间。
老生代:: 较老的对象存储的空间。这些对象预计将长期留存并持续上很长一段时间。老生代通常比新生代大很多。Elasticsearch 节点可以给老生代用到 30 GB。
当一个对象实例化的时候,它被放在新生代里。当新生代空间满了,就会发生一次新生代垃圾回收(GC)。依然是”存活”状态的对象就被转移到一个幸存区内,而”死掉”的对象被移除。如果一个对象在多次新生代 GC 中都幸存了,它就会被”终身”置于老生代了。
类似的过程在老生代里同样发生:空间满的时候,发生一次垃圾回收,死掉的对象被移除。
不过,天下没有免费的午餐。新生代、老生代的垃圾回收都有一个阶段会“停止时间”。在这段时间里,JVM 字面意义上的停止了程序运行,以便跟踪对象图,收集死亡对象。在这个时间停止阶段,一切都不会发生。请求不被服务,ping 不被回应,分片不被分配。整个世界都真的停止了。
对于新生代,这不是什么大问题;那么小的空间意味着 GC 会很快执行完。但是老生代大很多,而这里面一个慢 GC 可能就意味着 1 秒乃至 15 秒的暂停——对于服务器软件来说这是不可接受的。
JVM 的垃圾回收采用了 非常 精密的算法,在减少暂停方面做得很棒。而且 Elasticsearch 非常努力的变成对 垃圾回收友好 的程序,比如内部智能的重用对象,重用网络缓冲,以及默认启用 docvalues 功能。但最终,GC 的频率和时长依然是你需要去观察的指标。因为它是集群不稳定的头号嫌疑人。
一个经常发生长 GC 的集群就会因为内存不足而处于高负载压力下。这些长 GC 会导致节点短时间内从集群里掉线。这种不稳定会导致分片频繁重定位,因为 Elasticsearch 会尝试保持集群均衡,保证有足够的副本在线。这接着就导致网络流量和磁盘 I/O 的增加。而所有这些都是在你的集群努力服务于正常的索引和查询的同时发生的。
总而言之,长时间 GC 总是不好的,需要尽可能的减少。
因为垃圾回收对 Elasticsearch 是如此重要,你应该非常熟悉 node-stats
API 里的这部分内容:
"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 内存使用有关的常见统计值。你可以看到有多少 heap 被使用了,多少被指派了(当前被分配给进程的),以及 heap 被允许分配的最大值。理想情况下,heap_committed_in_bytes
应该等于heap_max_in_bytes
。如果指派的大小更小,JVM 最终会被迫调整 heap 大小——这是一个非常昂贵的操作。如果你的数字不相等,阅读 <> 学习如何正确的配置它。 heap_used_percent
指标是值得关注的一个数字。Elasticsearch 被配置为当 heap 达到 75% 的时候开始 GC。如果你的节点一直 >= 75%,你的节点正处于 内存压力 状态。这是个危险信号,不远的未来可能就有慢 GC 要出现了。如果 heap 使用率一直 >=85%,你就麻烦了。Heap 在 90–95% 之间,则面临可怕的性能风险,此时最好的情况是长达 10–30s 的 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 中每一个代的内存使用情况。这些统计值很方便观察其相对大小,但是在调试问题的时候,通常并不怎么重要。
"gc": {
"collectors": {
"young": {
"collection_count": 13,
"collection_time_in_millis": 923
},
"old": {
"collection_count": 0,
"collection_time_in_millis": 0
}
}
}
gc
部分显示新生代和老生代的垃圾回收次数和累积时间。大多数时候你可以忽略掉新生代的次数:这个数字通常都很大。这是正常的。与之相反,老生代的次数应该很小,而且
collection_time_in_millis
也应该很小。这些是累积值,所以很难给出一个阈值表示你要开始操心了(比如,一个跑了一整年的节点,即使很健康,也会有一个比较大的计数)。这就是像 Marvel 这类工具很有用的一个原因。GC 计数的 时间趋势 是个重要的考虑因素。GC 花费的时间也很重要。比如,在索引文档时,一系列垃圾生成了。这是很常见的情况,每时每刻都会导致 GC。这些 GC 绝大多数时候都很快,对节点影响很小:新生代一般就花一两毫秒,老生代花一百多毫秒。这些跟 10 秒级别的 GC 是很不一样的。
我们的最佳建议是定期收集 GC 计数和时长(或者使用 Marvel)然后观察 GC 频率。你也可以开启慢 GC 日志记录,在 logging 小节已经讨论过。
线程池部分
Elasticsearch 在内部维护了线程池。这些线程池相互协作完成任务,有必要的话相互间还会传递任务。通常来说,你不需要配置或者调优线程池,不过查看它们的统计值有时候还是有用的,可以洞察你的集群表现如何。
这有一系列的线程池,但以相同的格式输出:
"index": {
"threads": 1,
"queue": 0,
"active": 0,
"rejected": 0,
"largest": 1,
"completed": 1
}
每个线程池会列出已配置的线程数量( threads
),当前在处理任务的线程数量( active
),以及在队列中等待处理的任务单元数量( queue
)。
如果队列中任务单元数达到了极限,新的任务单元会开始被拒绝,你会在 rejected
统计值上看到它反映出来。这通常是你的集群在某些资源上碰到瓶颈的信号。因为队列满意味着你的节点或集群在用最高速度运行,但依然跟不上工作的蜂拥而入。
批量操作的被拒绝数
如果你碰到了队列被拒,一般来说都是批量索引请求导致的。通过并发导入程序发送大量批量请求非常简单。越多越好嘛,对不?
事实上,每个集群都有它能处理的请求上限。一旦这个阈值被超过,队列会很快塞满,然后新的批量请求就被拒绝了。
这是一件 好事情 。队列的拒绝在回压方面是有用的。它们让你知道你的集群已经在最大容量了。这比把数据塞进内存队列要来得好。增加队列大小并不能增加性能,它只是隐藏了问题。当你的集群只能每秒钟处理 10000 个文档的时候,无论队列是 100 还是 10000000 都没关系——你的集群还是只能每秒处理 10000 个文档。
队列只是隐藏了性能问题,而且带来的是真实的数据丢失的风险。在队列里的数据都是还没处理的,如果节点挂掉,这些请求都会永久的丢失。此外,队列还要消耗大量内存,这也是不理想的。
在你的应用中,优雅的处理来自满载队列的回压,才是更好的选择。当你收到拒绝响应的时候,你应该采取如下几步:
- 暂停导入线程 3–5 秒。
- 从批量操作的响应里提取出来被拒绝的操作。因为可能很多操作还是成功的。响应会告诉你哪些成功,哪些被拒绝了。
- 发送一个新的批量请求,只包含这些被拒绝过的操作。
- 如果依然碰到拒绝,再次从步骤 1 开始。
通过这个流程,你的代码可以很自然的适应你集群的负载,做到自动回压。
拒绝不是错误:它们只是意味着你要稍后重试。
这里的一系列的线程池,大多数你可以忽略,但是有一小部分还是值得关注的:
indexing
::
普通的索引请求的线程池
bulk
::
批量请求,和单条的索引请求不同的线程池
get
::
Get-by-ID 操作
search
::
所有的搜索和查询请求
merging
::
专用于管理 Lucene 合并的线程池
文件系统和网络部分
继续向下阅读 node-stats
API,你会看到一串和你的文件系统相关的统计值:可用空间,数据目录路径,磁盘 I/O 统计值,等等。如果你没有监控磁盘可用空间的话,可以从这里获取这些统计值。磁盘 I/O 统计值也很方便,不过通常那些更专门的命令行工具(比如 iostat
)会更有用些。
显然,Elasticsearch 在磁盘空间满的时候很难运行——所以请确保不会这样。
还有两个跟网络统计值相关的部分:
"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
显示和 传输地址 相关的一些基础统计值。包括节点间的通信(通常是 9300 端口)以及任意传输客户端或者节点客户端的连接。如果看到这里有很多连接数不要担心;Elasticsearch 在节点之间维护了大量的连接。http
显示 HTTP 端口(通常是 9200)的统计值。如果你看到total_opened
数很大而且还在一直上涨,这是一个明确信号,说明你的 HTTP 客户端里有没启用 keep-alive 长连接的。持续的 keep-alive 长连接对性能很重要,因为连接、断开套接字是很昂贵的(而且浪费文件描述符)。请确认你的客户端都配置正确。
断路器
终于,我们到了最后一段:跟 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
}
这里你可以看到断路器的最大值(比如,一个请求申请更多的内存时会触发断路器)。这个部分还会让你知道断路器被触发了多少次,以及当前配置的间接开销。间接开销用来铺垫评估,因为有些请求比其他请求更难评估。
主要需要关注的是 tripped
指标。如果这个数字很大或者持续上涨,这是一个信号,说明你的请求需要优化,或者你需要添加更多内存(单机上添加,或者通过添加新节点的方式)。