运维与监控
巡检从哪看起
ES 的运维接口非常全,但日常真正高频用的就那几组。先把「看一眼」的工具列出来,再讲指标怎么读。
集群健康
# 基础健康
curl -s "http://localhost:9200/_cluster/health?pretty"
# 带上索引级明细,看是哪个索引在拖后腿
curl -s "http://localhost:9200/_cluster/health?level=indices&pretty"
# 等集群恢复到指定状态,最多等 30 秒(适合重启后确认)
curl -s "http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=30s&pretty"
_cluster/health 返回的字段里,运维要盯的是这几个:status(三色)、unassigned_shards(未分配分片数)、initializing_shards(恢复中的分片数)、relocating_shards(迁移中的分片数)、number_of_pending_tasks(待处理的集群状态变更,持续大于 0 说明 master 忙不过来)。
_cat 系列
_cat 接口返回纯文本表格,适合终端里快速扫。常用的几条:
| 命令 | 看什么 |
|---|---|
_cat/health?v | 集群三色、节点数、分片数 |
_cat/nodes?v | 节点角色、堆使用率、CPU、负载、master 标记 |
_cat/indices?v&s=store.size:desc | 索引文档数、大小、健康状态,按大小排序 |
_cat/shards?v | 每个分片在哪个节点、状态、大小 |
_cat/allocation?v | 每个节点的分片数、磁盘占用、剩余空间 |
_cat/thread_pool?v | 各线程池的活跃数、队列、拒绝数 |
_cat/pending_tasks?v | 积压的集群任务 |
_cat/segments?v | 段数量,判断是否需要 forcemerge |
_cat/recovery?v&active_only=true | 正在恢复的分片及进度 |
_cat/aliases?v | 别名指向哪些索引 |
# 只挑关心的列,输出好读很多
curl -s "http://localhost:9200/_cat/nodes?v&h=name,node.role,heap.percent,ram.percent,cpu,load_1m,disk.avail,master"
curl -s "http://localhost:9200/_cat/indices?v&h=index,health,pri,rep,docs.count,store.size&s=store.size:desc"
_cat/nodes 里的 heap.percent 超过 75%、disk.used_percent 超过 85%、load_1m 持续高于 CPU 核数,都是该介入的信号。每天花一分钟扫这三列,比等告警响了再查要主动得多。
关键指标
JVM 堆使用率
堆是 ES 最脆弱也最关键的资源。对象(分片元数据、聚合桶、批量请求缓冲、fielddata)都堆在堆里,堆一满就是漫长的 GC,GC 一停就是查询超时。
# 节点级 JVM 指标
curl -s "http://localhost:9200/_nodes/stats/jvm?pretty"
# 只取堆使用率和 GC 次数
curl -s "http://localhost:9200/_nodes/stats/jvm?filter_path=nodes.*.jvm.mem.heap_used_percent,nodes.*.jvm.gc.collectors"
判断标准:
| 堆使用率 | 状态 | 动作 |
|---|---|---|
| 持续低于 60% | 健康 | 有余量,可承接更多分片或更大聚合 |
| 60%-75% | 正常偏高 | 关注趋势,检查是否有大聚合、分片过多 |
| 75%-85% | 警戒 | 排查大查询、fielddata、分片数;考虑扩容 |
| 持续高于 85% | 危险 | 随时可能 OOM 或触发熔断,优先降负载 |
| 老年代 GC 后仍高于 80% | 临界 | 说明对象回收不掉,扩容或减少分片,别硬扛 |
ES 还有一层保护叫熔断器(circuit breaker):堆用到 95% 左右会拒绝新的聚合/批量请求,返回 CircuitBreakingException。它不是故障,是最后的保险——看到这个异常,说明负载已经超了,该扩容或优化查询,而不是调大熔断阈值。
# 看熔断器触发次数
curl -s "http://localhost:9200/_nodes/stats/breaker?pretty"
段合并与段数量
每个 refresh 都会生成新段,段太多会拖慢查询、占用文件句柄。索引处于「只追加」状态时(比如日志),段数量会在几十到几百之间波动,这是正常的;但一个索引几百个段且不再下降,就该处理了:
# 单个索引的段信息
curl -s "http://localhost:9200/logs-2025.10.25/_segments?pretty"
# 合并统计:当前合并数、累计合并耗时
curl -s "http://localhost:9200/_stats/merge?pretty"
对只读的历史索引,可以做一次 forcemerge 把段合并到 1 个,查询会明显变快、磁盘占用也会下降:
POST /logs-2025.09.01/_forcemerge?max_num_segments=1
forcemerge 会重写整个索引,消耗大量磁盘 IO 和 CPU,几 TB 的索引能跑几个小时。只对确定不再写入的历史索引做,并且安排在低峰期。对活跃索引做 forcemerge,等于和业务写入抢资源,还会马上被新的 refresh 打回原形。
搜索与索引延迟
# 索引级:查询耗时、写入耗时、refresh 次数
curl -s "http://localhost:9200/_stats/search,indexing,refresh?pretty"
# 线程池:队列积压和拒绝数,最能反映「扛不扛得住」
curl -s "http://localhost:9200/_cat/thread_pool?v&h=node_name,name,active,queue,rejected,completed"
几个判断口径:
search.query_time_in_millis除以查询次数得到平均查询耗时,但它会把所有查询平均掉,长尾问题看不出来。真正有用的是看趋势:同一业务在数据量增长后平均耗时是否同步上涨。indexing.index_time_in_millis反映写入开销。写入慢通常不是 ES 慢,而是 refresh 太频繁、文档太大、或者 merge 在抢 IO。thread_pool.write.queue持续大于 0,说明写入速度超过了处理能力;rejected开始增长,就是明确的过载信号,客户端会收到 429。thread_pool.search.queue积压,通常是某个大聚合或深分页把协调节点拖住了。
磁盘水位
磁盘是 ES 最容易出硬故障的地方:写满不仅写不进,还会让分片无法分配、集群变红。ES 用三级水位线自动保护:
| 水位 | 默认阈值 | 行为 |
|---|---|---|
| low(低水位) | 85% | 不再向该节点分配新分片 |
| high(高水位) | 90% | 更积极地迁走分片,仍可写入 |
| flood_stage(洪水位) | 95% | 该节点上所有索引被设为只读(read_only_allow_delete),写入直接失败 |
# 每个节点磁盘剩余
curl -s "http://localhost:9200/_cat/allocation?v"
# 看当前水位配置
curl -s "http://localhost:9200/_cluster/settings?include_defaults=true&flat_settings=true&pretty" | grep -i watermark
水位线可以按集群调(比如大磁盘机器上适当放宽),但不要为了消警把阈值调到 99%——那只是把「写不进去」的时间点往后推,代价是恢复时更被动。正确做法是清理旧索引、扩容磁盘、或者用 ILM 自动删除过期数据。磁盘只读的解除方法在第 6 篇讲。
其他值得盯的指标
| 指标 | 位置 | 异常含义 |
|---|---|---|
number_of_pending_tasks | _cluster/health | 持续大于 0,master 处理不过来 |
unassigned_shards | _cluster/health | 大于 0 且不下降,分片分配有问题 |
jvm.gc.collectors.old.collection_time | _nodes/stats/jvm | 老年代 GC 时间占比高,堆不够 |
fs.total.available_in_bytes | _nodes/stats/fs | 剩余空间,配合水位线看 |
process.cpu.percent | _nodes/stats/process | 长期接近 100%,CPU 瓶颈 |
indices.fielddata.memory_size_in_bytes | _nodes/stats/indices | 异常增长,通常是给 text 字段开了 fielddata |
thread_pool.write.rejected | _cat/thread_pool | 写入过载,客户端收到 429 |
ILM:索引生命周期管理
日志、监控这类数据有明确的生命周期:刚写入时被频繁查询(热),几天后只做偶尔排查(温),一个月后基本不看但合规要求留着(冷),三个月后删除。手工管理这套流程既繁琐又容易出错,ILM 就是干这个的。
ILM 把索引的一生分成四个阶段:
| 阶段 | 典型动作 | 目的 |
|---|---|---|
| hot | rollover(按大小/时间滚动)、设置高优先级 | 承接写入,保证写入性能 |
| warm | forcemerge、降低优先级 | 索引不再写入,合并段提升查询效率 |
| cold | searchable_snapshot(转可搜索快照) | 大幅降低存储成本,仍可查询 |
| delete | delete | 到期清理,释放空间 |
一份完整的热温冷归档策略
PUT /_ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50gb",
"max_age": "30d"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "7d",
"actions": {
"forcemerge": { "max_num_segments": 1 },
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "30d",
"actions": {
"searchable_snapshot": {
"snapshot_repository": "fs_backup"
},
"set_priority": { "priority": 0 }
}
},
"delete": {
"min_age": "90d",
"actions": {
"delete": {}
}
}
}
}
}
含义逐段读:
- hot:索引一创建就进入 hot 阶段。单个主分片达到 50GB、或索引满 30 天就 rollover 出新索引,老索引留在 hot 继续服务查询。
set_priority让 hot 索引在恢复时优先分配。 - warm:rollover 后 7 天进入 warm。此时索引已不再写入,forcemerge 成 1 个段,查询更快、段更少。如果你的节点分了
data_warm角色,还需要在索引设置里指定_tier_preference,让分片迁到 warm 层。 - cold:rollover 后 30 天进入 cold。
searchable_snapshot把索引转成可搜索快照,数据搬到快照仓库、本地只留少量元数据,存储成本能降一个数量级,查询仍然可用(只是慢一些)。这要求先配好快照仓库,也就是第 4 篇建的那个。 - delete:rollover 后 90 天删除。注意
min_age是从 rollover 的时间点开始算的(没配 rollover 才从索引创建时间算),不是从「上一阶段完成」算,别把时间线搞混。
把策略挂到索引上
策略本身不会自动生效,索引必须带上 index.lifecycle.name。通常通过索引模板统一设置:
PUT /_index_template/logs-template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 4,
"number_of_replicas": 1,
"index.lifecycle.name": "logs-policy",
"index.lifecycle.rollover_alias": "logs"
}
}
}
rollover_alias 指定 rollover 用的写别名。然后手工引导第一个索引,并把别名设为写索引:
PUT /logs-000001
{
"aliases": {
"logs": { "is_write_index": true }
}
}
之后的写入都打到 logs 别名上,rollover 时 ILM 自动创建 logs-000002 并把写别名切过去。查询同样查 logs 别名,业务代码完全不用感知索引名在变。
看 ILM 执行到哪了
# 策略列表
curl -s "http://localhost:9200/_ilm/policy?pretty"
# 单个索引的生命周期状态:当前阶段、动作、是否出错
curl -s "http://localhost:9200/logs-000001/_ilm/explain?pretty"
_ilm/explain 是最常用的排障入口,重点看三个字段:phase(当前阶段)、action(当前动作)、step(执行到哪一步),以及 error(如果非空,就是卡住的原因)。常见卡点:仓库没配导致 cold 阶段失败、rollover 别名没设置、索引被手工设置了 index.lifecycle.indexing_complete。
监控接入:elasticsearch_exporter + Grafana
_cat 和 _nodes/stats 适合人肉巡检,长期趋势和告警要靠 Prometheus。ES 本身不暴露 Prometheus 格式的指标,中间需要一个 exporter——社区最常用的是 elasticsearch_exporter(prometheus-community 维护)。
启动 exporter,指向 ES 的地址:
docker run -d --name es-exporter \
-p 9114:9114 \
--add-host=host.docker.internal:host-gateway \
prometheuscommunity/elasticsearch-exporter:v1.8.0 \
--es.uri=http://host.docker.internal:9200
验证指标已经出来:
curl -s http://localhost:9114/metrics | grep '^elasticsearch_cluster_health'
然后在 Prometheus 里加一个抓取任务:
scrape_configs:
- job_name: "elasticsearch"
metrics_path: /metrics
static_configs:
- targets: ["10.0.0.20:9114"]
exporter 暴露的指标名大多和 ES 的 stats 接口对应,常用的几个:
| 指标 | 含义 |
|---|---|
elasticsearch_cluster_health_status | 集群状态,按 color 标签区分三色 |
elasticsearch_cluster_health_number_of_nodes | 节点数 |
elasticsearch_jvm_memory_used_bytes | 堆使用量,area="heap" |
elasticsearch_indices_store_size_bytes | 索引占用磁盘 |
elasticsearch_indices_search_query_time_seconds | 累计查询耗时 |
elasticsearch_thread_pool_rejected_count | 线程池拒绝数,按 type 区分读写 |
elasticsearch_process_cpu_percent | 进程 CPU 使用率 |
几条能直接用的告警表达式:
# 集群非 green
elasticsearch_cluster_health_status{color="green"} == 0
# 堆使用率超过 85%
elasticsearch_jvm_memory_used_bytes{area="heap"}
/ elasticsearch_jvm_memory_max_bytes{area="heap"} > 0.85
# 写入线程池开始拒绝请求
rate(elasticsearch_thread_pool_rejected_count{type="write"}[5m]) > 0
# 有分片未分配
elasticsearch_cluster_health_unassigned_shards > 0
指标接进 Prometheus 之后,仪表盘和告警规则交给 Grafana。Grafana 的数据源和面板配置见 Grafana 核心概念与架构,社区也有现成的 Elasticsearch exporter 面板可以直接导入,省掉从零画图的时间。
exporter 挂了,Prometheus 里看到的是「指标突然消失」而不是「集群异常」,很容易误判。给 exporter 加一条 up == 0 的告警,和 ES 本身的告警分开,故障定位会快很多。
日常巡检清单
| 频率 | 检查项 | 命令/位置 |
|---|---|---|
| 每天 | 集群三色、未分配分片 | _cluster/health |
| 每天 | 堆使用率、磁盘剩余 | _cat/nodes、_cat/allocation |
| 每天 | 写入/查询拒绝数 | _cat/thread_pool |
| 每周 | 索引大小增长趋势 | _cat/indices&s=store.size:desc |
| 每周 | 段数量异常的索引 | _cat/segments |
| 每周 | ILM 是否有卡住的索引 | _ilm/explain |
| 每月 | 快照是否正常、做一次恢复演练 | _snapshot |
| 每月 | 版本与安全补丁 | 官方 Release Notes |
小结
- 巡检入口就三组:
_cluster/health看三色、_cat系列看明细、_nodes/stats看资源。_cat/nodes的堆、磁盘、负载三列最值得天天扫。 - 核心指标:堆使用率(85% 红线)、段数量与合并、搜索/索引延迟、线程池队列与拒绝、磁盘水位(85% / 90% / 95% 三级)。
- 95% 洪水位会把索引打成只读,写请求直接失败;调高阈值只是拖延,正解是清数据、扩磁盘、上 ILM。
- ILM 用 hot → warm → cold → delete 四阶段管理时间序列数据:rollover 控制单索引大小、forcemerge 优化只读索引、searchable_snapshot 降存储成本、delete 到期清理。
- 策略要挂到索引上才生效:模板里写
index.lifecycle.name和rollover_alias,再手工引导第一个索引并设is_write_index。 - Prometheus 侧用 elasticsearch_exporter 抓指标,Grafana 出图;告警先配「非 green」「堆超 85%」「写入拒绝」「分片未分配」这四条。
指标和策略都有了,最后一篇处理「已经出问题了怎么办」:集群变红、分片分配失败、内存打满、写入被拒,逐条给排查路径。