写入瓶颈排查全流程
写入瓶颈排查全流程(rejected、bulk queue、429)
线上 ES 突然开始返回 429 Too Many Requests,写入服务日志刷满 es_rejected_execution_exception。你知道是 ES 扛不住写入压力了,但你不知道:到底是哪个环节慢了?该调什么参数?
这篇文章给你一套完整的写入瓶颈排查框架,按图索骥就行。
ES 写入链路回顾
客户端 → 协调节点 → 主分片写入 → 副本同步 → 客户端收到确认
↓
translog → refresh → flush
写入瓶颈可能出现在任何一个环节。排查的思路是从上往下:先看请求能不能进来,再看进来后卡在哪。
第一步:看 rejected 类型
GET /_cat/thread_pool?v&h=id,name,active,rejected,queue,queue_size
| 线程池 | 职责 | 被 rejected 意味着 |
|---|---|---|
write |
处理索引写入请求 | 主分片写入忙不过来 |
search |
处理搜索请求 | 查询太慢,积压大量搜索 |
bulk |
处理 Bulk 请求(旧版用这个,7.x+ 合并到 write) | 同上 |
flush |
刷盘操作 | 磁盘 IO 跟不上 |
refresh |
刷新 segment | refresh 频率太高 |
force_merge |
合并 segment | 有人点了 forcemerge,或者自动 merge 太激进 |
management |
集群管理操作 | mapping 更新、索引创建等 |
重点关注 write 和 bulk 线程池的 rejected 和 queue 列。如果 write rejected 持续增长,说明写入被拒绝——数据丢了或者需要客户端重试。
第二步:确认是不是队列太短
默认的 write 线程池队列长度是固定的(通常等于处理器核心数)。如果瞬时写入峰值超过队列容量,直接 rejected。
# 检查线程池配置
GET /_cluster/settings?include_defaults=true&filter_path=*.thread_pool.write
临时缓解(不推荐作为长期方案):
PUT /_cluster/settings
{
"transient": {
"thread_pool.write.queue_size": 1000
}
}
这只是暂时让队列入得更多,根因没解决。 队列入得越多,文档在队列里等的越久,客户端超时的可能越大。
第三步:看写入耗时
GET /_cat/nodes?v&h=name,diskTotal,diskUsed,heapPercent,load_1m
GET /_cat/indices?v&h=index,health,pri,rep,docsCount,docsDeleted,storeSize&s=storeSize:desc
关键指标:
# 索引级别的写入统计
GET /_stats/indexing?pretty
{
"indexing": {
"index_total": 12345678,
"index_time_in_millis": 987654,
"index_current": 15,
"delete_total": 234567,
"delete_time_in_millis": 45678
}
}
index_time_in_millis / index_total= 平均每条写入耗时。如果超过 5ms,说明写入链路上有问题index_current= 当前正在进行的写入操作数。持续在高位说明处理不过来
第四步:检查磁盘 IO
写入最终要落盘,磁盘 IO 是常见瓶颈:
# Linux 下
iostat -x 1
# 关注 %util 和 await
# %util > 80%:磁盘基本是瓶颈
# await > 10ms:IO 延迟偏高
如果磁盘是机械硬盘,读到这里就可以停了——换 SSD。ES 官方不推荐机械硬盘,写入场景尤其。
如果是 SSD 但 IO 仍然高,检查:
- 是否在高峰期做了 forcemerge
- 是否开启了 swap(
GET /_nodes/stats/os查看 swap 使用量) - translog 是否是同步模式
第五步:检查 Refresh 频率
GET /_stats/refresh?pretty
{
"refresh": {
"total": 987654,
"total_time_in_millis": 345678
}
}
如果 refresh 次数异常高(每秒远超一次),说明有索引的 refresh_interval 被设得太激进。每个 refresh 产生一个 segment,segment 过多 → merge 频繁 → IO 更高 → 写入变慢。
第六步:检查 Segment 数量
GET /_cat/segments?v&h=index,shard,segment,size,sizeMemory&s=size:desc
如果某个索引 segment 数上千,该索引的写入会被 merge 拖垮。
GET /_nodes/stats/indices/merge?pretty
查看 merge 线程的活跃数和耗时。如果 merge 持续跑满:
- 可能是索引不滚动,单索引太大
- 可能是有人开着 forcemerge
第七步:检查 Heap 和 GC
GET /_nodes/stats/jvm?pretty
重点关注 heap_used_percent 和 GC 耗时:
- heap 持续 > 75%:内存紧张,可能是 fielddata 或查询缓存膨胀
- old GC 耗时 > 1s:GC 时 ES 会暂停,写入请求堆积,GC 结束后瞬间涌入,直接打爆队列 → 429 链式反应
- GC 频率 > 每 10 分钟一次:堆配置不合理
综合排查顺序(SOP)
看到 429 / rejected →
1. GET /_cat/thread_pool → 确定是哪个线程池 rejected
2. GET /_nodes/stats → 检查 heap、GC、磁盘、CPU
3. GET /_stats/indexing → 检查写入耗时
4. GET /_stats/refresh → 检查 refresh 频率
5. GET /_stats/merge → 检查 merge 是否频繁
6. iostat → 检查磁盘 IO
7. GET /_cat/indices → 是否单索引过大(> 50GB)
常见根因与解法速查
| 症状 | 可能根因 | 解法 |
|---|---|---|
| write rejected | 写入峰值超过线程池容量 | 增大 queue_size(临时)、客户端限流(长期) |
| bulk rejected | 单批太大或太小 | 控制在 5-15MB/批 |
| refresh 耗时高 | refresh 太频繁 | 增大 refresh_interval,或大导入时设为 -1 |
| merge 耗时高 | segment 太多,单索引过大 | 启用 rollover,单索引 30-50GB |
| 磁盘 IO 高 | 机械硬盘或 swap | 换 SSD,关闭 swap(bootstrap.memory_lock: true) |
| GC 频繁 | heap 配置不合理 | 堆设为物理内存的 50%,上限 31GB(压缩指针) |
| 索引写入慢 | 副本太多 | 大导入时 number_of_replicas: 0 |
长远的架构优化
排查解决一时的问题,但本质上要避免写入瓶颈:
- 客户端限流——在应用层控制写入速率,别把 ES 当无限容量的黑洞
- 写入对列——应用侧加本地对队列(Kafka/RabbitMQ),削峰填谷
- 滚动索引——单索引控制在 50GB 以内,用 ILM 自动管理
- 独立写入节点——高写入场景用专门的 ingest/data 节点,和查询节点隔离
- 监控先行——把 thread_pool rejected、heap、disk、GC 指标接入告警,别等人报才知道
总结
写入瓶颈排查的黄金顺序:线程池 → Heap/GC → 磁盘 IO → refresh/merge 频率 → 单索引大小。 每层往下走一级,直到找到真正的瓶颈。
记住一句话:429 不是 ES 的错,是上游没有限流。所有不限速的写入,最终都会变成告警。