Es之只读

"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 }
  1. 写入恢复 → 新数据继续写入
  2. 磁盘使用率从 95% → 96% → 97%...
  3. 很快再次触发 flood_stage → 又被锁
  4. 如果磁盘真的写满(100%):
    • Lucene 无法写入新的 .cfs 文件
    • Translog 无法刷盘
    • 节点崩溃,分片损坏,数据丢失

不看远的,咱就看我是如何年少无知,看问题只看表面: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_percent
    • indices.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)

导入方式:

  1. 进入 Stack Management → Rules and connectors

  2. 点击 "Create rule" → "Import"

  3. 粘贴下方 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
相关推荐
云边有个稻草人19 小时前
关系数据库替换用金仓:数据迁移过程中的完整性与一致性风险
数据库·国产数据库·kingbasees·金仓数据库·关系数据库替换用金仓
Tangcan-19 小时前
【Redis】通用命令 1
数据库·redis·缓存
MSTcheng.19 小时前
【C++】C++异常
java·数据库·c++·异常
草莓熊Lotso20 小时前
Linux 文件描述符与重定向实战:从原理到 minishell 实现
android·linux·运维·服务器·数据库·c++·人工智能
大模型玩家七七20 小时前
基于语义切分 vs 基于结构切分的实际差异
java·开发语言·数据库·安全·batch
岳麓丹枫00121 小时前
PostgreSQL 中 pg_wal 目录里的 .ready .done .history 文件的生命周期
数据库·postgresql
陌上丨1 天前
Redis的Key和Value的设计原则有哪些?
数据库·redis·缓存
AI_56781 天前
AWS EC2新手入门:6步带你从零启动实例
大数据·数据库·人工智能·机器学习·aws
ccecw1 天前
Mysql ONLY_FULL_GROUP_BY模式详解、group by非查询字段报错
数据库·mysql