Elasticsearch 8.x 生产开发规范

0 阅读21分钟

适用版本:Elasticsearch 8.6+(关键差异处标注 7.x 兼容说明) 适用范围:业务搜索、日志/可观测性、向量检索 (kNN/RAG) 三类场景 文档定位:架构师、后端开发、SRE 共同遵循的落地规范,违反规范的设计应在 Code Review 阶段被驳回


目录

  1. 总体原则与红线
  2. 集群与容量规划
  3. 索引设计规范
  4. Mapping 规范
  5. 写入规范
  6. 查询规范
  7. 向量检索 / RAG 场景
  8. 日志 / 可观测性场景
  9. 客户端接入规范
  10. 安全规范
  11. 发布与变更流程
  12. 可观测性与告警
  13. 故障应急 SOP
  14. 附录 A:禁用 / 慎用清单
  15. 附录 B:Code Review Checklist

1. 总体原则与红线

1.1 设计哲学

ES 不是关系型数据库,不是消息队列,也不是主存储。它是面向读优化的分布式倒排索引。所有设计决策都应回到三个问题:

  1. 读多还是写多? 决定分片数与刷新策略。
  2. 数据是否随时间增长? 决定是否使用 Data Stream / ILM。
  3. 数据是否需要强一致? 如果是,ES 不是合适的选型,应在上游用 MySQL/PG 兜底。

1.2 七条红线(违反需架构组评审)

编号红线原因
R1禁止将 ES 作为唯一数据源ES 无事务,refresh 默认 1s,不保证立即可见
R2禁止使用 _update_by_query / _delete_by_query 处理 > 100w 文档而不限速会拖垮集群,必须设置 requests_per_second
R3禁止使用 from + size 翻页超过 10000 条内存爆炸,必须用 search_after 或 PIT
R4禁止在生产索引上动态修改 mapping 字段类型字段类型不可变,必须 reindex
R5禁止单分片体积 > 50GB(日志类)/ > 30GB(搜索类)恢复慢、查询慢、节点风险高
R6禁止生产环境关闭副本(number_of_replicas: 0节点宕机即丢数据
R7禁止使用 root / elastic 超管账号接入业务应用必须为每个服务申请最小权限角色

1.3 默认值(除非有明确理由,否则必须遵循)

项目默认值说明
主分片数单索引 1~3,按数据量调整一旦创建不可改
副本数1(生产)/ 0(开发)生产至少 1
refresh_interval搜索类 1s,日志类 30s写多读少调大
index.number_of_routing_shards30便于将来 split
translog.durabilityrequest(搜索类)/ async(日志类)异步可丢 5s 数据
字段类型显式 mapping,禁用 dynamic mapping 推断关键字段避免误推断成 text

2. 集群与容量规划

2.1 节点角色拆分

生产集群必须按角色拆分节点,禁止混部

master 节点 × 3   (专用,2C4G 即可,禁止承担数据)
data_hot × N      (SSD,承担当日写入与近期查询)
data_warm × N     (SATA SSD,承担 7~30 天数据)
data_cold × N     (HDD 或对象存储,30 天以上)
coordinating × 2  (可选,大查询场景前置聚合)
ingest × 2        (可选,pipeline 预处理)

配置示例elasticsearch.yml):

node.name: hot-01
node.roles: [ data_hot, data_content, ingest ]
node.attr.box_type: hot

2.2 容量公式

存储

预估存储 = 原始数据量 × (1 + 副本数) × 1.45
  • 1.45 ≈ 索引膨胀系数(含 _source + 倒排 + DocValues + 副本)
  • 如果开启 best_compression,膨胀系数降到约 1.15,但查询 CPU 上升 15%
  • 磁盘水位必须低于 70%,超过 85% 触发 flood_stage,索引变只读

堆内存

JVM Heap = min(物理内存 / 2, 31GB)
  • 超过 31GB 会失去指针压缩(Compressed OOPs),反而变慢
  • 剩余物理内存留给 Lucene FileCache(这是 ES 性能的关键)
  • 禁止开启 swapbootstrap.memory_lock: true

分片数上限

单节点分片数  Heap GB × 20
集群总分片数  节点数 × 1000(硬上限)

例:32GB heap → 单节点最多 ~600 个分片。超过会显著拖慢 cluster state 同步。

2.3 分片大小建议

场景单分片大小单分片文档数
业务搜索10~30 GB< 2 亿
日志30~50 GB< 5 亿
向量检索5~15 GB取决于 dim

经验法则:分片不是越多越好。每个分片都是一个独立的 Lucene 实例,有固定开销(约 50~100MB heap)。


3. 索引设计规范

3.1 命名规范

{业务域}-{数据类型}-{版本}[-{时间后缀}]

示例:
  search-product-v3                    # 业务搜索,固定索引
  logs-nginx-access-v1-2026.05         # 日志,按月切分
  vector-doc-embedding-v2              # 向量索引
  metrics-app-2026.05.11               # 指标,按天切分(用 Data Stream)

规则:

  • 全部小写,禁止下划线开头
  • 版本号 v{n} 用于 reindex 平滑切换,配合别名使用
  • 时间后缀必须是 YYYY.MMYYYY.MM.DD,便于按 pattern 删除

3.2 别名(Alias)必须使用

所有业务读写都必须通过别名,禁止直连物理索引

POST /_aliases
{
  "actions": [
    { "add":    { "index": "search-product-v3", "alias": "search-product",       "is_write_index": true } },
    { "remove": { "index": "search-product-v2", "alias": "search-product" } }
  ]
}

理由:

  • Reindex 时可零停机切换
  • 查询别名可跨索引(如同时查 v2 + v3 灰度对比)
  • 写入别名可指定 is_write_index 实现 rollover

3.3 时间序列数据:使用 Data Stream

ES 7.9+ 推荐用 Data Stream + ILM 替代手工 rollover。

// 1. 创建 ILM 策略
PUT _ilm/policy/logs-policy
{
  "policy": {
    "phases": {
      "hot":    { "actions": { "rollover": { "max_age": "1d", "max_primary_shard_size": "30gb" } } },
      "warm":   { "min_age": "7d",  "actions": { "shrink": { "number_of_shards": 1 }, "forcemerge": { "max_num_segments": 1 } } },
      "cold":   { "min_age": "30d", "actions": { "freeze": {} } },
      "delete": { "min_age": "90d", "actions": { "delete": {} } }
    }
  }
}

// 2. 创建 Index Template(必须包含 data_stream 字段)
PUT _index_template/logs-nginx-template
{
  "index_patterns": ["logs-nginx-*"],
  "data_stream": {},
  "template": {
    "settings": {
      "index.lifecycle.name": "logs-policy",
      "index.lifecycle.rollover_alias": "logs-nginx",
      "number_of_shards": 3,
      "number_of_replicas": 1,
      "refresh_interval": "30s"
    },
    "mappings": { ... }
  }
}

// 3. 创建 Data Stream
PUT _data_stream/logs-nginx

3.4 分片数选择决策树

单索引预计总数据量?
├── < 30GB        → 1 主分片
├── 30~150GB      → 3 主分片
├── 150~500GB     → 5 主分片
├── 500GB~1TB     → 7~10 主分片
└── > 1TB         → 必须按时间切分,使用 Data Stream

禁止

  • 设置奇怪的分片数(如 11、13),影响均衡
  • 单索引超过节点数 × 3 的分片
  • 副本数 > 2(除非有明确读扩展需求)

4. Mapping 规范

4.1 必须显式定义 Mapping

禁止依赖 dynamic mapping。新索引创建必须提交完整 mapping 到 Code Review。

PUT _index_template/search-product-template
{
  "index_patterns": ["search-product-v*"],
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1,
      "refresh_interval": "1s",
      "analysis": {
        "analyzer": {
          "ik_smart_pinyin": {
            "type": "custom",
            "tokenizer": "ik_smart",
            "filter": ["lowercase", "pinyin_filter"]
          }
        },
        "filter": {
          "pinyin_filter": {
            "type": "pinyin",
            "keep_first_letter": true,
            "keep_full_pinyin": true,
            "keep_original": true
          }
        }
      }
    },
    "mappings": {
      "dynamic": "strict",
      "properties": {
        "product_id":   { "type": "keyword" },
        "title":        {
          "type": "text",
          "analyzer": "ik_smart_pinyin",
          "search_analyzer": "ik_smart",
          "fields": {
            "keyword": { "type": "keyword", "ignore_above": 256 },
            "suggest": { "type": "completion" }
          }
        },
        "price":        { "type": "scaled_float", "scaling_factor": 100 },
        "stock":        { "type": "integer" },
        "category_path":{ "type": "keyword" },
        "tags":         { "type": "keyword" },
        "created_at":   { "type": "date", "format": "strict_date_optional_time||epoch_millis" },
        "updated_at":   { "type": "date" },
        "embedding":    {
          "type": "dense_vector",
          "dims": 768,
          "index": true,
          "similarity": "cosine"
        },
        "description":  {
          "type": "text",
          "analyzer": "ik_smart",
          "index_options": "offsets"
        },
        "_meta":        { "type": "object", "enabled": false }
      }
    }
  }
}

4.2 字段类型选型表

业务含义必选类型禁选类型原因
ID、订单号、SKUkeywordtext / long不分词;long 排序占用更多空间
状态枚举keywordtext / byte节省空间且支持 terms 聚合
全文搜索字段text + keyword 多字段text既能搜又能聚合排序
金额scaled_float(scaling_factor=100)double / float精度问题,禁用 float
时间戳datekeyword / long利于时间范围查询
IP 地址ipkeyword支持 CIDR 查询
地理位置geo_point / geo_shape两个 float利用 BKD 索引
高维向量dense_vector多个 float 字段走 HNSW 索引
大段不搜文本text 关闭 index默认 text节省倒排空间
JSON 元数据flattenedenabled:false普通 object避免字段爆炸

4.3 关键 Mapping 参数

{
  "properties": {
    "field_a": {
      "type": "keyword",
      "doc_values": true,         // 默认 true,关闭后无法排序/聚合
      "index": true,              // 默认 true,关闭后无法过滤
      "ignore_above": 256,        // keyword 超长截断,必填
      "null_value": "NA"          // 显式 null 值,便于过滤
    },
    "field_b": {
      "type": "text",
      "norms": false,             // 不需要相关性算分时关闭,省空间
      "index_options": "freqs"    // docs/freqs/positions/offsets,按需选择
    }
  }
}

4.4 多字段(Multi-Fields)模式

业务搜索字段几乎都需要多字段:

"title": {
  "type": "text",
  "analyzer": "ik_max_word",
  "fields": {
    "keyword":  { "type": "keyword", "ignore_above": 256 },     // 精确匹配/聚合
    "ngram":    { "type": "text", "analyzer": "ngram_analyzer" }, // 前缀搜索
    "pinyin":   { "type": "text", "analyzer": "pinyin_analyzer" } // 拼音搜索
  }
}

4.5 Mapping 变更策略

字段类型不可变更。允许的变更:

变更类型是否允许操作方式
新增字段PUT /index/_mapping
修改 ignore_above直接更新,仅对新数据生效
修改字段类型必须 reindex 到新版本
删除字段❌(标记废弃)应用层不再读写,下次 reindex 移除
修改 analyzer⚠️search_analyzer 可改;index_analyzer 必须 reindex

5. 写入规范

5.1 必须使用 Bulk API

禁止单条 index / update 写入业务数据

POST _bulk
{ "index": { "_index": "search-product", "_id": "p001" } }
{ "title": "...", "price": 99.0 }
{ "index": { "_index": "search-product", "_id": "p002" } }
{ "title": "...", "price": 88.0 }

Bulk 调优参数

参数推荐值说明
单批次大小5~15 MB不是文档数,是字节数
单批次文档数500~5000视文档大小
并发线程数节点数 × 2过高触发 bulk_rejected
客户端超时60sbulk 内部异步,无需短超时
失败重试仅对 429 和 5xx 重试4xx(除 429)重试无意义

5.2 写入路径设计

业务方 → MQ (Kafka) → Consumer → ES Bulk
        ↑
    解耦、削峰、重试可控

禁止业务接口同步写 ES。原因:

  • ES 抖动会拖垮业务
  • 失败无法补偿
  • 高峰期触发限流

5.3 文档 ID 策略

策略适用场景优劣
业务主键(如 product_id)需要 update/upsert 的业务数据可幂等,需注意分布均匀
ES 自动生成日志类只追加不更新写入更快(跳过版本检查)
雪花 ID时序数据需有序利于按 ID 范围查询

禁止用 UUID 作为文档 ID 用于 routing key 之外的场景——分布虽然均匀,但失去业务可追溯性。

5.4 Routing 策略

默认按 _id hash 路由。多租户场景必须自定义 routing

POST /search-product/_doc/p001?routing=tenant_001
{
  "tenant_id": "tenant_001",
  "title": "..."
}
// 查询时也必须带 routing,否则会跨所有分片
GET /search-product/_search?routing=tenant_001

注意事项:

  • routing 字段必须在 mapping 中声明 _routing.required: true
  • routing 值分布要均匀,否则出现分片热点
  • 一旦使用 routing,不能轻易迁移租户数据

5.5 Refresh 与可见性

write → translog → in-memory buffer → (refresh) → segment → (flush) → disk
                                       ↑
                              默认 1s,决定可见时延
  • 不要在 bulk 时设置 refresh=true,会强制刷新,性能下降 10x+
  • 可以在测试或低频管理操作中使用 refresh=wait_for
  • 大批量初始导入时,临时把 refresh_interval: -1,导入完恢复
// 大量初始导入
PUT /search-product/_settings
{ "index.refresh_interval": "-1", "index.number_of_replicas": 0 }

// 导入完成后
PUT /search-product/_settings
{ "index.refresh_interval": "1s", "index.number_of_replicas": 1 }
POST /search-product/_forcemerge?max_num_segments=1

5.6 并发更新与版本控制

ES 使用乐观锁。更新场景必须带版本号:

POST /search-product/_update/p001?if_seq_no=362&if_primary_term=2
{
  "doc": { "stock": 99 }
}

返回 409 Conflict 时,业务方应:

  1. 重读最新文档
  2. 应用本次变更
  3. 重试更新(最多 3 次)

禁止retry_on_conflict 处理强业务语义的更新(会丢失中间状态)。


6. 查询规范

6.1 查询基本原则

  1. 永远显式指定 _source:减少网络传输
  2. 永远设置 track_total_hits: false 或具体上限:精确计数代价高
  3. filter 优先于 query:filter 可缓存,query 算分
  4. 聚合必须设置 size 上限:避免内存爆炸
  5. 禁止 match_all + 大 size:必须分页

6.2 标准查询模板

GET /search-product/_search
{
  "_source": ["product_id", "title", "price"],
  "track_total_hits": 1000,
  "from": 0,
  "size": 20,
  "query": {
    "bool": {
      "must": [
        { "match": { "title": { "query": "无线耳机", "operator": "and" } } }
      ],
      "filter": [
        { "term":  { "category_path": "3C/audio" } },
        { "range": { "price": { "gte": 100, "lte": 1000 } } },
        { "term":  { "status": "on_sale" } }
      ],
      "should": [
        { "term": { "tags": { "value": "hot", "boost": 2.0 } } }
      ]
    }
  },
  "sort": [
    "_score",
    { "created_at": { "order": "desc" } }
  ]
}

6.3 分页方案选型

场景方案上限
用户分页(< 100 页)from + sizefrom + size ≤ 10000
深分页(瀑布流)search_after无上限,必须有唯一排序字段
全量导出(一致性)Point-in-Time (PIT) + search_after无上限,PIT 默认 1m
实时滚动(旧版本)scroll新代码禁用,用 PIT 替代

search_after 示例

// 第一页
GET /search-product/_search
{
  "size": 20,
  "query": { "match_all": {} },
  "sort": [
    { "created_at": "desc" },
    { "_id": "desc" }
  ]
}

// 后续页:用上一页最后一条的 sort 值
GET /search-product/_search
{
  "size": 20,
  "query": { "match_all": {} },
  "search_after": [1715472000000, "p999"],
  "sort": [
    { "created_at": "desc" },
    { "_id": "desc" }
  ]
}

PIT 示例(强一致性遍历)

POST /search-product/_pit?keep_alive=1m
// 返回 { "id": "xxx" }

GET /_search
{
  "pit": { "id": "xxx", "keep_alive": "1m" },
  "size": 1000,
  "sort": [{ "_shard_doc": "asc" }],
  "search_after": [...]
}

6.4 聚合规范

{
  "size": 0,                          // 不要 hits
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category_path",
        "size": 50,                   // 必须设上限
        "shard_size": 100,            // 默认 size×1.5+10,精度不足时调大
        "min_doc_count": 1
      },
      "aggs": {
        "avg_price": { "avg": { "field": "price" } }
      }
    }
  }
}

禁止

  • terms 不设 size
  • cardinality 在 high-cardinality 字段上不设 precision_threshold
  • 多层 nested terms 聚合(>3 层),必要时改用 composite 聚合
  • text 字段上聚合(默认无 doc_values,会报错或开 fielddata 爆内存)

6.5 慢查询治理

任何查询超过 200ms(业务搜索)或 1s(分析查询)必须:

  1. _search?profile=true 分析
  2. 检查是否有 wildcard *xxx*、正则、script_score
  3. 检查 filter 是否被缓存(indices.requests.cache
  4. 检查是否触发 fielddata(应改为 doc_values)

慢查询日志配置

PUT /search-product/_settings
{
  "index.search.slowlog.threshold.query.warn": "500ms",
  "index.search.slowlog.threshold.query.info": "200ms",
  "index.search.slowlog.threshold.fetch.warn": "200ms",
  "index.indexing.slowlog.threshold.index.warn": "1s"
}

6.6 算分与排序

业务搜索的相关性建议结构:

final_score = BM25(text_match) × function_score(business_signals)
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query": "无线耳机",
          "fields": ["title^3", "description", "tags^2"],
          "type": "best_fields",
          "tie_breaker": 0.3
        }
      },
      "functions": [
        { "filter": { "term": { "is_promoted": true } }, "weight": 1.5 },
        { "field_value_factor": { "field": "sales_7d", "modifier": "log1p", "missing": 0 } },
        { "gauss": { "created_at": { "origin": "now", "scale": "30d", "decay": 0.5 } } }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}

禁止 script_score 写复杂业务逻辑——脚本无法缓存,每次执行都解析。


7. 向量检索 / RAG 场景

7.1 dense_vector 字段定义

{
  "embedding": {
    "type": "dense_vector",
    "dims": 768,                  // 必须等于模型输出维度
    "index": true,                // 必须开启才能走 HNSW
    "similarity": "cosine",       // cosine / dot_product / l2_norm
    "index_options": {
      "type": "hnsw",
      "m": 16,                    // 每节点连接数,默认 16
      "ef_construction": 100      // 构建时搜索深度
    }
  }
}

关键约束

  • dot_product 要求向量已归一化,否则结果错误
  • dims 上限 4096(ES 8.11+),建议 ≤ 1536
  • 单分片向量数 > 100w 时,构建时间显著上升

7.2 kNN 查询

GET /vector-doc/_search
{
  "knn": {
    "field": "embedding",
    "query_vector": [0.12, -0.34, ...],
    "k": 20,                      // 返回数量
    "num_candidates": 200,        // 每分片候选数,越大越准但越慢
    "filter": [
      { "term": { "tenant_id": "t001" } },
      { "range": { "created_at": { "gte": "now-30d" } } }
    ]
  },
  "_source": ["doc_id", "title", "snippet"]
}

num_candidates 调优:

  • 默认 = max(k, 100)
  • 召回率不够 → 调大(500、1000)
  • 延迟过高 → 调小,但不要小于 k × 5

7.3 混合检索(Hybrid Search)

RAG 场景通常需要 BM25 + 向量融合:

GET /vector-doc/_search
{
  "size": 10,
  "query": {
    "bool": {
      "should": [
        { "match": { "content": { "query": "如何配置 ES 副本数", "boost": 0.3 } } }
      ],
      "filter": [{ "term": { "lang": "zh" } }]
    }
  },
  "knn": {
    "field": "embedding",
    "query_vector": [...],
    "k": 50,
    "num_candidates": 200,
    "boost": 0.7
  },
  "rank": {
    "rrf": { "rank_window_size": 100, "rank_constant": 60 }
  }
}

ES 8.8+ 支持 RRF (Reciprocal Rank Fusion) 原生融合,强烈推荐。

7.4 向量场景容量

维度单向量字节100w 向量大小(含 HNSW)
3841.5 KB~3 GB
7683 KB~6 GB
15366 KB~12 GB

向量索引全部加载到 off-heap 内存,物理内存预算必须覆盖。

7.5 向量量化(ES 8.12+)

大规模场景启用 int8_hnsw 量化,内存占用降至 1/4:

{
  "embedding": {
    "type": "dense_vector",
    "dims": 768,
    "index": true,
    "similarity": "cosine",
    "index_options": {
      "type": "int8_hnsw",
      "m": 16,
      "ef_construction": 100
    }
  }
}

精度损失通常 < 1% recall@10。


8. 日志 / 可观测性场景

8.1 推荐架构

App → Filebeat / Vector → Kafka → Logstash / Ingest Pipeline → ES (Data Stream)
                                                                    ↓
                                                               Kibana / Grafana

8.2 标准日志字段(ECS)

强制采用 Elastic Common Schema (ECS),避免每个团队自定义字段名:

{
  "@timestamp": "2026-05-11T10:23:45.123Z",
  "log.level": "ERROR",
  "service.name": "order-service",
  "service.version": "1.2.3",
  "host.name": "node-01",
  "trace.id": "abc123",
  "span.id": "def456",
  "message": "payment failed",
  "error.type": "TimeoutException",
  "error.stack_trace": "...",
  "labels": { "tenant_id": "t001", "region": "cn-east-1" }
}

8.3 ILM 策略示例

按数据价值分层:

阶段时长节点副本操作
Hot1 天SSD1写入 + 高频查询
Warm7 天SATA SSD1shrink 到 1 分片,forcemerge
Cold30 天HDD0freeze,可搜索快照
Frozen90 天对象存储0可搜索快照
Delete90+ 天--删除

8.4 日志索引特殊优化

{
  "settings": {
    "index.refresh_interval": "30s",
    "index.translog.durability": "async",
    "index.translog.sync_interval": "5s",
    "index.codec": "best_compression",
    "index.sort.field": ["@timestamp"],
    "index.sort.order": ["desc"]
  },
  "mappings": {
    "properties": {
      "@timestamp": { "type": "date" },
      "message":    { "type": "match_only_text" },   // 8.x 新类型,省 90% 空间
      "host.name":  { "type": "keyword" },
      "labels":     { "type": "flattened" }
    }
  }
}

match_only_text 仅支持全文搜索,不支持 phrase/highlight 评分,但占用空间极小,日志场景必选


9. 客户端接入规范

9.1 客户端选型(按语言)

语言官方客户端备注
Javaco.elastic.clients:elasticsearch-java8.x 必选,旧 RestHighLevelClient 已废弃
Gogithub.com/elastic/go-elasticsearch/v8官方,避免 olivere/elastic(社区)
Pythonelasticsearch[async]==8.x + elasticsearch-dsl注意 8.x 强制 https
Node.js@elastic/elasticsearch 8.xTS 类型完整

禁止使用 curl / okhttp 裸调 REST 写业务代码。

9.2 连接配置

# 配置示例(应用层)
elasticsearch:
  hosts:
    - https://es-node-01.internal:9200
    - https://es-node-02.internal:9200
    - https://es-node-03.internal:9200
  username: ${ES_USER}
  password: ${ES_PASSWORD}             # 必须从 KMS / Vault 注入,禁止明文
  ca_fingerprint: "AB:CD:..."          # 8.x 推荐用指纹替代证书文件
  request_timeout: 30s
  connect_timeout: 5s
  max_retries: 3
  retry_on_status: [429, 502, 503, 504]
  max_connections_per_node: 50
  sniff: false                         # K8s 内网禁用 sniff(节点 IP 会变)

9.3 客户端必须实现的能力

能力说明
连接池单例化 client,禁止每次请求新建
重试仅对幂等操作(GET、PUT with id)和 429/5xx 重试
熔断触发 ES circuit_breaking_exception 时主动降级
指标上报 QPS、P99、bulk_rejected、connection_pool_active
链路追踪注入 X-Opaque-Id: {trace_id},便于在 ES slow log 中关联
# X-Opaque-Id 示例
GET /search-product/_search
X-Opaque-Id: req-abc123-user-001

ES 会在 task management、audit log、slow log 中带上此 ID。

9.4 连接池与并发

应用实例数 × max_connections_per_node × ES 节点数 ≤ ES 端 http.max_content_length

应用侧推荐:

  • 业务搜索:max_connections_per_node = 20~50
  • 批量写入:单独连接池,= 5~10
  • 禁止业务查询和批量写入共用连接池

10. 安全规范

10.1 认证

ES 8.x 默认开启 security,禁止关闭

场景方式
服务间调用API Key(推荐)或 Service Account Token
用户登录SAML / OIDC / LDAP
Kibana必须接 SSO,禁止本地账号

禁止

  • 使用 elastic 超管账号接入应用
  • 在配置文件、Git、日志中出现明文密码
  • API Key 不设过期时间

10.2 RBAC 最小权限

每个服务申请独立角色:

POST /_security/role/order_service_role
{
  "indices": [
    {
      "names": ["search-order-*"],
      "privileges": ["read", "write", "create_index"],
      "field_security": { "grant": ["*"], "except": ["pii.*"] },
      "query": "{\"term\": {\"tenant_id\": \"{{_user.metadata.tenant_id}}\"}}"
    }
  ],
  "cluster": ["monitor"]
}

要点:

  • field_security 屏蔽敏感字段(如手机号、身份证)
  • query 实现行级权限(多租户隔离)
  • cluster 权限仅给 monitor,禁止 manage

10.3 传输加密

  • 节点间通信:必须 mTLS(xpack.security.transport.ssl.enabled: true
  • HTTP 接入:必须 HTTPS,禁用 TLS 1.0/1.1
  • 跨集群(CCS/CCR):必须双向证书

10.4 审计日志

生产集群必须开启审计日志:

xpack.security.audit.enabled: true
xpack.security.audit.logfile.events.include:
  - access_denied
  - authentication_failed
  - connection_denied
  - tampered_request
  - run_as_denied
  - security_config_change

审计日志单独落盘并接入 SIEM。


11. 发布与变更流程

11.1 索引变更分级

级别变更类型审批
L1 安全新增可空字段、调整 refresh_interval、修改 search_analyzerTech Lead
L2 高风险修改分片数(必须 reindex)、修改字段类型、删除字段架构组 + DBA
L3 紧急删索引、修改 ILM、关闭副本CTO 授权

11.2 标准 Reindex 流程

1. 创建新索引 v(n+1),使用新 mapping
   PUT /search-product-v4 ...

2. 双写新旧索引(应用层)
   write → search-product-v3 (alias)
        → search-product-v4 (直连)

3. 历史数据 reindex(带限速)
   POST _reindex?wait_for_completion=false
   {
     "source": { "index": "search-product-v3", "size": 1000 },
     "dest":   { "index": "search-product-v4" },
     "conflicts": "proceed"
   }
   // 返回 task_id,用 GET _tasks/{task_id} 跟踪

4. 数据校验(count + 抽样 diff)

5. 切换别名(原子操作)
   POST /_aliases
   { "actions": [
     { "add":    { "index": "search-product-v4", "alias": "search-product" } },
     { "remove": { "index": "search-product-v3", "alias": "search-product" } }
   ]}

6. 观察 1~3 天,下线 v3 双写

7. 删除 v3 索引

禁止

  • 在线高峰期 reindex
  • reindex 不带 slices 参数(默认 1 slice,慢)
  • 删除旧索引前未确认无客户端引用

11.3 限流的 Reindex

POST _reindex?slices=auto&requests_per_second=2000
{
  "source": { "index": "search-product-v3" },
  "dest":   { "index": "search-product-v4" }
}

调速:

POST _reindex/{task_id}/_rethrottle?requests_per_second=500

11.4 灰度发布(业务侧)

新查询逻辑上线必须灰度:

10% → 30% → 50% → 100%
观察指标:QPS、P99 延迟、错误率、业务相关性指标(CTR、CVR)

A/B 测试可用别名 + filter 实现:

search-product-stable  → search-product-v3
search-product-canary  → search-product-v4

12. 可观测性与告警

12.1 必须采集的指标

类别指标告警阈值
集群status非 green 持续 5min P2,red 即 P0
集群unassigned_shards> 0 持续 10min
节点JVM heap usage> 75% 持续 10min
节点Old GC time / 分钟> 5s
节点磁盘使用率> 70% 预警,> 80% P1
节点CPU iowait> 30%
索引bulk rejected> 0
索引search rejected> 0
索引indexing latency P99> 业务 SLA
索引search latency P99> 业务 SLA
业务客户端连接池占用> 80%
业务4xx/5xx 比例> 1%

12.2 监控接入

推荐链路:

ES → Metricbeat / Elastic Agent → 监控集群(独立!) → Grafana / Kibana

不要用业务集群存自己的监控数据。

12.3 关键查询

# 集群健康
GET /_cluster/health?level=indices

# 未分配分片原因
GET /_cluster/allocation/explain

# 节点资源
GET /_cat/nodes?v&h=name,heap.percent,ram.percent,cpu,load_1m,disk.used_percent

# 待处理任务(应该接近 0)
GET /_cat/pending_tasks?v

# 线程池排队(重点关注 write、search 的 rejected)
GET /_cat/thread_pool?v&h=node_name,name,active,queue,rejected

13. 故障应急 SOP

13.1 集群 Red

  1. GET /_cluster/health 找出 red 索引
  2. GET /_cluster/allocation/explain 找未分配原因
  3. 常见原因:
    • 节点宕机 → 等待恢复或从快照还原
    • 磁盘满 → 清理 cold 索引或扩容
    • 副本配置错误 → PUT /index/_settings { "number_of_replicas": N }

13.2 写入被拒绝(429 / bulk_rejected)

  1. 检查 _cat/thread_pool?v 中 write 队列
  2. 临时措施:客户端指数退避重试
  3. 根因排查:
    • segment merge 跟不上 → 减小 bulk 并发
    • refresh 太频繁 → 调大 refresh_interval
    • 慢节点拖累 → 检查单节点 CPU/IO

13.3 查询超时

  1. GET /_tasks?actions=*search*&detailed 查看在跑的查询
  2. 必要时 cancel:POST /_tasks/{task_id}/_cancel
  3. 根因:
    • 深分页 → 改 search_after
    • 大聚合 → 加 size 上限或 sample
    • fielddata 触发 → 改用 keyword + doc_values

13.4 节点 OOM

  1. 立即扩容堆?不推荐,超过 31GB 反而变慢
  2. 正确动作:
    • 排查是否单查询打爆(indices.breaker
    • 减少分片数(合并小索引)
    • 关闭非必要的 fielddata

13.5 误删索引

  1. 第一时间停止所有写入
  2. 从快照恢复:
POST /_snapshot/my_repo/snapshot_20260511/_restore
{
  "indices": "search-product-v3",
  "rename_pattern":     "(.+)",
  "rename_replacement": "$1-restored"
}
  1. 别名切到 restored 索引

前提:必须有定期快照。生产集群必须配置:

PUT /_slm/policy/daily-snapshot
{
  "schedule": "0 30 1 * * ?",
  "name": "<snap-{now/d}>",
  "repository": "s3-repo",
  "config": { "indices": ["*"], "ignore_unavailable": true },
  "retention": { "expire_after": "30d", "min_count": 7, "max_count": 60 }
}

附录 A:禁用 / 慎用清单

A.1 禁用 API / 配置

原因替代方案
_search?scroll=...(新代码)已废弃PIT + search_after
from + size > 10000OOM 风险search_after
wildcard "*xxx*"(前导通配)全索引扫描ngram analyzer 或 wildcard 字段类型
script_score 复杂逻辑不可缓存function_score / rank_feature 字段
_update_by_query 无限速拖垮集群requests_per_second
dynamic: true 生产索引字段爆炸dynamic: strict
字段名超过 1000 个mapping explosion用 flattened
fielddata: true on text内存爆炸改 keyword + doc_values
单节点多角色(master+data+ingest)故障域大角色拆分
discovery.type: single-node 生产无 HA至少 3 master

A.2 慎用

注意事项
nested 字段查询/更新代价高,深度 ≤ 2
parent-child (join 字段)必须同分片,慢;优先 nested
percolator大量预存 query,内存敏感
Cross-Cluster Search跨网络延迟,不适合实时
Custom plugin升级 ES 版本会卡住

附录 B:Code Review Checklist

索引/Mapping 变更 PR

  • 是否走别名?是否带版本号?
  • 分片数依据是什么?是否在 30GB/分片范围内?
  • 副本数 ≥ 1?
  • dynamic 是否设为 strictfalse
  • 字段类型选型是否合理(参见 4.2)?
  • 是否有 text 字段未配多字段?
  • keyword 是否设了 ignore_above
  • 时间序列是否用 Data Stream + ILM?
  • 是否有禁用清单(附录 A)中的配置?

查询代码 PR

  • 是否显式指定 _source
  • 是否设了 size 上限?
  • 翻页是否用 search_after / PIT?
  • filter 是否优先于 query?
  • 聚合是否设了 size
  • 是否带 X-Opaque-Id 用于追踪?
  • 客户端是否单例?是否有连接池配置?
  • 重试策略是否仅对幂等 + 429/5xx?
  • 是否有降级方案(ES 不可用时)?

写入代码 PR

  • 是否走 Bulk API?批次大小是否 5~15MB?
  • 是否经 MQ 削峰?
  • 是否处理 429?是否指数退避?
  • 更新是否带 if_seq_no / if_primary_term
  • 是否使用业务主键作为 _id(除日志)?
  • 多租户是否设置了 routing?

文档版本

版本日期变更作者
v1.02026-05-11初版-

本文档为强约束规范。新人入职必读,每季度复盘更新。 反馈与例外申请:提交 issue 至 infra/es-governance 仓库。