2022 年我们做一个电商日志检索系统 ES 集群 3 节点 数据量从 50G 涨到 8TB 索引数从 30 个涨到 1200 个 业务方说"搜不动了"。第一版直接 docker-compose 起三个节点 默认配置 上线 demo 老板说"ES 真香" 半年后整个集群红得发紫。第一种最让我傻眼是默认 5 shard 1 replica 1200 个索引 = 12000 个 shard master 节点 OOM 重启就花 20 分钟;第二种最难缠是 mapping 自动检测 一个字段第一条是数字第二条是字符串 整个索引报错拒绝写入;第三种最离谱是日志按 month 切索引 每个月几百 G 删数据要 delete by query 跑 6 小时 io 打满;第四种最致命是节点磁盘 92% 触发 read-only 整个集群拒绝写入 业务方紧急上 PagerDuty 凌晨叫我起床;第五种最莫名其妙是 query 平均 800ms P99 12 秒一查发现没用 keyword 全文本字段做 term 查询 全 disk scan;第六种最坑是 reindex 跑到一半失败 没有 task checkpoint 重新跑又重头来 折腾两天。真正能扛生产的 ES 不是 docker run elasticsearch 就够,而是一个集群规划 + 索引模板 + ILM 生命周期 + mapping 设计 + 分片策略 + 查询优化 + 监控告警 + 灾备恢复的完整工程体系,任何一环失守都会让你的"全文检索神器"变成"运维噩梦"。本文从踩坑视角梳理 ES 集群运维与索引设计要点,集群拓扑怎么规划 shard 怎么切 mapping 怎么设计 ILM 怎么落地 query 怎么优化 监控怎么搭 灾备怎么做,以及一些把 ES 做扎实要避开的工程坑。
问题背景:为什么 docker run elasticsearch 远远不够
很多团队跑 ES 是 docker run + 默认配置 数据小时无感 数据大了全是炸药:
- 集群拓扑:master / data / coordinating / ingest 角色不分 master 跑业务必崩。
- 分片规划:默认 5 shard 不考虑数据量 shard 过多 OOM 过少 无法扩展。
- Mapping 设计:dynamic mapping 类型冲突 + 全字段索引 = 写入慢 + 存储爆。
- ILM 生命周期:不按时间 rollover 不分层存储 老数据浪费 SSD。
- 查询优化:全文本字段做 term / aggregation 无 cardinality 限制 直接 OOM。
- 监控备份:磁盘告警 / shard 不均衡 / 慢查询日志 / snapshot 一个不能少。
一 集群拓扑与角色分离
ES 节点角色不分是新手最大坑 master 节点跑业务流量 query 一来就 OOM master 选举失败整个集群红色。
# 1 生产推荐拓扑(7.x / 8.x)
# - 3 个 master-eligible 节点(纯 master 不存数据)
# - N 个 data 节点(分 hot/warm/cold 层)
# - 2 个 coordinating only 节点(接前端 query 聚合)
# - 1-2 个 ingest pipeline 节点(数据预处理)
# 2 master 节点配置 elasticsearch.yml
cluster.name: prod-logs-es
node.name: es-master-01
node.roles: [ master ] # 纯 master 不接数据/查询
network.host: 10.0.1.10
http.port: 9200
transport.port: 9300
discovery.seed_hosts:
- es-master-01:9300
- es-master-02:9300
- es-master-03:9300
cluster.initial_master_nodes:
- es-master-01
- es-master-02
- es-master-03
# master 节点 JVM(小内存即可 不参与数据计算)
# /etc/elasticsearch/jvm.options
-Xms4g
-Xmx4g
# 不超过 32GB(避免压缩 OOPS 失效)
# 3 hot data 节点(SSD 接最新数据写入查询)
node.name: es-hot-01
node.roles: [ data_hot, data_content, ingest ]
node.attr.data: hot
path.data: /data/ssd/elasticsearch
# JVM heap 31g(留一半给 OS file cache)
# -Xms31g -Xmx31g
# 4 warm data 节点(SAS/HDD 历史数据 只读为主)
node.name: es-warm-01
node.roles: [ data_warm ]
node.attr.data: warm
path.data: /data/hdd/elasticsearch
# JVM heap 16g
# 5 cold data 节点(冷数据 snapshot 后可清理)
node.name: es-cold-01
node.roles: [ data_cold ]
node.attr.data: cold
# 6 coordinating 节点(纯转发聚合)
node.name: es-coord-01
node.roles: [ ] # 空 roles = coordinating only
# JVM 16g 内存大 用于 aggregation buffer
# 7 关键集群参数
indices.query.bool.max_clause_count: 4096 # bool 子句上限
indices.fielddata.cache.size: 30% # field data 缓存
indices.breaker.fielddata.limit: 40% # 断路器(防 OOM)
indices.breaker.request.limit: 60%
search.max_buckets: 65536 # aggregation bucket 上限
thread_pool.write.queue_size: 1000 # 写入队列(默认 200 太小)
实战经验:master 一定要 3 节点 奇数避免脑裂 跨可用区部署 别全在一个机房;master 节点 JVM 不超过 4-8G 给 OS file cache 即可 master 不需要大内存;data 节点 heap 不超过 31g 超过会失去 compressed oops 内存反而少;hot/warm/cold 分层是省钱关键 SSD 只放 7 天数据 老的迁 HDD;coordinating 节点是查询性能关键 大 aggregation 在这里聚合 别让 data 节点既写又聚合;ingest pipeline 节点用于数据预处理 grok / date parse 别压垮 data 节点;thread_pool.write.queue_size 默认 200 写入高峰必满 改 1000+;集群参数 indices.breaker.* 是断路器 防 query OOM 必须配。
二 索引模板与 Mapping 设计
Mapping 是 ES 性能的灵魂 自动检测 + 全字段索引是新手最大杀手 写入慢 + 存储爆 + 查询不准。
{
"_doc": "1 索引模板(Index Template 7.x+)",
"index_template_example": {
"PUT": "_index_template/logs-app",
"body": {
"index_patterns": ["logs-app-*"],
"priority": 200,
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s",
"index.codec": "best_compression",
"index.translog.durability": "async",
"index.translog.sync_interval": "30s",
"index.merge.scheduler.max_thread_count": 1,
"index.routing.allocation.require.data": "hot",
"index.lifecycle.name": "logs-policy",
"index.lifecycle.rollover_alias": "logs-app"
},
"mappings": {
"dynamic": "strict",
"_source": {
"enabled": true,
"excludes": ["raw_body"]
},
"properties": {
"@timestamp": { "type": "date" },
"level": {
"type": "keyword",
"ignore_above": 32
},
"service": {
"type": "keyword",
"ignore_above": 64
},
"trace_id": {
"type": "keyword",
"ignore_above": 64,
"doc_values": true
},
"message": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"duration_ms": {
"type": "long",
"index": false
},
"user_id": {
"type": "long",
"doc_values": true
},
"tags": {
"type": "keyword"
},
"http": {
"properties": {
"method": { "type": "keyword" },
"status": { "type": "short" },
"path": { "type": "keyword", "ignore_above": 256 }
}
},
"raw_body": {
"type": "text",
"index": false,
"store": false
}
}
}
},
"_meta": {
"owner": "platform-team",
"created": "2024-01-15"
}
}
}
}
模板搞定之后真正要重点关注的是 mapping 的各种细节决策 这些细节决定写入速度 存储大小 查询能不能命中索引:
# 2 mapping 设计原则(踩过的坑)
# a) dynamic: strict 必须用!
# 默认 dynamic: true 第一条 field=123 推断 long 第二条 field="abc" 写入失败
# 用 strict 强制 schema 不在 mapping 的字段直接拒绝
# 或 dynamic: false 接收但不索引
# b) text 必带 keyword multi-field
# text 用于全文检索(分词)
# keyword 用于 term query / aggregation / sort
# 不带 keyword 你会发现 group by 无法用
# c) ignore_above 必须设
# keyword 字段超过 ignore_above 不索引(但仍存 _source)
# 防止超长字段(URL 1KB+)把索引撑爆
# d) doc_values 控制(7.x 默认 true)
# 不需要 sort/agg 的字段(如 raw text)设 doc_values: false 省 30% 存储
# e) index: false(只存不索引)
# 大字段如 raw body / stack trace 不需要查 设 index: false
# 我们日志里 raw_body 5KB+ 设 index: false 节省 50% 索引大小
# f) numeric 字段选最小类型
# user_id 用 long 浪费 status code 用 short(2字节)
# 大流量场景能省 20% 存储
# g) nested 慎用
# 每个 nested 文档实际是独立 lucene doc shard 占用翻倍
# 能用 flatten 用 flatten 能用 object 用 object
# h) date 字段必显式定义
# 自动推断 "2024-01-15 10:00:00" 失败(带空格)
# 显式 "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss"
# i) 关闭 _all 字段(6.x+ 默认关 但要检查)
# _all 把所有字段拼成一个大字段 占空间 7.x 已废弃 别开
# j) 中文必须自定义分词
# ik_max_word(细粒度)/ ik_smart(粗粒度)
# 装 elasticsearch-analysis-ik 插件 否则中文按单字切分 完全不准
# 3 mapping 创建后修改限制
# - 不能修改已有字段类型
# - 只能新增字段
# - 想改类型只能 reindex
# 因此首版 mapping 必须慎重 + review
# 4 字段数量限制
# index.mapping.total_fields.limit: 1000(默认)
# 业务复杂时可调到 2000 但更多说明 schema 设计有问题
# nested object explosion 是常见原因
# 5 调试 mapping
GET logs-app-2024.01.15/_mapping
GET logs-app-2024.01.15/_settings
GET logs-app-2024.01.15/_stats
实战经验:dynamic strict 是救命的 没设过的同学第二天就被 mapping 冲突教育;text 字段必带 keyword multi-field 不然 agg 用不了;raw_body / stack_trace 设 index:false 能省 50% 索引;numeric 选最小类型(short/int 别都用 long)是被严重低估的省钱手段;nested 慎用 性能差 一个 nested doc 实际占 shard 一个槽位;中文必装 IK 否则就是按单字索引 召回率灾难;mapping 改不了类型 上线前评审一次抵后面 reindex 100 次。
三 分片策略与 ILM 生命周期
分片是 ES 横向扩展基础 但 shard 不是越多越好 1200 个索引 * 5 shard = 6000 shard master 内存爆炸。
# 1 分片大小经验值
# 单 shard 推荐 10-50 GB
# 超过 50GB:rebalance/recovery 慢 GC 长
# 小于 10GB:overhead 占比高 shard 元数据浪费
# 单节点 shard 上限:1000 个(包括 replica)
# 集群总 shard:10万级开始性能下降
# 2 按时间 rollover(日志场景)
# 不要按 month 切 一个月数百 G 太大
# 按 day 切 或 size+age 双条件 rollover
# 3 ILM 策略(Index Lifecycle Management)
PUT _ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "30gb",
"max_age": "1d",
"max_docs": 100000000
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "7d",
"actions": {
"allocate": {
"require": { "data": "warm" },
"number_of_replicas": 1
},
"forcemerge": { "max_num_segments": 1 },
"shrink": { "number_of_shards": 1 },
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "30d",
"actions": {
"allocate": {
"require": { "data": "cold" },
"number_of_replicas": 0
},
"freeze": {},
"set_priority": { "priority": 0 }
}
},
"delete": {
"min_age": "180d",
"actions": {
"delete": {}
}
}
}
}
}
# 4 创建索引别名 + 初始索引
PUT logs-app-000001
{
"aliases": {
"logs-app": {
"is_write_index": true
}
}
}
# 5 ILM 执行检查
GET logs-app-000001/_ilm/explain
# 返回当前阶段 / 下一步 / 错误信息
# 6 手动触发 rollover(调试用)
POST logs-app/_rollover
{
"conditions": {
"max_age": "1d"
}
}
# 7 shard 分布检查
GET _cat/shards/logs-app-*?v&s=node
# 看 shard 是否均匀分布在节点上
# 8 强制 shard 迁移(节点维护时)
POST _cluster/reroute
{
"commands": [{
"move": {
"index": "logs-app-000005",
"shard": 0,
"from_node": "es-hot-01",
"to_node": "es-hot-02"
}
}]
}
# 9 shard 不均衡告警
# 用 cluster.routing.allocation.balance.* 调整权重
PUT _cluster/settings
{
"persistent": {
"cluster.routing.allocation.balance.shard": 0.45,
"cluster.routing.allocation.balance.index": 0.55,
"cluster.routing.allocation.balance.threshold": 1.0
}
}
实战经验:单 shard 30GB 是甜点 推 50GB 上限就是恢复时间会受不了;ILM 是日志场景必上的 hot 7 天 warm 30 天 cold 90 天 delete 180 天 是常见配方;forcemerge 在 warm 阶段做 把 segment 合到 1 个查询快 30%;shrink 操作把多 shard 合一 节省 master 内存 但要保证数据已经稳定不再写;cold 阶段可以 freeze 释放 heap 只在查询时短暂加载 适合极少访问的合规归档;rollover 用 max_primary_shard_size 比 max_size 准确 因为 replica 不算;别用 delete by query 删老数据 永远用 ILM delete phase 删整个索引 快 1000 倍。
[mermaid]
flowchart TD
A[应用日志] --> B[Filebeat]
B --> C[Logstash/Ingest pipeline]
C --> D[ES hot 节点 SSD]
D -->|rollover 30GB/1d| E[新 hot 索引]
D -->|7d| F[ES warm 节点 HDD]
F -->|forcemerge + shrink| G[只读优化]
G -->|30d| H[ES cold 节点 + freeze]
H -->|180d| I[Snapshot S3 后删除]
J[Kibana] --> K[coordinating 节点]
K --> D
K --> F
K --> H
四 查询优化与聚合调优
ES 查询慢 90% 是写法问题 10% 是配置问题 不懂查询优化用再贵的硬件也救不了。
{
"_comment_1": "1 慢查询的几种常见根因",
"patterns": {
"anti_pattern_1": {
"wrong": {
"query": {
"match": {
"message": "error"
}
},
"aggs": {
"by_user": {
"terms": { "field": "user_name", "size": 100000 }
}
}
},
"problem": "size=100000 让 ES 把 10w 个 bucket 全返回 内存爆",
"right": {
"query": { "match": { "message": "error" } },
"aggs": {
"by_user": {
"terms": {
"field": "user_name",
"size": 100,
"shard_size": 500,
"min_doc_count": 5
}
}
}
}
},
"anti_pattern_2": {
"wrong": {
"query": { "term": { "message": "error" } }
},
"problem": "message 是 text 字段 term 查询大小写敏感 + 不分词 永远查不到",
"right": {
"query": { "match": { "message": "error" } }
}
},
"anti_pattern_3": {
"wrong": {
"query": {
"wildcard": { "url": "*login*" }
}
},
"problem": "前缀通配符 wildcard 全表扫描 7-8 秒级",
"right": {
"query": {
"match_phrase": { "url": "login" }
},
"or_better": "用 ngram 索引 + match 查询"
}
},
"anti_pattern_4": {
"wrong": {
"size": 10000,
"from": 50000,
"query": { "match_all": {} }
},
"problem": "深分页 from+size 必须每个 shard 排序 50010 条再合并 内存爆",
"right": "用 search_after + sort 游标分页 或 scroll(scroll 7.x 后不推荐 用 PIT)"
}
}
}
反模式认清之后再看正面的优化套路 这些是日常排障必备工具集:
# 2 慢查询日志(必开)
PUT logs-app/_settings
{
"index.search.slowlog.threshold.query.warn": "5s",
"index.search.slowlog.threshold.query.info": "1s",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.indexing.slowlog.threshold.index.warn": "5s",
"index.indexing.slowlog.threshold.index.info": "1s"
}
# 日志路径: /var/log/elasticsearch/prod-logs-es_index_search_slowlog.json
# 3 query profiler(单条 query 调优)
POST logs-app/_search
{
"profile": true,
"query": { "match": { "message": "error" } }
}
# 返回每个 shard 每个阶段(rewrite, weight, score)耗时
# 4 query cache 利用率
GET _nodes/stats/indices/query_cache
# hit_count / miss_count 看是否生效
# bool filter 子句天然走 query cache 比 must 快很多
# 5 filter context 比 query context 快 10x
# 不需要打分用 filter
POST logs-app/_search
{
"query": {
"bool": {
"filter": [
{ "term": { "level": "ERROR" } },
{ "range": { "@timestamp": { "gte": "now-1h" } } }
]
}
}
}
# 6 search_after 深分页(替代 from + size)
POST logs-app/_search
{
"size": 100,
"sort": [{ "@timestamp": "desc" }, { "_id": "asc" }],
"search_after": ["2024-01-15T10:00:00Z", "doc_id_123"]
}
# 7 PIT(Point In Time)+ search_after(替代 scroll)
POST logs-app/_pit?keep_alive=1m
# 返回 pit_id
POST _search
{
"size": 100,
"sort": [{ "@timestamp": "desc" }],
"pit": { "id": "xxx", "keep_alive": "1m" },
"search_after": ["2024-01-15T10:00:00Z"]
}
# 8 aggregation 调优
# - terms agg shard_size = size * 5 提高准确度
# - composite agg 替代 terms 实现分页 agg
# - cardinality 用 HyperLogLog 近似(precision_threshold 控制误差)
# - 大 agg 加 sampler aggregation 先采样再算
{
"aggs": {
"sample": {
"sampler": { "shard_size": 1000 },
"aggs": {
"by_user": { "terms": { "field": "user_id", "size": 10 } }
}
}
}
}
# 9 routing 强制路由(同 user 数据进同 shard)
PUT logs-app/_doc/1?routing=user_123
{ "user_id": "user_123", "message": "..." }
# 查询带 routing 只扫一个 shard 快 N 倍
GET logs-app/_search?routing=user_123
实战经验:filter context 取代 query context 是查询优化第一大招 不要 score 就别 score 快 10 倍;深分页永远用 search_after + PIT 别用 from+size 上千就崩;wildcard 前缀通配是诅咒 改成 ngram 索引或换 match_phrase;terms agg size 不要超 1000 真要全量用 composite agg;cardinality 用 precision_threshold 1000-40000 之间权衡精度与内存;routing 在多租户场景能让查询从 O(N) 降到 O(1) 神器;慢查询日志必须开 不开等于盲飞 我们靠它一周抓出 8 个性能问题。
五 写入优化与索引刷新策略
ES 写入瓶颈通常不在网络而在 refresh / flush / merge 三件套 调优能让吞吐翻 5-10 倍。
# 1 refresh_interval 调大(默认 1s 是查询友好 不是写入友好)
PUT logs-app/_settings
{
"index.refresh_interval": "30s", # 30 秒可见 写入快 3x
"index.translog.durability": "async", # 异步刷盘(允许丢 5s 数据)
"index.translog.sync_interval": "30s",
"index.translog.flush_threshold_size": "1gb"
}
# 大批量导入时临时关闭 refresh + replica
PUT logs-app/_settings
{
"index.refresh_interval": "-1", # 完全关闭
"index.number_of_replicas": 0 # 临时关 replica
}
# 导入完成后恢复
PUT logs-app/_settings
{
"index.refresh_interval": "30s",
"index.number_of_replicas": 1
}
# 2 bulk API(必须用 别用单文档 API)
POST _bulk
{ "index": { "_index": "logs-app", "_id": "1" } }
{ "@timestamp": "2024-01-15T10:00:00", "level": "ERROR", "message": "..." }
{ "index": { "_index": "logs-app", "_id": "2" } }
{ "@timestamp": "2024-01-15T10:00:01", "level": "INFO", "message": "..." }
# bulk 推荐配置
# - 每批 5-15 MB(过大 OOM 过小 overhead 高)
# - 每批 1000-5000 doc
# - 并发 = data 节点数 * 2
# 3 merge 调优(SSD vs HDD)
PUT logs-app/_settings
{
"index.merge.scheduler.max_thread_count": 1, # HDD 设 1(避免随机 IO)
# SSD 可以 4 但默认 auto 一般够用
"index.merge.policy.max_merge_at_once": 10,
"index.merge.policy.segments_per_tier": 10,
"index.merge.policy.max_merged_segment": "5gb"
}
# 4 routing 提升写入分布
# 默认 hash(_id) % shard 路由
# 高基数 routing(如 user_id)分布更均匀
POST logs-app/_doc?routing=service-xyz
{ "service": "service-xyz", "msg": "..." }
# 5 写入背压检测
GET _cat/thread_pool/write?v&h=node_name,active,queue,rejected
# rejected > 0 说明已经在拒绝写入 必须扩容
# 6 索引 codec 压缩
PUT logs-app/_settings
{
"index.codec": "best_compression" # LZ4 -> DEFLATE 节省 25% 存储 但写入慢 10%
}
# 7 segment 状态检查
GET logs-app/_segments
# count: segment 数(太多查询慢)
# size: 单 segment 大小
# 8 forcemerge(只读索引才能做!)
POST logs-app-2024.01.01/_forcemerge?max_num_segments=1
# - 把 N 个 segment 合成 1 个 查询快 30%
# - 但 IO 极高 必须在低峰
# - 正在写的索引不能做 否则 segment 巨大无法 merge
实战经验:refresh_interval 30s 是日志场景标配 1s 默认太激进 写入吞吐砍半;批量导入 + 临时关 replica 是 reindex / 历史数据迁移神技 写入快 5-10x;bulk 批 5-15MB 是甜点 太大 ES 内部要拆分反而慢;HDD 必须 merge thread = 1 否则随机 IO 把磁盘打死;routing 用 high cardinality 字段 别用 high collision 的(如 country_code 只有几十值);write thread pool rejected 是写入失败的最直接信号 监控告警必上;forcemerge 只对只读索引做 ILM warm phase 是最佳时机;best_compression 在 cold 阶段开 hot 阶段别开 写入太慢。
六 监控告警、备份与故障演练
ES 集群没监控就是开盲盒 没备份就是开赌场 我们经历过节点磁盘 100% 凌晨 PagerDuty 灾难后才上完整体系。
# 1 关键监控指标(Prometheus + elasticsearch_exporter)
# 集群级
# elasticsearch_cluster_health_status green=0 yellow=1 red=2
# elasticsearch_cluster_health_active_shards
# elasticsearch_cluster_health_unassigned_shards > 0 必须告警
# elasticsearch_cluster_health_relocating_shards
# elasticsearch_cluster_health_number_of_pending_tasks > 100 危险
# 节点级
# elasticsearch_filesystem_data_used_percent > 80% 告警 > 85% read-only
# elasticsearch_jvm_memory_used_bytes / max_bytes > 75% 持续 = GC 频繁
# elasticsearch_jvm_gc_collection_seconds_count GC 次数
# elasticsearch_indices_docs doc 总数
# elasticsearch_indices_store_size_bytes 索引大小
# elasticsearch_thread_pool_rejected_count rejected > 0 必告警
# elasticsearch_indices_search_query_time_seconds 查询延迟
# elasticsearch_indices_indexing_index_time_seconds 写入延迟
# elasticsearch_breakers_tripped 断路器触发次数 > 0 危险
# 2 关键告警规则
groups:
- name: elasticsearch-critical
rules:
- alert: ESClusterRed
expr: elasticsearch_cluster_health_status == 2
for: 1m
annotations:
summary: "ES 集群 RED 状态 数据不可用"
- alert: ESUnassignedShards
expr: elasticsearch_cluster_health_unassigned_shards > 0
for: 5m
annotations:
summary: "未分配 shard {{ $value }} 个 检查节点状态"
- alert: ESDiskFull
expr: elasticsearch_filesystem_data_used_percent > 80
for: 5m
annotations:
summary: "ES 节点 {{ $labels.node }} 磁盘 {{ $value }}% 临近 85% read-only"
- alert: ESHeapHigh
expr: elasticsearch_jvm_memory_used_bytes{area="heap"} / elasticsearch_jvm_memory_max_bytes{area="heap"} > 0.85
for: 10m
annotations:
summary: "ES 节点 heap > 85% GC 风险"
- alert: ESWriteRejected
expr: increase(elasticsearch_thread_pool_rejected_count{type="write"}[5m]) > 100
annotations:
summary: "写入被拒绝 容量不足"
# 3 snapshot 备份(必须 不要赌)
# 注册 S3 repository
PUT _snapshot/s3_backup
{
"type": "s3",
"settings": {
"bucket": "my-es-backup",
"region": "cn-north-1",
"base_path": "snapshots/prod",
"compress": true,
"chunk_size": "100mb"
}
}
# 配置 SLM 自动 snapshot
PUT _slm/policy/daily-snapshot
{
"schedule": "0 30 1 * * ?",
"name": "<daily-snap-{now/d}>",
"repository": "s3_backup",
"config": {
"indices": ["logs-*", "metrics-*"],
"include_global_state": true,
"partial": false
},
"retention": {
"expire_after": "30d",
"min_count": 7,
"max_count": 90
}
}
# 触发手动 snapshot
POST _slm/policy/daily-snapshot/_execute
# 4 恢复演练(每季度做!)
# 在测试集群恢复某天 snapshot 看能否完整查询
POST _snapshot/s3_backup/snapshot_2024.01.10/_restore
{
"indices": "logs-app-2024.01.10",
"rename_pattern": "logs-app-(.+)",
"rename_replacement": "restored-logs-app-$1"
}
# 5 故障演练(混沌工程)
# a) 杀掉 1 个 data 节点 看 shard 是否自动恢复到其他节点
kubectl delete pod es-hot-02
# 验证: yellow -> green 时间 < 10 min
# b) 模拟磁盘满 触发 read-only
fallocate -l 100G /data/ssd/elasticsearch/filler
# 验证: alert 触发 + 自动迁移到其他节点
# c) 模拟 master 选举
# 杀掉当前 master(用 _cat/master 查)
# 验证: 新 master 选举 < 30s + 集群仍可写
# d) 模拟跨机房网络分区
iptables -A INPUT -s 10.0.2.0/24 -j DROP
# 验证: 不脑裂(因为 quorum=2 单机房 1 个 master 拿不到多数票)
# 6 滚动重启(升级/配置变更)
# a) 关闭 shard 分配
PUT _cluster/settings
{ "persistent": { "cluster.routing.allocation.enable": "primaries" } }
# b) 停止节点 升级 启动
systemctl restart elasticsearch
# c) 恢复 shard 分配
PUT _cluster/settings
{ "persistent": { "cluster.routing.allocation.enable": "all" } }
# d) 等 green 再处理下一个节点
GET _cluster/health?wait_for_status=green&timeout=10m
实战经验:磁盘 85% 触发 read-only 是 ES 死亡线 必须在 80% 就告警留 5% 缓冲;unassigned shard 持续 5 分钟必须人工介入 否则一定是配置或硬件问题;snapshot 一定要存对象存储 S3/OSS 别存本地 节点挂了你哭都没用;SLM 自动 snapshot + retention 是免运维标配;恢复演练每季度做一次 不演练等于没有备份 我们演练第一次就发现 mapping 不兼容;滚动重启必须关 shard allocation 否则数据来回搬运几小时;集群 RED 时第一件事看 _cluster/allocation/explain 它会告诉你为什么 shard 起不来;断路器触发(breakers_tripped)是查询 OOM 的最早信号 必须告警。
关键概念速查
| 问题 | 关键配置 | 推荐值 | 备注 |
|---|---|---|---|
| master 节点 | node.roles | [master] 纯角色 | 3 节点跨 AZ |
| JVM heap | -Xms / -Xmx | ≤ 31g | 过 32 失去 compressed oops |
| 单 shard 大小 | rollover max_primary_shard_size | 30 GB | 10-50 GB 区间 |
| refresh 间隔 | index.refresh_interval | 30s | 1s 写入吞吐砍半 |
| mapping 动态 | dynamic | strict | 避免类型冲突 |
| text 字段 | multi-field keyword | 必带 | agg/sort 用 keyword |
| 深分页 | search_after + PIT | 替代 from+size | scroll 7.x 已退役 |
| 磁盘告警 | used_percent | 80% | 85% 触发 read-only |
| snapshot | SLM 策略 | 每天 + 保留 30d | S3/OSS 远端 |
| 断路器 | indices.breaker.* | 40%/60% | 触发立即告警 |
避坑清单
- master / data / coordinating 角色必须分离 不分离 master 跑业务必崩。
- JVM heap 不超过 31g 超过失去 compressed oops 内存反而少 留一半给 OS file cache。
- shard 数不要爆 单节点不超 1000 单集群 10w 级就性能下降。
- dynamic mapping 必须设 strict 不然类型冲突拒绝写入。
- text 字段必带 keyword multi-field 否则 agg/sort 用不了。
- raw_body / stack_trace 设 index:false store:false 省 50% 索引大小。
- 不要用 delete_by_query 删老数据 用 ILM delete phase 删整个索引快 1000 倍。
- 查询不要 score 用 filter context 快 10 倍 90% 业务查询都该 filter。
- 深分页永远用 search_after + PIT 不要 from + size。
- SLM 自动 snapshot 必上 每季度做一次恢复演练 不演练等于没有备份。
总结
Elasticsearch 集群运维不是 docker run elasticsearch 就够 而是一个 集群拓扑 + 索引模板 + ILM 生命周期 + Mapping 设计 + 分片策略 + 查询优化 + 写入调优 + 监控告警 + 灾备恢复 的完整工程体系。每个环节都对应着真实生产事故:master 跑业务 OOM 集群红色、shard 过多 master 重启 20 分钟、mapping 自动检测类型冲突拒写、磁盘 92% 触发 read-only 业务断、深分页 OOM Coord 节点崩、慢查询拖垮整集群、snapshot 没做节点挂数据丢、滚动升级 shard 来回搬迁几小时。
本文给出的 6 个维度只是骨架 真实落地还要结合业务读写比 数据保留周期 查询模式 SLA 做精细化容量规划。比如日志场景与电商搜索场景索引设计完全不同 时序数据要 ILM + cold tier 搜索数据要 keyword + completion suggester 多语言要分词器选型。建议每月评审一次集群健康度 慢查询 TOP 10 索引模板与 mapping 是否还合理 让 ES 从"能搜"进化到"快 准 稳"。
打个比方 ES 集群就像一座大型物流仓储中心:master 节点是仓库调度中心(不搬货只指挥)data 节点是货架(hot 是常用品分拣区 SSD warm 是季节品 HDD cold 是积压品归档)shard 是货架格子(太大搬不动 太小浪费空间)mapping 是商品分类规则(乱分类找货能找疯)ILM 是货物轮换制度(过期下架打折清仓)query 是仓管员路线规划(用条码扫 vs 一格一格找差 100 倍)refresh interval 是上架频率(刚到就上架 vs 30 分钟批量上架 工作量天差地别)snapshot 是异地备份(本地仓库烧了还能从异地恢复)监控告警是消防系统 + 库存预警(磁盘满 / 断路器跳闸 / shard 失踪一个不能漏)。任何一个环节疏忽都会让仓库瘫痪 客户拿不到货评分暴跌。把这套体系打磨扎实 你的 ES 集群才能从"能搜的小破工具"变成"稳如老狗的检索基础设施"。
愿你的 ES 集群永远绿色 shard 分布均匀 mapping 严格 ILM 跑得稳 慢查询日志干净 snapshot 按时落地 老板再也不会问"日志怎么搜不到"。
—— 别看了 · 2026