写入瓶颈排查全流程

22 May 2026 – wusfe · 4 min read

写入瓶颈排查全流程(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 更新、索引创建等

重点关注 writebulk 线程池的 rejectedqueue 列。如果 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

长远的架构优化

排查解决一时的问题,但本质上要避免写入瓶颈:

  1. 客户端限流——在应用层控制写入速率,别把 ES 当无限容量的黑洞
  2. 写入对列——应用侧加本地对队列(Kafka/RabbitMQ),削峰填谷
  3. 滚动索引——单索引控制在 50GB 以内,用 ILM 自动管理
  4. 独立写入节点——高写入场景用专门的 ingest/data 节点,和查询节点隔离
  5. 监控先行——把 thread_pool rejected、heap、disk、GC 指标接入告警,别等人报才知道

总结

写入瓶颈排查的黄金顺序:线程池 → Heap/GC → 磁盘 IO → refresh/merge 频率 → 单索引大小。 每层往下走一级,直到找到真正的瓶颈。

记住一句话:429 不是 ES 的错,是上游没有限流。所有不限速的写入,最终都会变成告警。