"ES 把索引设为只读,不是 bug,而是它在拼命拉住你,不让你跳下悬崖。"
磁盘水位触发只读(read-only index)
-
💥 现象:突然无法写入,报错
cluster_block_exception: index read-only{
"error": {
"type": "cluster_block_exception",
"reason": "blocked by: [FORBIDDEN/12/index read-only / allow delete (api)];"
}
} -
Kibana 能查历史数据,但无法写入新日志
GET _cluster/allocation/explain 显示:
"explanations": ["the node is above the high watermark cluster setting [95.0%]"]
这时候我就慌了,刚开始的时候"unlock unlock"快快快,结果自挂东南枝。
假设你执行
PUT _all/_settings { "index.blocks.read_only": false }
- 写入恢复 → 新数据继续写入
- 磁盘使用率从 95% → 96% → 97%...
- 很快再次触发 flood_stage → 又被锁
- 如果磁盘真的写满(100%):
- Lucene 无法写入新的
.cfs文件 - Translog 无法刷盘
- 节点崩溃,分片损坏,数据丢失
- Lucene 无法写入新的
不看远的,咱就看我是如何年少无知,看问题只看表面:unlock 后未清理数据,2 小时后磁盘 100%,3 个节点宕机,丢失 12 小时日志。所以不能这么干!
so原因是什么
ES 通过 三个水位阈值 保护集群:节点磁盘使用率
| 水位 | 默认值 | 行为 |
|---|---|---|
| low | 85% | 停止往该节点分配新分片 |
| high | 90% | 将现有分片迁出该节点 |
| flood_stage | 95% | 强制索引只读(防止写爆磁盘)cluster.routing.allocation.disk.watermark.high |
当flood_stage阶段,ES大大会认为:"不能再写下去了呀,再写下去磁盘会满,Lucene 无法 commit,数据可能永久损坏"呀,呀仔;
正确处理流程:四步安全法
先腾空间,再 unlock
1、评估磁盘使用情况
# 查看各节点磁盘使用率
GET _cat/allocation?v
# 查看具体节点磁盘
GET _nodes/stats/fs?pretty
#输出示例:
shards disk.indices disk.used disk.avail disk.total disk.percent host
6 450gb 950gb 50gb 1000gb 95 10.0.0.1
2、立刻 马上 刻不容缓释放空间
- 删旧索引:# 删除 30 天前的日志 DELETE logstash-2024-04-*
- 强行合并,减少segment数量 #只对只读索引操作! POST old_index/_forcemerge?max_num_segments=1
- (云环境)临时扩容: AWS EBS / 阿里云云盘 → 在线扩容,重启ES(除非lvm)
3、确认磁盘<95%
# 等待磁盘降到 94% 以下
GET _cat/allocation?v
4、安全解除只读
PUT _all/_settings
{
"index.blocks.read_only": false
}
✅ 此时解锁才是安全的!
长期防护:三道防线
为人还是要长久计:又又又见到了老朋友
防线 1:调整水位阈值(根据磁盘大小)
# elasticsearch.yml
cluster.routing.allocation.disk.watermark.low: 80%
cluster.routing.allocation.disk.watermark.high: 85%
cluster.routing.allocation.disk.watermark.flood_stage: 90%
💡 聪明的你隐约看出了些许规则:
- 磁盘越大,水位可设越高(如 10TB → 90%/95%/98%)
- 磁盘越小,水位要越保守(如 500GB → 70%/80%/85%)
防线 2:启用 ILM(自动生命周期管理)
PUT _ilm/policy/logs_delete_after_30d
{
"policy": {
"phases": {
"hot": { "actions": { "rollover": { "max_age": "1d" } } },
"delete": {
"min_age": "30d",
"actions": { "delete": {} }
}
}
}
}
防线 3:监控告警(提前干预)
- Kibana 告警规则 :
- 条件:
disk.percent > 80% - 动作:钉钉通知 SRE
- 条件:
- 指标 :
node.fs.disk.used_percentindices.fielddata.memory_size
- 😅 "ES 不是不想写,是怕你把磁盘写爆后连日志都救不回来。" 这岂不是大不妙
老规矩,自动化工具少不了
一键清理旧索引 Shell 脚本(安全 + 可审计)
#!/bin/bash
set -euo pipefail # 任一命令失败立即退出,未定义变量报错
# =============================================================================
# 安全清理 Elasticsearch 旧索引脚本
# 功能:
# - 自动识别形如 logstash-YYYY.MM.DD 的索引
# - 保留最近 N 天,删除更早的
# - 支持 dry-run 预览,防止误删
# 适用场景:日志、指标等时序数据
# 安全原则:绝不删除无日期后缀的索引(如 .kibana, security-*)
# =============================================================================
# ========================
# 配置区(按需修改)
# ========================
ES_HOST="http://localhost:9200"
USERNAME="" # 如启用安全,填用户名
PASSWORD="" # 密码
INDEX_PREFIX="logstash-,filebeat-" # 要清理的索引前缀:多个前缀用逗号分隔
RETAIN_DAYS=30 # 保留最近 N 天,至少保留 7 天,合规要求可能需 30/90/180 天
DRY_RUN=true # true = 只打印要删的索引;false = 真删,上线前务必先 dry-run
LOG_FILE="/var/log/es_cleanup_$(date +%Y%m%d).log"# 日志文件路径(用于审计)
# ========================
# 工具函数
# ========================
log() { #统一日志格式:[时间] 消息,并同时输出到终端和日志文件
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# 安全封装 curl,自动处理认证
es_curl() {# 用法: es_curl GET "http://es/_cat/indices"
local method="$1"; shift
if [[ -n "$USERNAME" ]]; then
curl -s -u "$USERNAME:$PASSWORD" -X"$method" "$@"
else
curl -s -X"$method" "$@"
fi
}
# ========================
# 主流程
# ========================
log "开始清理旧索引(保留 $RETAIN_DAYS 天)"
# 1. 计算 cutoff 日期(格式:YYYY.MM.DD)
# 使用 GNU date(Linux);macOS 用户需安装 gdate 或调整!!!
CUTOFF_DATE=$(date -d "$RETAIN_DAYS days ago" +"%Y.%m.%d")
log " 保留日期 >= $CUTOFF_DATE的索引"
# 2. 获取所有匹配前缀的索引 例如: logstash-2026.01.01, filebeat-8.12.0-2026.01.02
ALL_INDICES=""
for prefix in ${INDEX_PREFIX//,/ }; do
# 调用 _cat/indices API,提取第三列(索引名)
# awk '{print $3}' 只取索引名
indices=$(es_curl GET "$ES_HOST/_cat/indices/${prefix}*" | awk '{print $3}' | sort)
if [[ -n "$indices" ]]; then
ALL_INDICES="$ALL_INDICES $indices"
log "找到前缀 '$prefix' 的索引: $(echo $indices | wc -w) 个"
fi
done
if [[ -z "$ALL_INDICES" ]]; then
log " 未找到匹配前缀的索引"
exit 0
fi
# 3. 筛选出过期索引
EXPIRED_INDICES=""
for index in $ALL_INDICES; do
# 正则匹配日期:2026.02.01
if [[ $index =~ [0-9]{4}\.[0-9]{2}\.[0-9]{2} ]]; then
idx_date="${BASH_REMATCH[0]}"
# 字符串比较(YYYY.MM.DD 是字典序安全的)
if [[ "$idx_date" < "$CUTOFF_DATE" ]]; then
EXPIRED_INDICES="$EXPIRED_INDICES $index"
fi
else
# 跳过无日期的索引(如 .kibana, .security)
log " 跳过无日期格式的索引: $index"
fi
done
# 若无过期索引,退出
if [[ -z "$EXPIRED_INDICES" ]]; then
log " 无过期索引需要清理,退出。"
exit 0
fi
# 打印待删除列表
log "发现 $(echo $EXPIRED_INDICES | wc -w) 个过期索引:"
for idx in $EXPIRED_INDICES; do
log " - $idx"
done
# 执行删除(或 dry-run)
if [[ "$DRY_RUN" == "true" ]]; then
log " DRY RUN 模式:未执行删除!"
log " 提示:编辑脚本,将 DRY_RUN=false 后再运行以真实删除。"
exit 0
fi
# 危险操作:开始真实删除
log " 执行真实删除(DRY_RUN=false)..."
for idx in $EXPIRED_INDICES; do
log " 正在删除索引: $idx"
response=$(es_curl DELETE "$ES_HOST/$idx")
# 检查响应是否包含 "acknowledged":true
if echo "$response" | grep -q '"acknowledged":true'; then
log " 成功删除: $idx"
else
log " 删除失败: $idx → 响应: $response"
# 不退出,继续尝试其他索引
fi
done
log "清理任务完成!共处理 $(echo $EXPIRED_INDICES | wc -w) 个索引。"
log "=== 任务结束 ==="
# 1. 先 dry-run 查看会删哪些
chmod +x cleanup_old_indices.sh
./cleanup_old_indices.sh
# 2. 确认无误后,编辑脚本:DRY_RUN=false
# 3. 加入 crontab(每天凌晨 2 点)
0 2 * * * /path/to/cleanup_old_indices.sh
工具 2:ILM(索引生命周期管理)策略生成器
#!/usr/bin/env python3
"""
自动生成 ILM 策略:hot → delete
支持按天 rollover,N 天后删除
使用示例:
python generate_ilm_policy.py --name logs-prod --delete-days 30
"""
import argparse
import json
import sys
def generate_ilm_policy(policy_name: str, rollover_days: int, delete_after_days: int):
policy = {
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_age": f"{rollover_days}d" # 滚动条件:索引年龄 > N 天
}
}
},
"delete": {
"min_age": f"{delete_after_days}d",# 进入 delete 阶段的条件
"actions": {
"delete": {}#删除
}
}
}
}
}
# 创建策略
print(f"PUT _ilm/policy/{policy_name}")
print(json.dumps(policy, indent=2, ensure_ascii=False))
# 绑定到索引模板
template = {
"index_patterns": [f"{policy_name}-*"],
"template": {
"settings": {
"number_of_shards": 3,# 分片数(根据数据量调整)
"number_of_replicas": 1, # 副本数(高可用必需)
"index.lifecycle.name": policy_name,# 绑定 ILM 策略
"index.lifecycle.rollover_alias": policy_name# 滚动别名
}
}
}
print(f"\n# 应用到索引模板")
print(f"PUT _index_template/{policy_name}_template")
print(json.dumps(template, indent=2, ensure_ascii=False))
# 创建初始索引:必须
print(f"\n# 创建初始写入索引")
print(f"PUT {policy_name}-000001")
print(json.dumps({
"aliases": {
policy_name: {"is_write_index": True}
}
}, indent=2))
def main():
parser = argparse.ArgumentParser(
description="生成 Elasticsearch ILM 策略(自动滚动 + 自动删除)",
epilog="示例: python generate_ilm_policy.py --name logs-prod --delete-days 30"
)
parser.add_argument(
"--name",
required=True,
help="策略名称,也将作为写入别名(如 logs-prod, metrics-app)"
)
parser.add_argument(
"--rollover-days",
type=int,
default=1,
help="多少天滚动一次新索引(默认 1 天)"
)
parser.add_argument(
"--delete-days",
type=int,
required=True,
help="多少天后自动删除索引(必须指定,如 30)"
)
args = parser.parse_args()
# 安全校验
if args.delete_days <= args.rollover_days:
print(" 错误: delete-days 必须大于 rollover-days!", file=sys.stderr)
sys.exit(1)
generate_ilm_policy(args.name, args.rollover_days, args.delete_days)
if __name__ == "__main__":
main()
# 生成一个"每天滚动,30天后删除"的策略
python generate_ilm_policy.py --name logs-prod --delete-days 30
# 输出:
PUT _ilm/policy/logs-prod
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_age": "1d"
}
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}
# 应用到索引模板
PUT _index_template/logs-prod_template
...
# 创建初始写入索引
PUT logs-prod-000001
...
工具 3:Kibana 磁盘水位告警规则(JSON 模板)
适用于 Elastic Stack 7.10+ / 8.x ,基于 Stack Monitoring 数据
- 触发条件:任一节点磁盘使用率 ≥ 80%
- 检查频率:每 5 分钟
- 恢复条件:所有节点 < 75%
- 通知方式:Webhook(可接钉钉/企业微信/Slack)
导入方式:
-
进入 Stack Management → Rules and connectors
-
点击 "Create rule" → "Import"
-
粘贴下方 JSON
{
"name": "ES Disk Usage High (>=80%)",
"tags": ["elasticsearch", "disk", "capacity"],
"consumer": "alerts",
"enabled": true,
"throttle": null,
"schedule": {
"interval": "5m"
},
"params": {
"indices": [".monitoring-es-*"],
"query": {
"bool": {
"must": [
{ "term": { "type": "node_stats" } },
{ "range": { "node.fs.total.available_ratio": { "lte": 0.2 } } }
]
}
},
"size": 100,
"timeField": "timestamp",
"timeWindowSize": 5,
"timeWindowUnit": "m"
},
"rule_type_id": ".es-query",
"actions": [
{
"id": "your-webhook-connector-id", // ← 替换为你的 Webhook ID
"group": "default",
"params": {
"message": "Elasticsearch 磁盘告警\n节点: {{context.hits.0._source.node.name}}\n磁盘使用率: {{#math}}100 - ({{context.hits.0._source.node.fs.total.available_ratio}} * 100){{/math}}%\n总空间: {{context.hits.0._source.node.fs.total.total_in_bytes}} bytes\n可用空间: {{context.hits.0._source.node.fs.total.available_in_bytes}} bytes\n\n请立即处理!"
}
}
],
"notify_when": "onActionGroupChange",
"mute_all": false
}
| 场景 | 方案 |
|---|---|
| 已有大量历史索引 | 先用 cleanup_old_indices.sh 清理一次 |
| 新业务上线 | 用 generate_ilm_policy.py 配置 ILM |
| 混合场景 | 清理旧数据 + 新数据走 ILM |