Elasticsearch 日志索引设计:按小时切分 + 小时内 Rollover 兜底(ILM 实战,附完整脚本)
日志场景下 Elasticsearch 索引怎么切?按天建索引,大流量日单索引轻松突破几百 GB,merge 变慢、查询抖动;只靠 ILM 按大小 rollover,索引边界又和业务时间对不上,排障时想拉某个小时的日志都费劲。本文给出一套外层按整点小时切分 + 内层 Rollover 兜底的双层别名方案,ILM 策略、索引模板、cron 脚本、应用层写入路由全部可复制。
一、为什么日志索引推荐按小时切分
先看两种常见做法的坑:
| 做法 | 问题 |
|---|---|
| 单一大索引(按月/永久) | 索引无限膨胀,segment merge 停不下来,删除只能 delete_by_query,集群越用越慢 |
| 按天切分 | 常规流量没问题,但高峰日单索引可到几百 GB;排查问题想精确定位某小时,仍要在巨型索引里扫 |
| 只用 ILM rollover | 索引边界由数据量决定,与时间无关,对账、归档、按时间段删除都不方便 |
日志数据有强时间特征:排查看小时、对账按小时、归档删过期也按时间。所以理想方案是索引边界严格对齐整点小时,同时在单小时数据突增时还能自动拆分------这就是双层设计。
二、方案总览:双层别名架构
- 外层(小时级):每整点由 cron 新建一个独立索引,名字带小时戳
- 内层(rollover 级):同一小时内数据量超阈值时自动 rollover 出新物理索引
- 双层 alias:外层别名按小时命名,承载写入和查询;内层 rollover 由 ILM 维护
text
logs-2026061114-000001 ─┐
logs-2026061114-000002 ─┼─ alias: logs-2026061114 (is_write_index: 当前可写)
logs-2026061115-000001 ──── alias: logs-2026061115 (is_write_index: 当前可写)
应用层只认 logs-YYYYMMDDHH 这个小时级别名,rollover 发生时别名自动切换,写入方零改动。
需要的资源一共 5 项:
| 资源类型 | 名称 | 数量 | 作用 |
|---|---|---|---|
| ILM Policy | logs-policy |
1 份 | 全集群共用,管 rollover 和过期删除 |
| Index Template | logs-template |
1 份 | 自动应用 mappings + settings |
| Cron 脚本 | create_hourly_index.sh |
1 个 | 每整点执行 |
| 外层 Alias | logs-YYYYMMDDHH |
每小时 1 个 | 写入与查询入口 |
| 内层 Alias | 索引名即别名 | 每物理索引 1 个 | rollover 维护 |
三、部署步骤
3.1 创建 ILM 策略(全集群一份)
bash
PUT _ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "20gb",
"max_docs": 100000000
}
}
},
"warm": {
"min_age": "3d",
"actions": {
"forcemerge": { "max_num_segments": 1 }
}
},
"delete": {
"min_age": "30d",
"actions": { "delete": {} }
}
}
}
}
阶段说明:
hot:可写阶段,单索引到 20GB 或 1 亿文档即 rollover,防止索引过大warm:3 天后强制段合并为 1 段,省存储、提查询delete:30 天后自动删除
⚠️ 不要配
max_age。整点切索引已经承担了时间维度的切分,ILM 里再配max_age会和小时边界冲突,出现"不到整点就滚"的脏边界。
3.2 创建索引模板
bash
PUT _index_template/logs-template
{
"index_patterns": ["logs-*"],
"priority": 100,
"template": {
"settings": {
"number_of_shards": 10,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"message": { "type": "text" },
"service": { "type": "keyword" },
"host": { "type": "keyword" }
}
}
}
}
注:mappings 按实际日志字段补充。ILM 绑定和
rollover_alias不要 写在模板里,留到每小时的种子索引上单独指定,避免污染所有logs-*索引。
3.3 Cron 脚本:每整点创建种子索引
脚本路径 /opt/scripts/create_hourly_index.sh:
bash
#!/bin/bash
set -euo pipefail
ES_HOST="${ES_HOST:-http://es-host:9200}"
HOUR=$(date -u +%Y%m%d%H)
INDEX="logs-${HOUR}-000001"
ALIAS="logs-${HOUR}"
# 幂等检查:索引已存在则跳过
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-u "${ES_USER}:${ES_PASS}" \
-X HEAD "${ES_HOST}/${INDEX}")
if [ "$STATUS" = "200" ]; then
echo "[$(date)] Index ${INDEX} already exists, skip."
exit 0
fi
if [ "$STATUS" != "404" ]; then
echo "[$(date)] Unexpected status ${STATUS} for ${INDEX}, abort."
exit 1
fi
# 创建种子索引,同时挂外层 alias 和 ILM 配置
RESP=$(curl -s -u "${ES_USER}:${ES_PASS}" \
-X PUT "${ES_HOST}/${INDEX}" \
-H 'Content-Type: application/json' \
-d "{
\"aliases\": {
\"${ALIAS}\": { \"is_write_index\": true }
},
\"settings\": {
\"index.lifecycle.name\": \"logs-policy\",
\"index.lifecycle.rollover_alias\": \"${ALIAS}\"
}
}")
echo "[$(date)] Create ${INDEX} alias ${ALIAS}: ${RESP}"
crontab 配置:
bash
# 每小时 0 分 5 秒执行(避开整点集群压力尖峰)
5 * * * * /opt/scripts/create_hourly_index.sh >> /var/log/es-hourly-index.log 2>&1
两个细节:脚本里
date -u用 UTC,全链路统一时区;crontab 建议主备双机部署,防止单点漏建。
四、应用层接入
4.1 写入路由:按当前小时选别名
Java 示例:
java
String hour = ZonedDateTime.now(ZoneOffset.UTC)
.format(DateTimeFormatter.ofPattern("yyyyMMddHH"));
String alias = "logs-" + hour;
IndexRequest req = new IndexRequest(alias)
.source(jsonMap, XContentType.JSON);
Python 示例:
python
from datetime import datetime, timezone
def current_alias():
hour = datetime.now(timezone.utc).strftime("%Y%m%d%H")
return f"logs-{hour}"
es.index(index=current_alias(), document=log_entry)
4.2 查询姿势
bash
# 单小时查询
GET logs-2026061114/_search
# 跨小时:明确列出,别用通配符(展开慢)
GET logs-2026061113,logs-2026061114/_search
# 时间范围查询兜底
GET logs-*/_search
{
"query": {
"range": { "@timestamp": { "gte": "now-2h", "lte": "now" } }
}
}
五、Rollover 行为详解
触发时机:小时内数据满足任一条件自动 rollover------单索引 ≥20GB,或文档数 ≥1 亿。
状态变化:
text
触发前:
logs-2026061114-000001 ← 可写 (is_write_index: true)
触发后:
logs-2026061114-000001 ← 只读
logs-2026061114-000002 ← 可写 (is_write_index: true)
小时别名 logs-2026061114 自动指向 000002,应用层无感知。
与下个整点的衔接:
- 14:59:59 数据仍写入
logs-2026061114-000002 - 15:00:05 cron 执行,新建
logs-2026061115-000001 - 应用层下次写入按当前小时自动切到
logs-2026061115-*
六、验证与日常运维命令
bash
# 查看某小时索引的 ILM 进度
GET logs-2026061114/_ilm/explain
# 查看别名当前指向
GET _alias/logs-2026061114
# 列出最近索引的大小与文档数
GET _cat/indices/logs-*?h=index,docs.count,store.size&s=index
测试时手动触发一次 rollover 验证链路:
bash
POST logs-2026061114/_rollover
{
"conditions": { "max_docs": 1 }
}
七、监控告警配置
| 监控项 | 阈值 | 级别 |
|---|---|---|
| 某小时索引缺失 | 当前时间 +5 分钟该小时索引不存在 | Critical |
| ILM 阶段卡住 | phase 5 分钟无变化 |
Warning |
| 集群分片数 | 接近 cluster.max_shards_per_node |
Warning |
| 磁盘使用率 | ≥80% | Warning |
监控建议接 Prometheus + ElastAlert 或自研 watcher,重点盯索引缺失------cron 失败意味着应用写入直接报错。
八、踩坑清单
| 坑 | 后果 | 应对 |
|---|---|---|
| cron 失败整点索引缺失 | 应用层写入失败 | 监控告警 + 应用层降级写 logs-* 通配兜底 |
| 单小时数据爆增 | 索引过大性能劣化 | rollover 自动兜底(max_size 20gb) |
rollover_alias 配置缺失 |
ILM 静默不工作 | 种子索引脚本里强制写入该配置 |
跨小时查询用 logs-* |
通配展开慢 | 应用层按需拼接具体小时别名 |
| 应用节点时区不一致 | 写错小时索引 | 全链路统一 UTC |
九、部署 Checklist
- 创建 ILM policy
logs-policy - 创建 Index template
logs-template - 部署
create_hourly_index.sh到独立调度机 - 配置 crontab(主备双机)
- 应用层改造写入路由逻辑
- 配置监控告警(缺失索引、ILM 卡住、磁盘)
- 测试环境完整跑一次 rollover 验证
十、版本兼容性
| ES 版本 | 支持情况 |
|---|---|
| 7.8+ | ✅ 推荐,ILM + rollover 稳定 |
| 7.0 - 7.7 | ⚠️ 部分 API 行为有差异,建议升级 |
| 8.x | ✅ 兼容本方案全部 API |
总结
这套方案的本质是让时间边界和数据量边界各管各的:整点切分满足排障、对账、归档的时间对齐需求,ILM rollover 兜住流量突增,双层别名把两者对应用层透明。全部资源就一份 ILM 策略 + 一份模板 + 一个 cron 脚本,复制文中 DSL 即可落地。
你们的日志索引是按天切、按小时切,还是纯 rollover?有没有遇到过单索引几百 GB 的场面?评论区聊聊你的切法。
文中脚本与 DSL 在 ES 7.17/8.x 环境验证通过,字段映射请按实际业务调整。