欢迎光临
我们一直在努力

【ElasticSearch从入门到架构师】第12章:索引与数据模型调优

索引设计是 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 各阶段关键动作解读

阶段关键 Action作用注意事项
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 logs000001
{
"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 nginxaccess2026.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 nginxaccess/_search

3.3 按月拆分(业务数据)

适合数据量中等、查询通常按月粒度的场景(如订单、交易流水)。

索引命名:{业务前缀}-{YYYY-MM}
例:orders-2026-06

动态分片数(按月同比调整):

月份预期数据量分片数
常规月 300GB 10
大促月(618/双11) 1TB 30
春节月 100GB 5

PUT orders202606
{
"settings": {
"number_of_shards": 10, // 常规月
"number_of_replicas": 1
}
}

3.4 跨索引查询优化技巧

使用 index_patterns + allow_no_indices:

GET orders20260[16]/_search
{
"query": {
"bool": {
"filter": [
{ "range": { "order_date": { "gte": "2026-01-01", "lte": "2026-06-06" } } }
]
}
}
}

避免 * 扫全量:

// ❌ 不要这样
GET orders*/_search

// ✅ 精确到月份范围
GET orders20260[16]/_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 myapp20260606
{
"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 // 完全不索引
}
}
}
}

索引选项对比:

配置index: trueindex: falseenabled: false
可搜索
可聚合
原值可取出
占用磁盘 极低

4.6 source 字段过滤(慎重!)

PUT myapp20260606
{
"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 /orders202601/_freeze

// 解冻(恢复前必须先 unfreeze)
POST /orders202601/_unfreeze

冻结后的限制:

  • 查询变慢(3~10 倍,取决于查询复杂度)
  • 不能写入
  • 不能 forcemerge
  • 部分聚合操作受限

适用条件:

  • 数据 > 90 天,偶尔需要查
  • 法律合规要求保留但几乎不访问

5.3 索引关闭(Close)

// 关闭索引
POST /orders202601/_close

// 打开索引
POST /orders202601/_open

状态内存磁盘可查询元数据可见
Open 占用 占用
Frozen 极低 占用 ✅(慢)
Closed 0 占用

关闭索引 vs 删除索引:

  • 关闭:还能重新打开,数据完整保留
  • 删除:数据没了,除非有 snapshot

5.4 索引删除

// 单索引删除
DELETE /orders202601

// 批量按 pattern 删除
DELETE /nginxaccess2026.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_20260606?wait_for_completion=true
{
"indices": "orders-2026-01",
"ignore_unavailable": true,
"include_global_state": false
}

// Step3: 验证快照
GET _snapshot/es_backup/snapshot_20260606/_status

// Step4: 确认无误 → 删除原索引
DELETE /orders202601

// Step5: 需要时从快照恢复
POST _snapshot/es_backup/snapshot_20260606/_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 风暴……那些让你半夜惊醒的故障,我们一一拆解。

赞(0)
未经允许不得转载:171主机测评 » 【ElasticSearch从入门到架构师】第12章:索引与数据模型调优
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址