凌晨两点,手机疯狂震动。打开一看,监控告警:Elasticsearch 写入延迟飙升到 30 秒,批量写入请求堆积如山。

我第一反应是:是不是索引 mapping 又改崩了?或者是分片分布不均?

排查了两个小时,最后发现真相竟然是——一个定时任务在疯狂往单个索引灌数据,把写入线程池活活打满了。

这篇文章记录了我如何一步步定位问题,以及最后给出的监控脚本和优化方案。

问题现场:写入请求突然卡死

晚上 10 点前一切正常,日志里写入延迟稳定在 100ms 左右。10 点一到,延迟突然飙升:

[2026-04-10 22:00:15] bulk request latency: 32.5s (queue_size: 200)
[2026-04-10 22:00:20] bulk request latency: 45.8s (queue_size: 500)
[2026-04-10 22:00:25] bulk request rejected: queue full

写入请求开始被拒绝,应用层疯狂重试,反而让情况更糟。

我第一时间想到的几个常见原因:

  1. 分片分布不均:某些节点负载过高?
  2. Mapping 问题:字段类型冲突导致解析慢?
  3. 索引刷新间隔:refresh_interval 设置太小?

但检查了一圈,都没发现异常。集群状态是 green,分片分布均匀,mapping 也没问题。

用 _cat API 快速诊断

既然常规手段看不出问题,我决定从 ES 自带的 _cat API 入手。这套 API 虽然文档不多,但诊断问题时极其好用。

第一步:检查线程池状态
curl -XGET 'http://localhost:9200/_cat/thread_pool?v&h=node_name,name,active,queue,rejected&s=queue:desc'

输出吓我一跳:

node_name            name                active queue rejected
node-1               write               32     1000  5423
node-2               write               32     1000  4821
node-3               write               32     1000  3927

写入线程池队列全部打满,拒绝数还在疯狂增长。这说明不是某个节点的问题,而是整个集群的写入能力被耗尽了。

第二步:定位热点索引

哪个索引在疯狂占用写入资源?用 _cat/indices 查看索引写入速率:

curl -XGET 'http://localhost:9200/_cat/indices?v&health=green&s=docs.count:desc' | head -20

结果:

health status index              docs.count store.size
green  open   user_behavior_2026  15234567    15.2gb
green  open   order_records       8234567     8.2gb
green  open   product_catalog     123456      1.2gb

user_behavior_2026 这个索引的文档数异常多,而且还在快速增长。但光看文档数还不够,我要知道实时的写入速率。

_cat/segments 查看索引段文件增长情况:

curl -XGET 'http://localhost:9200/_cat/segments/user_behavior_2026?v&h=index,shard,segment,size,size.memory'

发现这个索引有大量新段文件在生成,刷新频率异常高。

第三步:检查索引设置

查看该索引的设置:

curl -XGET 'http://localhost:9200/user_behavior_2026/_settings?pretty'

问题找到了:

{
  "user_behavior_2026": {
    "settings": {
      "index": {
        "refresh_interval": "1s",
        "number_of_replicas": 2,
        "number_of_shards": 3
      }
    }
  }
}

refresh_interval 只有 1 秒!这意味着每秒都会生成新的段文件,然后触发段合并,消耗大量 I/O 和 CPU。

而且这个索引只有 3 个分片,却承载了全站的用户行为数据写入。单分片写入压力过大,直接把线程池打满。

根因:定时任务批量灌数据

找到热点索引后,我联系了业务方,才知道晚上 10 点有个定时任务,把一天积累的用户行为日志批量写入 ES。

这个任务没有做流量控制,直接并发写入,瞬间把写入线程池打满。而 refresh_interval=1s 的设置,让情况雪上加霜。

解决方案:分步优化

1. 调整索引刷新间隔

临时缓解:

curl -XPUT 'http://localhost:9200/user_behavior_2026/_settings' -H 'Content-Type: application/json' -d'
{
  "index": {
    "refresh_interval": "30s"
  }
}
'

将刷新间隔从 1s 调整为 30s,减少段文件生成和合并频率。

2. 增加索引分片数

长期方案:重新规划分片数量。

根据写入量和节点数,建议分片数 = (节点数 × 单节点最大写入TPS) / 单分片写入TPS。

我们的集群有 3 个节点,单节点写入 TPS 约 2000,单分片 TPS 约 500。计算得出需要 12 个分片。

创建新索引模板:

curl -XPUT 'http://localhost:9200/_template/user_behavior_template' -H 'Content-Type: application/json' -d'
{
  "index_patterns": ["user_behavior_*"],
  "settings": {
    "number_of_shards": 12,
    "number_of_replicas": 1,
    "refresh_interval": "30s"
  }
}
'
3. 业务侧限流

给定时任务加上流量控制:

import time
from elasticsearch import Elasticsearch

es = Elasticsearch(['http://localhost:9200'])

def bulk_insert_with_throttle(actions, batch_size=1000, delay_ms=100):
    """
    批量写入,带限流控制
    """
    for i in range(0, len(actions), batch_size):
        batch = actions[i:i+batch_size]
        es.bulk(body=batch)
        if i + batch_size < len(actions):
            time.sleep(delay_ms / 1000.0)

# 使用示例
actions = [
    {"_index": "user_behavior_2026", "_source": {...}},
    # ... 更多数据
]

bulk_insert_with_throttle(actions, batch_size=500, delay_ms=200)
4. 监控脚本:线程池队列告警

最后,我写了个监控脚本,每分钟检查线程池队列,超过阈值就告警:

#!/bin/bash
# es_thread_pool_monitor.sh

THRESHOLD=500
ES_HOST="localhost:9200"

while true; do
    # 获取所有节点的写入线程池状态
    result=$(curl -s -XGET "http://${ES_HOST}/_cat/thread_pool/write?v&h=node_name,queue,rejected" | tail -n +2)
    
    # 检查队列是否超过阈值
    echo "$result" | while read node_name queue rejected; do
        if [ "$queue" -gt "$THRESHOLD" ]; then
            echo "[ALERT] $(date '+%Y-%m-%d %H:%M:%S') - Node: $node_name, Queue: $queue, Rejected: $rejected"
            # 这里可以接入告警系统(钉钉、飞书、邮件等)
        fi
    done
    
    sleep 60
done

部署到服务器后,配合 crontab 或 systemd 管理:

# crontab
* * * * * /path/to/es_thread_pool_monitor.sh >> /var/log/es_monitor.log 2>&1

优化效果

调整后,写入延迟从 30s 降回 100ms,线程池队列稳定在 50 以下:

[2026-04-11 10:00:15] bulk request latency: 95ms (queue_size: 45)
[2026-04-11 10:00:20] bulk request latency: 102ms (queue_size: 38)
[2026-04-11 10:00:25] bulk request latency: 88ms (queue_size: 52)

写在最后

这次排查让我深刻体会到:ES 的线程池监控太重要了

很多 ES 问题表面上是索引慢、查询慢,但根因往往是线程池被打满。而 _cat API 是定位这类问题的利器,比翻日志快得多。

几个经验教训:

  1. 批量写入一定要限流:别让定时任务无脑并发,ES 扛不住
  2. refresh_interval 别设太小:除非实时性要求极高,否则 30s 够用
  3. 分片规划要提前做:根据写入量估算分片数,别等打满再改
  4. 线程池队列要监控:队列堆积是写入瓶颈的早期信号

如果你也遇到 ES 写入卡死的问题,先别急着查 mapping,看看线程池队列再说。


相关命令速查

# 查看线程池状态
curl -XGET 'http://localhost:9200/_cat/thread_pool?v&h=node_name,name,active,queue,rejected'

# 查看索引写入速率
curl -XGET 'http://localhost:9200/_cat/indices?v&s=docs.count:desc'

# 调整刷新间隔
curl -XPUT 'http://localhost:9200/your_index/_settings' -H 'Content-Type: application/json' -d'{"index": {"refresh_interval": "30s"}}'

# 查看索引设置
curl -XGET 'http://localhost:9200/your_index/_settings?pretty'
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐