ES集群分片打满导致Red与日志链路中断排障
记录时间:2026-10-17(2026-10-19 更新 4.4 告警表达式)
环境:dyck 生产 ELK(RKE1,3 节点 Elasticsearch 7.17.9,tools 命名空间,NFS 数据盘 nfs-dyck,Logstash → Kafka 日志链路)
一、问题现象
2026-10-17 上午连续收到两条告警,外加 Logstash 日志刷屏:
- 08:02:30 ESClusterRed(critical):dyck-elk-cluster 存在未分配主分片,读写不可用
- 09:17:00 Kafka 消费者组无成员(warning):消费者组 logstash 无活跃成员,Topic 积压
- Logstash 持续重试写入,报
unavailable_shards_exception:
[kafka-gzeport-openapi-cms-2026.10.17][0] primary shard is not active Timeout: [1m],
request: [BulkShardRequest ... containing [53] requests]
我看时间线就对上了:ES Red 后 18 秒 Logstash 开始报错,Kafka 告警滞后约 75 分钟。这两条告警是一条因果链。
二、排查过程
2.1 先排节点与磁盘
我的第一反应是节点掉了。查下来:
kubectl -n tools get sts dyck-elasticsearch # 3/3
kubectl -n tools get pods -o wide | grep elastic
三个 Pod 全是 Running,183 天零重启,events 也没有异常。接着怀疑磁盘水位触发 flood stage:
kubectl -n tools exec dyck-elasticsearch-0 -- df -h /usr/share/elasticsearch/data
NFS 卷 6.9T 只用了 814G(12%),这条也排除。节点和磁盘都不是问题。
2.2 用 allocation explain 定位
带上 elastic 账号查集群健康:3 节点、active_shards 6000、unassigned_shards 6,状态 red。然后查分配失败原因:
kubectl -n tools exec dyck-elasticsearch-0 -- curl -s -u elastic:<password> \
'http://localhost:9200/_cluster/allocation/explain?pretty'
三个节点的 decider 输出完全一样:
"decider": "shards_limit",
"explanation": "too many shards [2000] allocated to this node,
cluster setting [cluster.routing.allocation.total_shards_per_node=2000]"
每个节点都顶满了 2000 片,新索引的主分片没有地方可放。
2.3 顺带排掉的两个假设
- 我一开始按仓库文档用了
logging命名空间,命令全部返回 404,实际生产在 tools。这个差异已记入仓库文档的待更正项。 - 日志里同时段有
lettuce Reconnecting to 10.202.17.52:6379(Redis 重连),我怀疑过是不是基础设施级故障。后来确认 Redis 侧没有对应告警,ES 节点也没有掉线,两者没有关联。
三、根因分析
日志链路是 31 个 Kafka topic,每个服务每天建一个日索引(kafka-<service>-YYYY.MM.DD),1 主 1 副。日索引只建不删,攒了大约 3 到 4 个月:
约 3000 个日索引 × 2 分片(主+副)= 6000 分片
3 节点 × 2000 片/节点上限 = 6000
数字严丝合缝。ES 的日索引按 UTC 日期命名,北京 08:00 正好是 UTC 00:00 滚动点,新索引在这个时刻创建,主分片分配失败,集群转 Red,Logstash 写不进去,输出端反压导致消费停摆。Kafka 的积压是次生现象,Kafka 本身没有问题。
total_shards_per_node=2000 是人为设置的(默认 -1 无限制),说明当初有防分片爆炸的意识,但没有配套的清理策略,缺一个长期清理机制。
四、解决方案
4.1 临时抬高上限
先把上限从 2000 提到 3000,让 6 个未分配分片能落地,恢复写入。注意用 persistent 固化;如果之前用过 transient,要先清掉(transient 优先级更高,会覆盖 persistent)。
| 参数 | 值 | 说明 |
|---|---|---|
cluster.routing.allocation.total_shards_per_node |
2000 → 3000 | 临时生效,清理完成后可调回 2000 |
kubectl -n tools exec dyck-elasticsearch-0 -- curl -s -u elastic:<password> -X PUT \
'http://localhost:9200/_cluster/settings' -H 'Content-Type: application/json' \
-d '{"persistent":{"cluster.routing.allocation.total_shards_per_node":3000}}'
4.2 定时清理过期索引
新建 CronJob delete-kafka-es-index-job(tools),每天 08:08 删除 90 天前的日索引。参考了仓库里 v1 的 es-delete-job.yaml,主要改了两处:服务地址换成 v3 集群的 dyck-elasticsearch:9200;删除范围从”正好 N 天前那一天”改成”所有早于截止日的”,否则历史存量永远清不掉。
镜像用 reg-hub.gzeport.com/library/curl-jq:latest;截止日用 date -u 按 UTC 计算,和索引命名对齐。tools 命名空间有 ResourceQuota,Pod 必须显式写 cpu/memory 的 requests 与 limits。
4.3 索引规划收敛
Logstash 输出从”每服务一个索引”改成多服务合并的日索引:
# 改前
index => "kafka-%{[service]}-%{+YYYY.MM.dd}"
# 改后
index => "kafka-logs-%{+YYYY.MM.dd}"
service 字段还在文档里,Kibana 用字段过滤。合并后分片数从约 3420 降到约 180。
4.4 分片数预警
在 vmrule-elasticsearch.yaml 的 elasticsearch.shards 组加了一条规则。第一版用的是 elasticsearch_cluster_health_number_of_shards,上线后发现 elasticsearch_exporter 根本没有这个指标(规则能加载但永远查不到数据),改成 active_shards 口径,同时把”每节点多少片”改成”分片使用率”表达,以后调上限只改一个百分比:
| 参数 | 值 | 说明 |
|---|---|---|
| 表达式 | (elasticsearch_cluster_health_active_shards / elasticsearch_cluster_health_number_of_data_nodes) / 2000 * 100 > 95 |
分片使用率 > 95%(2000 片/数据节点为上限);exporter 无 number_of_shards 指标 |
| for | 15m | 与同组规则一致 |
| severity | warning | 提前预警,不是故障告警 |
分母里的 2000 是 total_shards_per_node 的集群配置,exporter 不暴露配置值,只能写成常量,调整上限时同步改。告警 summary 显示使用率百分比,不再显示每节点片数。