Elasticsearch 集群运维与索引设计完全指南:从一次"1200 个索引 12000 个 shard master OOM 重启 20 分钟全站搜不出"看懂为什么 docker run elasticsearch 远远不够

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 设计加分片策略加查询优化加监控告警加灾备恢复的完整工程体系

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% 触发立即告警

避坑清单

  1. master / data / coordinating 角色必须分离 不分离 master 跑业务必崩。
  2. JVM heap 不超过 31g 超过失去 compressed oops 内存反而少 留一半给 OS file cache。
  3. shard 数不要爆 单节点不超 1000 单集群 10w 级就性能下降。
  4. dynamic mapping 必须设 strict 不然类型冲突拒绝写入。
  5. text 字段必带 keyword multi-field 否则 agg/sort 用不了。
  6. raw_body / stack_trace 设 index:false store:false 省 50% 索引大小。
  7. 不要用 delete_by_query 删老数据 用 ILM delete phase 删整个索引快 1000 倍。
  8. 查询不要 score 用 filter context 快 10 倍 90% 业务查询都该 filter。
  9. 深分页永远用 search_after + PIT 不要 from + size。
  10. 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

LLM 推理服务部署与 GPU 调度完全指南:从一次"vLLM 单卡 A100 跑 Qwen2-72B 5000 用户同时上线 KV Cache 爆显存全站 OOM"看懂为什么 pip install vllm 远远不够

2026-5-25 11:43:29

技术教程

asyncio event loop 被同步代码卡死的真实事故:P99 从 80ms 飙到 12s 的 6 小时复盘

2026-5-25 12:22:38

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索