日志索引按天建:app-logs-2026.11.12、app-logs-2026.11.13……一开始很清爽——查哪天就查哪个索引,删旧数据就删整个索引。半年后集群开始报 too many shards,主节点 CPU 常年 80 %,而每天的日志量其实只有 2 GB。
问题不在日志变多,在索引数量变多:180 个索引 × 默认 1 主 1 副 = 360 个分片,每个分片都有自己的段文件、自己的元数据,全都要进集群状态。集群状态的维护成本随分片数线性上升,而它由主节点单线程处理——这才是主节点顶不住的原因。
1. "每天一个索引"的收益与代价
| 收益 | 代价 |
|---|---|
| 删旧数据 = 删索引(秒级) | 分片数随时间线性增长 |
| 只查一天 = 只扫一个索引 | 跨天查询要 fan-out 到多个分片 |
| 单个索引小、恢复快 | mapping 要每天"复制"一遍(否则漂移) |
第五行是隐含成本:每个新索引都要重新应用一次映射。如果靠人复制,某个字段的类型在某天变了(status 从 keyword 变 text),那天之后的查询就开始出错——这类问题在"字段爆炸"之外,是另一条隐蔽的漂移路径。
// 180 天后的真实账单
索引数 180 分片数 360(1 主 1 副)
集群状态 ≈ 每分片几 KB → 总计数 MB,每次变更全量广播
主节点:单线程处理集群状态更新 → CPU 80%
// 而数据总量只有 360 GB,完全不需要 360 个分片
2. 模板解决的是"每天重复正确"
| 模板类型 | 解决什么 | 关键点 |
|---|---|---|
| 索引模板(旧) | 映射 + 设置 | 已被 composable 取代 |
| composable template | 组件可复用(mappings / settings / aliases) | 支持多模板叠加与优先级 |
| 组件模板(component template) | 把"映射片段"抽出来复用 | 一处改、处处生效 |
第二种是当前的标准做法:把"日志的公共映射"和"分片设置"拆成组件,模板只负责组合 + 匹配 app-logs-*。这样新增索引自动继承,不需要人复制。
// 一个可用的组合:组件 + 索引模板
PUT _component_template/logs-mappings
{ "template": { "mappings": { "dynamic": "strict",
"properties": { "@timestamp": {"type":"date"},
"level": {"type":"keyword"},
"msg": {"type":"text"} } } } }
PUT _index_template/app-logs
{ "index_patterns": ["app-logs-*"],
"composed_of": ["logs-mappings"],
"template": { "settings": { "number_of_shards": 1,
"number_of_replicas": 1 } },
"priority": 200 }
// 关键:dynamic=strict 让"新字段"直接失败,而不是悄悄建字段
3. ILM 的四个阶段,真正要定的是三个数
| 阶段 | 动作 | 关键参数 |
|---|---|---|
| hot | 写入 + rollover | max_primary_shard_size / max_age |
| warm | 只读、合并段、降副本 | forcemerge 到 1 段 |
| cold | 冻结、迁到廉价节点 | 查询变慢 |
| delete | 删除 | min_age |
第二行的三个数(分片大小、rollover 条件、保留时长)是联动的:
- 分片目标 30–50 GB(太大恢复慢、太小浪费)
- rollover 用
max_primary_shard_size: 50gb比max_age: 1d更好——按大小切比按时间切更稳定,因为日流量会波动 - 保留时长决定索引总数:
保留天数 × 每天索引数
// 两种 rollover 策略在流量波动下的差异
按时间(1d): 大促日 20GB → 一个 20GB 分片;平日 2GB → 一堆 2GB 小分片
按大小(50gb): 大促日切 1 次,平日 25 天才切一次 → 分片数稳定
// 判据:分片数应随"数据量"变化,而不是随"天数"变化
4. 别名:写指向"唯一",读指向"全部"
| 别名 | 指向 | 谁用 |
|---|---|---|
app-logs-write |
恰一个 write index | 写入方 |
app-logs-read |
所有活跃索引 | 查询方 |
app-logs-2026.11 |
单月索引 | 特定排查 |
// 一次 rollover 实际发生了什么(ILM 自动做,但你得知道顺序)
1 新建 app-logs-000002,自动应用模板
2 把 app-logs-write 别名移到新索引(唯一写入点)
3 把 app-logs-read 别名加上新索引,旧的保留
4 旧索引进入 warm 阶段
// 顺序错了:写别名同时指两个索引 → 写入分散 / 新索引没人写
第一行的"恰一个"是硬约束:写入别名若指向多个索引,写入会分散到多个分片,而 rollover 时又会出现"写到一半切新索引"的不一致。正确姿势是:写入永远通过 rollover 别名,ILM 自己维护它指向谁。
5. 半年后"维护不动"的三种形状
| 症状 | 根因 | 修法 |
|---|---|---|
too many shards |
分片数随时间线性涨 | 改 rollover 按大小 + 合并旧索引 |
| 主节点 CPU 高 | 集群状态过大 | 减少分片(reindex 到更少分片) |
| 某天开始查询报 mapping 错 | 模板未版本化,新索引映射漂移 | 建模板 + 版本化组件 |
第三种是全篇最值得记住的:它是一个"时间点"故障——某天之后的索引有问题,之前的都正常。排查时容易误判成"代码改动导致的",实际是那天的索引用了不同的映射。
// 三步体检(半年期索引都该跑一次)
$ curl -s 'localhost:9200/_cat/shards?v&h=index,shard,prirep,store' | awk 'NR>1{s[$1]++} END{for(i in s) print s[i], i}' | sort -rn | head
$ curl -s 'localhost:9200/_cluster/health?pretty' | grep -E 'number_of_(pending_tasks|in_flight)'
$ curl -s 'localhost:9200/app-logs-*/_mapping' | python3 -c 'import sys,json;d=json.load(sys.stdin);print(len(d),"indexes");[print(k, sorted(v["mappings"]["properties"])) for k,v in list(d.items())[:2]]'
// 判据:分片数 > 数据量(GB)/50 就该合并;mapping 各索引不一致就该建模板
6. 排错:五种"索引越建越多"
| 现象 | 八成原因 | 处理方向 |
|---|---|---|
| 每天 1 索引但流量很小 | 按时间 rollover | 改按大小 |
| 旧索引没被删 | ILM 没挂上或 min_age 写错 | 查 ilm explain |
写入报 index_not_found |
写别名指错/未创建 | 检查 rollover 别名 |
| 跨月查询很慢 | 分片太多、每片很小 | 合并旧索引 |
| 新索引 mapping 与旧的不同 | 没用模板 | 建 composable template |
第二行要用 _ilm/explain 逐个确认:ILM 是"挂了但没匹配",还是"匹配了但条件没满足",两者修法完全不同。
7. 边界:什么情况就该用单一索引
| 场景 | 为什么 | 说明 |
|---|---|---|
| 数据量小(< 50 GB 总量) | 一个索引足够 | 简单最好 |
| 需要全局排序/聚合 | 跨索引 fan-out 代价高 | 单索引 |
| 更新频繁(非追加) | 按天分会把更新拆散 | 单索引 + 分区字段 |
| 保留期无限 | 没有"删旧"需求 | 不用按天 |
判断口径一句话:按天建索引的收益只有"删旧数据"一条,代价是"分片数随时间增长"——所以只有当数据确实需要"到期删除"、且总量会涨到分片该合并的规模时才这么做;否则单索引 + 定期清理更省。
8. 自测
- "每天一个索引"的唯一真实收益是什么?它带来的主要代价是哪一项指标的线性增长?
- 为什么主节点会成为瓶颈?集群状态的维护成本由什么决定?
- composable template 与组件模板分别解决什么问题?
dynamic: strict在这里起什么作用? - 为什么 rollover 按"分片大小"比按"时间"更稳?分片目标大小该定多少?
- 写别名与读别名为什么必须分开?写别名指向多个索引会发生什么?
- "某天开始查询报 mapping 错"的根因是什么?为什么容易误判?
9. 一句话总结
按天建索引的收益只有"删旧数据",代价是分片数线性增长——而分片数直接决定集群状态大小,主节点单线程处理它,这才是半年后"维护不动"的机制。修法是三步:composable 模板(映射一处改、处处生效 + dynamic: strict)→ rollover 按分片大小而非时间 → 写/读别名分离。判据:分片数应随数据量变化,而不是随天数变化;数据总量 < 50 GB 时,单索引 + 定期清理更省。