索引设计是 ES 性能的基石。分片规划不当,集群再大也是白搭;字段类型乱选,查询再少也慢如蜗牛。本章从生产实战出发,把索引调优这件事彻底讲透。
一、分片数、副本数合理规划
1.1 先理解两个核心概念
| 主分片(Primary Shard) | 数据写入的入口,数据实际存储单元 | 决定写入吞吐 & 数据容量上限 |
| 副本分片(Replica Shard) | 主分片的完整拷贝 | 决定读取吞吐 & 高可用 |
一个索引的数据量 = 主分片数 × 单分片容量,所以分片数本质上是在切蛋糕。
1.2 分片数规划黄金法则
单分片大小经验值:
| 日志/时序类(写多读少) | 30GB ~ 50GB | 写入优先,分片大一点减少 overhead |
| 搜索业务(读多写少) | 10GB ~ 30GB | 查询优先,分片小一点提升并行度 |
| 高并发 OLAP 分析 | 5GB ~ 15GB | 聚合计算密集,需更多并行分片 |
分片数计算公式:
分片数 = 预估总数据量(GB) ÷ 推荐单分片大小(GB)
实战举例:
某电商订单索引,日均 200GB,保留 30 天,总量 6TB。
搜索场景,单分片按 20GB 规划:
分片数 = 6000GB ÷ 20GB = 300 个分片(太离谱了!)
等等——这个结果说明按天建索引的分片数不能这么算。正确做法是:
按天分索引,单个索引 200GB。
单日索引分片数 = 200GB ÷ 20GB = 10 个分片 ✅
1.3 分片数天花板
ES 官方硬性限制:单个节点最多 1000 个分片(含主分片+副本)。
软性限制(集群稳定性考虑):
集群总分数 = 节点数 × (堆内存 GB) × 20
举例:10 个节点,每个堆 32GB,建议总分片数不超过 10 × 32 × 20 = 6400。
红线规则:
- 单节点分片数 ≤ 1000(硬限制)
- 单节点分片数 ≤ 600(生产推荐)
- 单分片文档数 ≤ 21.4 亿(Lucene 硬限制)
1.4 副本数规划
| 0 | 临时数据、可重建索引 | 无高可用,节点挂了就丢数据 |
| 1 | 生产环境标配 | 存储翻倍 |
| 2 | 金融/高可靠性业务 | 存储 3 倍,写入压力增加 |
# 创建索引时指定分片和副本
PUT /orders-2026-06-06
{
"settings": {
"number_of_shards": 10,
"number_of_replicas": 1
}
}
动态调整(热迁移不中断):
# 副本数可随时调整,分片数创建后不可改
PUT /orders-2026-06-06/_settings
{
"index": {
"number_of_replicas": 2
}
}
1.5 不同数据量适配方案速查
| 小型(< 100GB/天) | 几十GB | 3~5 分片/天 | 1 | 3 节点即可 |
| 中型(100GB~1TB/天) | 几百GB | 5~10 分片/天 | 1 | 5~10 节点 |
| 大型(1TB~10TB/天) | TB 级 | 10~20 分片/天 | 1~2 | 10~30 节点 + 冷热分离 |
| 超大型(> 10TB/天) | 数TB | 20~50 分片/天 + 冷热分离 | 1 | 30+ 节点,考虑分层架构 |
二、冷热数据分离 & 索引生命周期管理(ILM)
2.1 为什么要冷热分离
日志/时序数据的典型特征:越新的数据被查询的越频繁。
查询热度
│ ████
│ ████
│ ██
│ █
│ ▏
└──────────────▶ 时间
热 温 冷
把 SSD 节点留给热数据,HDD 节点留给冷数据,成本降低 50% 以上,性能不降反升。
2.2 架构设计
┌─────────────────────────────────────────────────┐
│ ES Cluster │
│ │
│ ┌────────── Hot Nodes ──────────┐ │
│ │ node.attr.data: hot │ │
│ │ SSD, 64GB RAM, 16 vCPU │ │
│ │ 保存近 3 天数据 │ │
│ └───────────────────────────────┘ │
│ │ migrate (ILM) │
│ ▼ │
│ ┌────────── Warm Nodes ─────────┐ │
│ │ node.attr.data: warm │ │
│ │ SSD/HDD 混合, 32GB RAM │ │
│ │ 保存 3~30 天数据 │ │
│ └───────────────────────────────┘ │
│ │ migrate (ILM) │
│ ▼ │
│ ┌────────── Cold Nodes ─────────┐ │
│ │ node.attr.data: cold │ │
│ │ HDD, 16GB RAM, 可搜索 │ │
│ │ 保存 30~90 天数据 │ │
│ └───────────────────────────────┘ │
│ │ forcemerge + freeze │
│ ▼ │
│ ┌────────── Frozen / Delete ────┐ │
│ │ 冻结索引 / 快照到 S3 │ │
│ │ 超 90 天 → 归档或删除 │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────────────────┘
2.3 节点标记配置
# elasticsearch.yml — hot 节点
node.attr.data: hot
node.attr.rack: rack1
# warm 节点
node.attr.data: warm
# cold 节点
node.attr.data: cold
2.4 ILM 策略完整配置
PUT _ilm/policy/logs_policy
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50GB",
"max_age": "1d"
},
"set_priority": {
"priority": 100
}
}
},
"warm": {
"min_age": "3d",
"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
},
"set_priority": {
"priority": 0
}
}
},
"delete": {
"min_age": "90d",
"actions": {
"delete": {}
}
}
}
}
}
2.5 ILM 各阶段关键动作解读
| Hot | rollover | 按条件自动创建新索引 | 必须配合 index template + alias |
| Warm | forcemerge | 合并 segment 为 1 个 | 只读阶段执行,减少 50%+ 文件句柄 |
| Warm | shrink | 减少主分片数 | 要求目标分片数是原分片数的因子 |
| Warm | allocate | 迁移到 warm 节点 | 配合 index.routing.allocation 使用 |
| Cold | allocate | 迁移到冷节点 + 副本降为 0 | 搜索变慢但可接受 |
| Delete | delete | 删除过期索引 | 数据不可恢复,建议先 snapshot |
2.6 Rollover 完整链路
// Step1: 创建索引模板
PUT _index_template/logs_template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.lifecycle.name": "logs_policy",
"index.lifecycle.rollover_alias": "logs"
}
}
}
// Step2: 创建初始索引 + alias
PUT logs–000001
{
"aliases": {
"logs": {
"is_write_index": true
}
}
}
// Step3: 之后全部写 alias,ES 自动 rollover
POST logs/_doc
{
"message": "this goes to current write index",
"@timestamp": "2026-06-06T10:00:00Z"
}
Rollover 触发条件可以组合:
"rollover": {
"max_primary_shard_size": "50GB", // 主分片总大小
"max_age": "1d", // 索引存在时长
"max_docs": 100000000, // 文档数量上限
"max_size": "100GB" // 索引总大小(含副本)
}
三、索引拆分:按天/按月分索引设计
3.1 为什么要拆分
单个大索引的致命问题:
- 查询要扫全量数据,无法利用时间过滤剪枝
- 扩容只能增加副本,无法拆分热点分片
- 删除历史数据只能 delete_by_query,产生大量 tombstone
- 索引越大,segment merge 成本指数级增长
拆成小索引的好处:
单一大索引 按天拆分
┌─────────────────────┐ ┌── Day1 ──┐
│ 365 天全量数据 │ ├── Day2 ──┤
│ 10TB · 100 分片 │ → ├── Day3 ──┤
│ 删老数据要扫全量 │ ├── … ───┤
└─────────────────────┘ └── Day365 ┘
删老数据 = DELETE /index
瞬间完成,零开销!
3.2 按天拆分(日志类首选)
索引命名规范:{业务前缀}-{日期}
例:nginx-access-2026.06.06
PUT _index_template/nginx_access_template
{
"index_patterns": ["nginx-access-*"],
"template": {
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1,
"index.lifecycle.name": "logs_policy"
},
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"remote_addr": { "type": "ip" },
"request": { "type": "keyword" },
"status": { "type": "integer" },
"body_bytes_sent": { "type": "long" },
"request_time": { "type": "float" }
}
}
}
}
按天查询策略:
// 查询最近 7 天,自动利用索引名 pattern 做前置过滤
GET nginx–access–2026.06.*/_search
{
"query": {
"range": { "@timestamp": { "gte": "now-7d" } }
}
}
// 或者用 alias 统一入口
POST _aliases
{
"actions": [
{ "add": { "index": "nginx-access-2026.06.06", "alias": "nginx-access" } },
{ "add": { "index": "nginx-access-2026.06.05", "alias": "nginx-access" } },
{ "add": { "index": "nginx-access-2026.06.04", "alias": "nginx-access" } }
]
}
// 业务代码直接用 alias 查询
GET nginx–access/_search
3.3 按月拆分(业务数据)
适合数据量中等、查询通常按月粒度的场景(如订单、交易流水)。
索引命名:{业务前缀}-{YYYY-MM}
例:orders-2026-06
动态分片数(按月同比调整):
| 常规月 | 300GB | 10 |
| 大促月(618/双11) | 1TB | 30 |
| 春节月 | 100GB | 5 |
PUT orders–2026–06
{
"settings": {
"number_of_shards": 10, // 常规月
"number_of_replicas": 1
}
}
3.4 跨索引查询优化技巧
使用 index_patterns + allow_no_indices:
GET orders–2026–0[1–6]/_search
{
"query": {
"bool": {
"filter": [
{ "range": { "order_date": { "gte": "2026-01-01", "lte": "2026-06-06" } } }
]
}
}
}
避免 * 扫全量:
// ❌ 不要这样
GET orders–*/_search
// ✅ 精确到月份范围
GET orders–2026–0[1–6]/_search
四、字段类型优化 & 禁用无用字段
4.1 字段类型的性能代价
不同字段类型的存储和查询效率差异巨大:
| keyword | 低(倒排+doc_values) | 精确匹配极快 | 状态码、标签、ID |
| text | 高(倒排+分词+位置) | 全文搜索 | 文章内容、日志消息 |
| long | 低(8字节/doc_values) | 高 | 金额、计数 |
| integer | 更低(4 字节) | 高 | 状态码、年龄 |
| keyword + text (默认) | 双倍 | — | 不推荐,选其一 |
| object (nested) | 极高 | 慢 | 非必要不用 nested |
4.2 动态映射的坑:string 默认双字段
// 当你写入一个字符串字段,ES 动态映射会同时创建:
{
"message": {
"type": "text", // ← 做全文搜索
"fields": {
"keyword": { // ← 做排序聚合
"type": "keyword",
"ignore_above": 256
}
}
}
}
每个字符串字段都存了两份索引数据! 如果你的日志有 200 个字段,其中 100 个是字符串,存储膨胀非常严重。
4.3 精准字段映射模板
PUT _index_template/production_template
{
"index_patterns": ["myapp-*"],
"template": {
"mappings": {
"dynamic": "strict", // 禁止自动创建字段,先定义再用
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"message": { "type": "text" },
"service": { "type": "keyword" },
"host": { "type": "keyword" },
"user_id": { "type": "keyword" },
"order_id": { "type": "long" },
"amount": { "type": "scaled_float", "scaling_factor": 100 },
"status": { "type": "byte" },
"is_vip": { "type": "boolean" },
"created_at": { "type": "date" },
"tags": { "type": "keyword" },
"metadata": {
"type": "object",
"enabled": false // 不索引,只存储原始 JSON
}
}
}
}
}
4.4 字段类型选择速查
| 性别/状态码 (0~255) | byte | integer / keyword | 75% |
| 金额(精确到分) | scaled_float | float / double | 50% |
| 布尔标志 | boolean | keyword("true") | 90% |
| 只存不查的 JSON | object { enabled: false } | 自动展开 | 巨大 |
| IP 地址 | ip | keyword / text | 支持 CIDR 查询 |
| 时间戳 | date | long / keyword | 支持时间表达式 |
4.5 禁用无用字段
PUT myapp–2026–06–06
{
"mappings": {
"dynamic": true,
"dynamic_templates": [
{
"strings_as_keyword": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword",
"ignore_above": 256
}
}
}
],
"properties": {
"debug_stacktrace": {
"type": "text",
"index": false // 可搜但不评分
},
"raw_request_body": {
"type": "keyword",
"index": false, // 不建索引,纯存储
"doc_values": false // 不做排序聚合
},
"internal_trace_id": {
"type": "keyword",
"enabled": false // 完全不索引
}
}
}
}
索引选项对比:
| 可搜索 | ✅ | ❌ | ❌ |
| 可聚合 | ✅ | ❌ | ❌ |
| 原值可取出 | ✅ | ✅ | ✅ |
| 占用磁盘 | 高 | 低 | 极低 |
4.6 source 字段过滤(慎重!)
PUT myapp–2026–06–06
{
"mappings": {
"_source": {
"includes": ["@timestamp", "level", "message", "service"],
"excludes": ["raw_request_body", "debug_stacktrace"]
}
}
}
⚠️ 警告:_source 排除的字段将完全不可见,_update 会失败,_reindex 也会丢失。只在确定字段完全无用时才用 _source 过滤。
4.7 存储优化效果估算
优化前(动态映射全默认):
日均 500GB × 30 天 = 15TB
优化后(类型精简化 + 无用字段 index:false):
日均 200GB × 30 天 = 6TB
存储成本降低 60%
五、索引冻结、删除、归档生产规范
5.1 三种生命周期终态
运行中 → 冻结 → 关闭 → 删除
│ │ │ │
│ │ │ └─ 数据消失
│ │ └─ 零内存,零查询
│ └─ 可搜索(慢),内存开销极小
└─ 正常状态
5.2 索引冻结(Frozen)
冻结原理: 将索引的 translog 清理,内存结构释放,只保留磁盘上的持久化文件。查询时临时加载必要结构到内存,查完即释放。
// 冻结索引
POST /orders–2026–01/_freeze
// 解冻(恢复前必须先 unfreeze)
POST /orders–2026–01/_unfreeze
冻结后的限制:
- 查询变慢(3~10 倍,取决于查询复杂度)
- 不能写入
- 不能 forcemerge
- 部分聚合操作受限
适用条件:
- 数据 > 90 天,偶尔需要查
- 法律合规要求保留但几乎不访问
5.3 索引关闭(Close)
// 关闭索引
POST /orders–2026–01/_close
// 打开索引
POST /orders–2026–01/_open
| Open | 占用 | 占用 | ✅ | ✅ |
| Frozen | 极低 | 占用 | ✅(慢) | ✅ |
| Closed | 0 | 占用 | ❌ | ✅ |
关闭索引 vs 删除索引:
- 关闭:还能重新打开,数据完整保留
- 删除:数据没了,除非有 snapshot
5.4 索引删除
// 单索引删除
DELETE /orders–2026–01
// 批量按 pattern 删除
DELETE /nginx–access–2026.03.*
// 通过 ILM 自动删除(推荐)
// 已在 2.4 节 ILM policy 中配置 delete phase
删除前必做 checklist:
□ 确认快照已生成(snapshot repository 中有备份)
□ 确认数据保留策略合规(金融 5 年、一般业务 90 天?)
□ 确认无下游消费(Kafka connect、Flink 等是否还在读)
□ 用 _cat/indices 确认索引大小,记录归档日志
5.5 快照归档(Snapshot)
绝不在没有快照的情况下删索引!
// Step1: 注册快照仓库(首次)
PUT _snapshot/es_backup
{
"type": "fs",
"settings": {
"location": "/data/backups/es_snapshots",
"compress": true
}
}
// Step2: 创建快照
PUT _snapshot/es_backup/snapshot_2026–06–06?wait_for_completion=true
{
"indices": "orders-2026-01",
"ignore_unavailable": true,
"include_global_state": false
}
// Step3: 验证快照
GET _snapshot/es_backup/snapshot_2026–06–06/_status
// Step4: 确认无误 → 删除原索引
DELETE /orders–2026–01
// Step5: 需要时从快照恢复
POST _snapshot/es_backup/snapshot_2026–06–06/_restore
{
"indices": "orders-2026-01",
"index_settings": {
"index.number_of_replicas": 0
}
}
5.6 对象存储归档(超大规模)
本地快照成本随着数据量线性增长。对于 PB 级数据,建议归档到 S3/OSS/MinIO:
PUT _snapshot/s3_archive
{
"type": "s3",
"settings": {
"bucket": "es-archive-bucket",
"region": "ap-guangzhou",
"base_path": "prod/snapshots",
"max_restore_bytes_per_sec": "100mb",
"max_snapshot_bytes_per_sec": "100mb"
}
}
5.7 索引生命周期生产 SOP
┌─────────────────────────────────────────────────────────────┐
│ 索引生命周期 SOP │
├──────┬──────────────────────────────────────────────────────┤
│ DAY │ 0~3 Hot 节点 写入活跃 5 shard + 1 replica │
│ DAY │ 4~30 Warm 节点 只读 1 shard + 1 replica │
│ DAY │ 31~90 Cold 节点 只读 1 shard + 0 replica │
│ DAY │ 91~180 Frozen 慢查询 冻结状态 │
│ DAY │ 181 Snapshot 归档 S3/本地仓库快照 │
│ DAY │ 181+ Delete 销毁 永久删除 │
└──────┴──────────────────────────────────────────────────────┘
5.8 监控告警配置
// 索引按天大小监控
GET _cat/indices/orders–*?h=index,store.size&bytes=gb&s=index:desc
// 分片健康检查
GET _cat/shards?h=index,shard,prirep,state,node&s=state:desc
// 未分配分片告警
GET _cluster/allocation/explain
关键告警规则:
| 未分配分片数 > 0 | 任何 | 🔴 严重 |
| 单节点分片数 > 600 | 接近上限 | 🟡 警告 |
| 磁盘使用率 > 85% | 高水位线 | 🔴 严重 |
| 磁盘使用率 > 80% | 接近高水位 | 🟡 警告 |
| 索引无 ILM policy | — | 🟢 提示 |
本章小结
| 分片规划 | 按数据量 & 场景定分片数 | 查询并行度 ↑,写入吞吐 ↑ |
| 冷热分离 | ILM + node attribute | 存储成本 ↓ 50%,热数据性能 ↑ |
| 索引拆分 | 按天/月建索引 + alias | 删除成本 O(1),查询剪枝效率 ↑ |
| 字段优化 | 精准类型 + 禁用无用字段 | 存储成本 ↓ 30%~60%,写入速度 ↑ |
| 归档删除 | ILM 自动化 + snapshot 兜底 | 零运维人力,数据安全有保障 |
一句话总结: 索引调优不是一次性动作,而是伴随数据增长持续迭代的过程。把 ILM 配好,把字段映射定好,后面才能睡得着觉。
下一章预告:【第13章:集群运维与故障排查】—— 节点失联、脑裂、OOM、GC 风暴……那些让你半夜惊醒的故障,我们一一拆解。



