一、现象:ES 集群忽快忽慢,到底怎么了?
实际操作要点
如果你正在处理JVM,先别急着照搬网上的参数。当你发现 Elasticsearch 查询偶尔变慢、甚至超时,但节点 CPU 和磁盘看起来负载不高,往往是因为 JVM GC 停顿。GC 暂停所有线程时,请求就会排队等回复。而 GC 频繁发生的根源,常常是 shard 碎片化导致堆内存膨胀。这两个概念对新手比较抽象,下面我拆开讲清楚。
二、先理解 ES 的 shard(分片)
2.1 什么是 shard?
Elasticsearch 用 shard 来水平拆分数据。你可以把索引想象成一本书,shard 就是其中的章节。每个 shard 本身是一个完整的 Lucene 索引,可以独立存储和搜索。
2.2 碎片化是怎么产生的?
ES 的 shard 内部由多个 segment(段)组成。每次写入新文档,都会生成一个新 segment。后台 merge 线程会合并小 segment,但频繁写入和删除操作会让 segment 数量暴增——这就是“碎片化”。
碎片化的后果:
- 内存浪费:每个 segment 都占用文件句柄和缓存,堆内存被大量小 segment 占满。
- 查询变慢:查询需要读取更多 segment 文件,磁盘 I/O 和 CPU 增加。
- GC 压力增大:堆内存里存活的对象太多,GC 要花更长时间回收。
关联教程:此处可内链到“JVM部署与验证”内容。
三、JVM 堆内存和 GC 停顿
3.1 堆内存是 ES 的工作台
ES 运行在 Java 虚拟机上,堆内存(Heap)用来存放索引缓存、查询结果、批量请求等临时数据。堆空间有限(通常设置不超过 32GB),一旦使用率过高,JVM 就要启动垃圾回收(GC)。
3.2 GC 为什么会“停”?
GC 有两种常见算法:
- CMS(Concurrent Mark Sweep):并发标记,但老年代内存碎片化严重时会转为 Serial Old,导致长时间暂停(Full GC)。
- G1(Garbage First):将堆划分为多个 Region,可预测暂停时间,但仍可能在混合收集时暂停。G1 是 ES 7.x 后的默认选项。
无论哪种算法,当堆内存被大量临时对象(比如 segment 查询时产生的 Filter、DocValues)填满,GC 频率和停顿时间都会飙升。停顿期间,所有线程被冻结,客户端请求超时。
四、shard 碎片如何加剧 GC 停顿?
我的处理经验
想象一下:堆内存好比一间小仓库。segment 就是仓库里的纸箱。如果纸箱又多又小(碎片化),仓库很快被塞满,管理员(GC)需要频繁清理,每次清理要搬出所有纸箱翻找垃圾。仓库越大、纸箱越多,清理时间就越长。
现实中:
- 大量小 segment → 堆内存中保留大量 SegmentReader 对象和缓存 → 老年代迅速增长 → Mixed GC / Full GC 时间从几十毫秒飙到几秒。
- merge 线程反作用:merge 本身会创建临时 Segment,如果 merge 跟不上写入速度,堆积的待合并 segment 也会占用堆。
- docvalues & fielddata:碎片化导致每个 segment 维护自己的倒排索引,堆中需要缓存更多元数据。
延伸阅读:此处可内链到“JVM配置案例”相关文章。
五、小白也能上手的调优策略
5.1 控制 shard 数量,从根源减少碎片
创建索引时:
- 每个 shard 大小建议 20-50GB(别太小也别太大)。
- 一个节点上的 shard 总数不要超过 3 倍 CPU 核心数(比如 8 核节点,shard ≤ 24 个)。
- 使用
index.routing.allocation.total_shards_per_node限制每个节点的 shard 个数。
5.2 主动合并 segment,减少碎片
通过 Force Merge API 将 segment 合并成少数大段:POST /my_index/_forcemerge?max_num_segments=1
注意:只对不再写入的索引(如日志索引)有效。正在写入的索引强制合并会影响写入性能。合并后 segment 数量减少,堆缓存和 GC 压力显著降低。
5.3 优化 JVM 堆内存设置
- 不要超过 32GB:JVM 在 32GB 后关闭压缩指针,导致对象引用占 8 字节而非 4 字节,内存反而更低效。
- 堆大小建议 50% 系统内存:比如 64GB 机器,堆分配 31GB,剩余留给操作系统和磁盘缓存。
- 启用 G1GC:ES 7.x 默认,但确认配置:
-XX:+UseG1GC
5.4 调高 merge 和 GC 相关配置
在 elasticsearch.yml 中:
indices.memory.index_buffer_size:默认 10%,若写入量大可提至 20%,减少 merge 频率。index.merge.scheduler.max_thread_count:根据磁盘性能调整(SSD 可加大)。- JVM 参数:
-XX:G1HeapRegionSize=16m(region 大小调大减少 region 数量)
但这些建议根据实际集群调整,不要盲目套用。
相关阅读:此处可内链到“JVM常见问题”专题。
补充参考:此处可内链到“JVM故障排查实例”。
进阶阅读:此处可内链到“JVM性能优化”指南。
六、如何知道自己有没有碎片和 GC 问题?
实际操作要点
用 Cat API 看 segment 总数:GET _cat/segments?v&h=index,shard,segment,size,committed,search
如果某个索引有几十个 segment(即使 committed 了),说明碎片严重。
用 Nodes Stats API 看 GC 时间:GET _nodes/stats/jvm,indices
关注 gc.collectors.young.collection_time_in_millis 和 old,如果 old gc 耗时占总运行时间 >5%,需要干预。
也可以用 GET _cat/thread_pool?h=name,active,rejected,completed,queue,size 查看是否有线程排队(search 线程池)。
七、调优后的验证与回滚——JVM
配置前的检查
验证:调优后持续监控 GC 停顿时间(建议用 Prometheus + Grafana 或 Cerebro)。如果 old gc 时长下降 50% 以上且查询 RT 稳定,则有效。
回滚:
- shard 数量调整只能通过重建索引实现(reindex),耗时较长,建议先备份原索引。
- JVM 参数修改需要重启节点,重启前确认其他节点能扛住负载。
- merge 操作不可逆,但可通过重新索引恢复原始数据。
八、总结
故障定位思路
ES 集群的性能抖动往往不是单个原因,而是 shard 碎片化和 JVM GC 相互放大的结果。理解 shard 和 segment 的关系、堆内存与 GC 的原理,就能有方向地排查。控制 shard 数量、定期 merge、合理配置堆和 GC,这三招能帮你解决大部分“忽快忽慢”的烦恼。别忘了先监控再调优,调优后验证。
延伸阅读
