适用版本:Elasticsearch 8.6+(关键差异处标注 7.x 兼容说明) 适用范围:业务搜索、日志/可观测性、向量检索 (kNN/RAG) 三类场景 文档定位:架构师、后端开发、SRE 共同遵循的落地规范,违反规范的设计应在 Code Review 阶段被驳回
目录
- 总体原则与红线
- 集群与容量规划
- 索引设计规范
- Mapping 规范
- 写入规范
- 查询规范
- 向量检索 / RAG 场景
- 日志 / 可观测性场景
- 客户端接入规范
- 安全规范
- 发布与变更流程
- 可观测性与告警
- 故障应急 SOP
- 附录 A:禁用 / 慎用清单
- 附录 B:Code Review Checklist
1. 总体原则与红线
1.1 设计哲学
ES 不是关系型数据库,不是消息队列,也不是主存储。它是面向读优化的分布式倒排索引。所有设计决策都应回到三个问题:
- 读多还是写多? 决定分片数与刷新策略。
- 数据是否随时间增长? 决定是否使用 Data Stream / ILM。
- 数据是否需要强一致? 如果是,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_shards | 30 | 便于将来 split |
translog.durability | request(搜索类)/ 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 性能的关键)
- 禁止开启 swap:
bootstrap.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.MM或YYYY.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、订单号、SKU | keyword | text / long | 不分词;long 排序占用更多空间 |
| 状态枚举 | keyword | text / byte | 节省空间且支持 terms 聚合 |
| 全文搜索字段 | text + keyword 多字段 | 仅 text | 既能搜又能聚合排序 |
| 金额 | scaled_float(scaling_factor=100) | double / float | 精度问题,禁用 float |
| 时间戳 | date | keyword / long | 利于时间范围查询 |
| IP 地址 | ip | keyword | 支持 CIDR 查询 |
| 地理位置 | geo_point / geo_shape | 两个 float | 利用 BKD 索引 |
| 高维向量 | dense_vector | 多个 float 字段 | 走 HNSW 索引 |
| 大段不搜文本 | text 关闭 index | 默认 text | 节省倒排空间 |
| JSON 元数据 | flattened 或 enabled: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 |
| 客户端超时 | 60s | bulk 内部异步,无需短超时 |
| 失败重试 | 仅对 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 时,业务方应:
- 重读最新文档
- 应用本次变更
- 重试更新(最多 3 次)
禁止用 retry_on_conflict 处理强业务语义的更新(会丢失中间状态)。
6. 查询规范
6.1 查询基本原则
- 永远显式指定
_source:减少网络传输 - 永远设置
track_total_hits: false或具体上限:精确计数代价高 - filter 优先于 query:filter 可缓存,query 算分
- 聚合必须设置 size 上限:避免内存爆炸
- 禁止
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 + size | from + 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不设 sizecardinality在 high-cardinality 字段上不设precision_threshold- 多层 nested terms 聚合(>3 层),必要时改用
composite聚合 - 在
text字段上聚合(默认无 doc_values,会报错或开 fielddata 爆内存)
6.5 慢查询治理
任何查询超过 200ms(业务搜索)或 1s(分析查询)必须:
- 用
_search?profile=true分析 - 检查是否有 wildcard
*xxx*、正则、script_score - 检查 filter 是否被缓存(
indices.requests.cache) - 检查是否触发
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) |
|---|---|---|
| 384 | 1.5 KB | ~3 GB |
| 768 | 3 KB | ~6 GB |
| 1536 | 6 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 策略示例
按数据价值分层:
| 阶段 | 时长 | 节点 | 副本 | 操作 |
|---|---|---|---|---|
| Hot | 1 天 | SSD | 1 | 写入 + 高频查询 |
| Warm | 7 天 | SATA SSD | 1 | shrink 到 1 分片,forcemerge |
| Cold | 30 天 | HDD | 0 | freeze,可搜索快照 |
| Frozen | 90 天 | 对象存储 | 0 | 可搜索快照 |
| Delete | 90+ 天 | - | - | 删除 |
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 客户端选型(按语言)
| 语言 | 官方客户端 | 备注 |
|---|---|---|
| Java | co.elastic.clients:elasticsearch-java | 8.x 必选,旧 RestHighLevelClient 已废弃 |
| Go | github.com/elastic/go-elasticsearch/v8 | 官方,避免 olivere/elastic(社区) |
| Python | elasticsearch[async]==8.x + elasticsearch-dsl | 注意 8.x 强制 https |
| Node.js | @elastic/elasticsearch 8.x | TS 类型完整 |
禁止使用 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_analyzer | Tech 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
GET /_cluster/health找出 red 索引GET /_cluster/allocation/explain找未分配原因- 常见原因:
- 节点宕机 → 等待恢复或从快照还原
- 磁盘满 → 清理 cold 索引或扩容
- 副本配置错误 →
PUT /index/_settings { "number_of_replicas": N }
13.2 写入被拒绝(429 / bulk_rejected)
- 检查
_cat/thread_pool?v中 write 队列 - 临时措施:客户端指数退避重试
- 根因排查:
- segment merge 跟不上 → 减小 bulk 并发
- refresh 太频繁 → 调大
refresh_interval - 慢节点拖累 → 检查单节点 CPU/IO
13.3 查询超时
GET /_tasks?actions=*search*&detailed查看在跑的查询- 必要时 cancel:
POST /_tasks/{task_id}/_cancel - 根因:
- 深分页 → 改 search_after
- 大聚合 → 加 size 上限或 sample
- fielddata 触发 → 改用 keyword + doc_values
13.4 节点 OOM
- 立即扩容堆?不推荐,超过 31GB 反而变慢
- 正确动作:
- 排查是否单查询打爆(
indices.breaker) - 减少分片数(合并小索引)
- 关闭非必要的 fielddata
- 排查是否单查询打爆(
13.5 误删索引
- 第一时间停止所有写入
- 从快照恢复:
POST /_snapshot/my_repo/snapshot_20260511/_restore
{
"indices": "search-product-v3",
"rename_pattern": "(.+)",
"rename_replacement": "$1-restored"
}
- 别名切到 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 > 10000 | OOM 风险 | 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是否设为strict或false? - 字段类型选型是否合理(参见 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.0 | 2026-05-11 | 初版 | - |
本文档为强约束规范。新人入职必读,每季度复盘更新。 反馈与例外申请:提交 issue 至
infra/es-governance仓库。