news 2026/10/19 2:49:35

Shell脚本安全清理Elasticsearch索引与过期数据实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Shell脚本安全清理Elasticsearch索引与过期数据实战

1. 场景与核心痛点

先说个真实场景,我估摸着不少同行的运维群里都出现过类似求助:ES集群健康状态变黄了,磁盘使用率飙到85%以上,kibana上一查才发现是某个业务索引疯狂增长,或者历史日志索引堆积太多。有人下意识就想登录服务器敲个DELETE /index试试,结果发现生产环境的ES没那么好说话——索引可能被保护、可能正在写入、可能有别名依赖、可能删错了索引整个部门来追杀你。于是“用shell删除ES数据”这件看起来简单的事,实际上牵扯到索引策略、数据安全、集群状态、调用方式、脚本健壮性一大堆细节。

这篇博文就把我实际做过的东西完整整理出来:从最基础的curl调用,到可复用的shell脚本模板,再到条件删除、日期范围清理、索引匹配规则,最后是执行时的坑和排查思路。适合谁看?第一种是刚接手ES运维、需要在测试环境清理数据的初级工程师;第二种是日志型ES集群长期被磁盘打满、需要定期回滚清理数据的后端开发;第三种是写自动化运维脚本、想把数据清理纳入定时任务体系的DevOps。看完你至少能自己写出一个不丢数据、不怕重复执行、留了日志和安全阀门的shell脚本。

提示:这篇文章里的脚本我只保证在Linux环境、Bash 4.x以上版本、配合curl 7.x测试通过。Windows环境想跑的话建议用Git Bash,但有些命令(比如date -d)需要调整,我就不展开说了。

2. 删除ES数据的思路拆解:先搞清楚你到底想删什么

2.1 ES删除数据的四种典型场景

写脚本第一步不是打开编辑器,而是搞明白你的需求属于哪一类。我把它归纳成四种:

场景编号需求描述推荐实现方式风险等级
场景A删除整个索引(比如测试索引重置)DELETE /index_name较低,但不可逆
场景B按时间删除文档(比如只保留最近15天日志)_delete_by_query+ 日期条件中高,需控制节点压力
场景C批量清理符合前缀规则的索引(比如app-log-2024.11.*)通配符 + curl循环中高,匹配错误会误删
场景D清空索引数据但保留索引结构POST /index/_delete_by_query匹配全部,或_reset相关方式中,看业务是否允许

很多新手最大的误区是把场景B和场景C混为一谈。删整个索引和删文档是两条完全不同的技术路线:索引删了是物理删除+释放磁盘空间,文档删了只标记删除段,后面的segment merge才会真正释放空间。你如果只想清理旧数据,结果图省事直接删了整个索引,下游的Kafka消费位点、日志采集配置、Index Pattern可能全部失效。同样的,如果只是磁盘告急想快速腾空间,但你用的是_delete_by_query而不是删索引,你会发现磁盘空间没有立刻下降,因为Lucene的段合并还没跑完。

2.2 为什么用Shell封装而不直接在Kibana控制台操作

Kibana的Dev Tools里拼一条DELETE /xxx太方便了,为什么还要用shell?我自己的体会是三个字:自动化。Kibana解决的是“我现在删一次”,shell解决的是“每天自动删一次”。日志型索引、线上数据流、业务临时表,它们的生命周期管理需要固化成一个稳定入口——比如接入crontab,比如在发布脚本里触发清理,甚至交接给不太懂ES的运维同事执行。一套带传参、带日志、带安全确认的shell脚本,比让同事打开Kibana敲DSL要稳妥得多。

另外shell脚本天然方便做“批量”和“条件判断”。你要清理30个按日分片的历史索引,手敲30条DELETE不现实;这次磁盘还剩多少、该删哪些日期的、删除前要不要确认,这些逻辑用脚本能写清楚,而且执行记录能留痕。ES本身有ILM(索引生命周期管理),但它只能管“索引策略”,管不了“今天磁盘突然告警需要临时再删一批”这种临时需求。

2.3 安全第一:删除操作必须做前置条件断言

我见过不止一次出事的起因都是“我以为脚本里写了保护”。给你看个事故现场:某同学写了个清理脚本,逻辑大概是curl -XDELETE "http://ip:9200/${INDEX_PREFIX}*",前缀变量是从配置文件里读的,结果配置文件被改成了空字符串,实际执行变成了DELETE /*,整个集群的索引全被干掉了。这不是段子,是真实发生过的惨案。所以我在设计脚本时给自己立了几个死规矩:

  • 任何批量匹配删除前,先做dry-run打印要删的索引列表,确认无误后加--confirm参数才真正执行;
  • 禁止直接使用空变量拼URL,如果前缀变量为空,脚本直接报错退出;
  • 删除操作默认只针对索引级或文档级,凡涉及通配符*的地方必须二次授权;
  • 所有执行记录、索引清单、删除结果都写入本地日志文件,方便回溯。

这套逻辑后面每一节都会体现,先记住一个核心思想:shell删除ES数据的核心风险不是ES难连,而是人容易犯错,脚本的最大价值是给“手滑”兜底。

3. 核心细节解析:从curl基础到脚本参数设计

3.1 最基础的curl调用方式,以及为什么经常失败

不管多复杂的shell脚本,根子上都离不开一句curl请求。先看最基础的三种调用:

# 方式一:直接请求REST API curl -X DELETE "http://127.0.0.1:9200/my_index" # 方式二:带认证信息的请求 curl -X DELETE -u elastic:password "https://127.0.0.1:9200/my_index" # 方式三:带请求头并显示HTTP状态码 curl -X DELETE -H "Content-Type: application/json" -u elastic:password -w "\nHTTP_CODE:%{http_code}\n" "https://127.0.0.1:9200/my_index"

我建议所有脚本都采用方式三的写法,大部分“脚本看起来没执行成功”的根源在于:curl在连接失败、证书错误、HTTP状态码异常时返回码是0,脚本没有感知到任何错误,照样继续往下跑。加上-w把HTTP CODE打出来,至少你在日志里能看到返回是200还是404还是403。

还有两个常见失败点。第一是ES默认开启安全认证后,不带-u会直接返回401,但这个错误信息在-s静默模式下会被吞掉,你会看到脚本“执行了”但其实什么都没发生。第二是ES集群如果配置了HTTPS,直接用http://访问会被拒,这种情况必须加--insecure或配置CA证书。注意,内网环境测试我建议先手动跑一次原始curl命令,确认能通再写进脚本,否则你根本分不清是ES拒绝了还是脚本逻辑错了。

3.2 环境变量与可配置项:不要硬编码任何东西

ES地址、索引前缀、认证信息、保留天数、删除策略,这些全部应该抽成脚本头部变量或者环境变量。硬编码的坏处不需要我多讲,最现实的问题是:换一台机器、换一个集群、改一个密码,你就得去脚本里搜索替换,而且很容易漏。下面是一份我常用的变量区域设计:

#!/bin/bash # ===== 可配置参数区 ===== ES_HOST="${ES_HOST:-https://127.0.0.1:9200}" ES_USER="${ES_USER:-elastic}" ES_PASS="${ES_PASS:-changeme}" INDEX_PREFIX="${INDEX_PREFIX:-app-log}" # 索引前缀,必填,禁止为空 RETAIN_DAYS="${RETAIN_DAYS:-15}" # 保留天数,默认15天 BATCH_LIMIT="${BATCH_LIMIT:-500}" # 每次批量删除的索引数量上限 CURL_TIMEOUT="${CURL_TIMEOUT:-60}" # curl超时时间,单位秒 LOG_FILE="${LOG_FILE:-/var/log/es_clean_$(date +\%Y\%m).log}"

特意说一下CURL_TIMEOUT这个变量。批量删除几十个索引的时候,单个DELETE操作可能因为段合并、集群负载等原因耗时较长。假设你curl默认不设超时,脚本可能长时间挂住;设太短又会导致明明删到了但curl认为超时,返回非零退出码。我的经验值是删除单索引给60秒,_delete_by_query给300秒以上,后面会讲到为什么。

另外密码这类敏感信息,建议不要直接写在脚本里,可以用环境变量传入,脚本内再校验是否为空:

if [ -z "$ES_PASS" ]; then echo "[ERROR] ES_PASS 环境变量未设置,拒绝执行" exit 1 fi

这虽然多了两步,但能避免你把密码提交到Git仓库之后满大街找怎么改历史记录。

3.3 安全确认机制:dry-run、二次确认、执行锁

我做的每一个清理脚本都带三重安全机制,你可以按需取舍,但建议至少保留第一重。

第一重是dry-run模式。脚本默认不真正删除,只打印将被删除的索引清单和影响文档数,只有当你显式加--confirm才真正执行。

DRY_RUN=true if [[ "$1" == "--confirm" ]]; then DRY_RUN=false fi

第二重是删除前二次确认。真实执行时,脚本会把每条要被删除的索引打印出来,然后要求你输入YES才能继续。这个动作在手动执行时有点烦,但确实能拦下不少错误操作,配合定时任务时可以用一个FORCE=1环境变量跳过交互。

第三重是执行锁。批量清理任务如果被crontab配置成了每隔5分钟跑一次,而上次删除还没跑完,两个进程同时执行会冲突。最朴素的实现是用PID文件:

LOCK_FILE="/tmp/es_clean.lock" if [ -f "$LOCK_FILE" ] && kill -0 "$(cat "$LOCK_FILE")" 2>/dev/null; then echo "[ERROR] 已有清理任务在运行,PID: $(cat "$LOCK_FILE")" exit 2 fi echo $$ > "$LOCK_FILE" trap 'rm -f "$LOCK_FILE"' EXIT

这段脚本的意义在于:forgive me for being blunt,锁文件不清理的话下次任务永远跑不了,所以trap必须写在注册锁之后,确保脚本退出时自动解除锁。

4. 实操过程:四套实战shell脚本模板与完整解析

4.1 模板一:按索引名精确删除单索引

最基础的需求:我知道有一个索引叫app-log-2025.01.10,现在要删掉它。适合测试环境、一次性手动操作。完整脚本如下:

#!/bin/bash # 用途:精确删除单个ES索引 # 用法:./delete_single_index.sh <index_name> set -euo pipefail ES_HOST="${ES_HOST:-http://127.0.0.1:9200}" ES_USER="${ES_USER:-}" ES_PASS="${ES_PASS:-}" INDEX_NAME="${1:-}" if [ -z "$INDEX_NAME" ]; then echo "用法: $0 <index_name>" exit 1 fi # 删除前先检查索引是否存在 AUTH_ARGS=() [ -n "$ES_USER" ] && AUTH_ARGS=(-u "$ES_USER:$ES_PASS") HTTP_CODE=$(curl -s "${AUTH_ARGS[@]}" -o /dev/null -w "%{http_code}" \ -X HEAD "http://${ES_HOST}/_cat/indices/${INDEX_NAME}" 2>/dev/null || echo "000") if [ "$HTTP_CODE" == "000" ]; then echo "[ERROR] 无法连接ES,请检查网络或地址配置" exit 3 fi # 直接发送删除请求 RESPONSE=$(curl -s "${AUTH_ARGS[@]}" -w "\nHTTP_CODE:%{http_code}" \ -X DELETE "http://${ES_HOST}/${INDEX_NAME}") echo "$RESPONSE" # 删除后验证 VERIFY_CODE=$(curl -s "${AUTH_ARGS[@]}" -o /dev/null -w "%{http_code}" \ -X HEAD "http://${ES_HOST}/${INDEX_NAME}") echo "验证结果:索引删除后HEAD返回码为 $VERIFY_CODE"

这里有几个细节值得展开:

第一,为什么先发一个HEAD请求而不是直接DELETE?因为ES对不存在的索引执行DELETE时有两种情况:配置了action.destructive_requires_name=true(ES7.0后默认开启)时不存在的索引返回404,但也可能因为通配符规则被拒。先检查存在性,能提前暴露网络或连接问题,也能避免后续误判“删除失败”。

第二,set -euo pipefail这三兄弟建议写进所有脚本。-e遇到错误立即退出,-u变量未定义时报错,pipefail管道中任何一环失败都视为失败。少了这三个,curl连接失败时脚本照样给0退出码继续往下走,非常坑。

第三,注意ES 7.0以后的安全变化:如果集群启用了安全特性,DELETE操作需要manage权限。这也是为什么我在脚本里把用户名密码做成了变量而不是写死,因为不同环境权限不同。

4.2 模板二:按日期批量删除历史索引

日志类ES集群最常干的事就是清理过期索引。索引常规命名是app-log-2025.01.01这种带日期后缀的格式。按日期批量删除的核心逻辑是:先算出截止日期,再遍历匹配到的索引列表,把日期部分早于阈值的挑出来删。

#!/bin/bash # 用途:按日期删除历史索引,保留最近N天 # 用法:./clean_by_days.sh --prefix=app-log --retain-days=15 --confirm set -euo pipefail ES_HOST="${ES_HOST:-http://127.0.0.1:9200}" ES_USER="${ES_USER:-}" ES_PASS="${ES_PASS:-}" # 从命令行参数解析 for arg in "$@"; do case "$arg" in --prefix=*) INDEX_PREFIX="${arg#*=}" ;; --retain-days=*) RETAIN_DAYS="${arg#*=}" ;; --confirm) DRY_RUN=false ;; *) echo "忽略未知参数: $arg" ;; esac done # 参数校验 if [ -z "${INDEX_PREFIX:-}" ]; then echo "[ERROR] 必须指定 --prefix 参数" exit 1 fi DRY_RUN="${DRY_RUN:-true}" # 计算截止日期,格式为YYYY.MM.DD CUTOFF_DATE=$(date -d "-${RETAIN_DAYS} days" +%Y.%m.%d) echo "[INFO] 截止日期: $CUTOFF_DATE,保留最近 ${RETAIN_DAYS} 天索引" # 查询所有匹配前缀的索引 AUTH_ARGS=() [ -n "$ES_USER" ] && AUTH_ARGS=(-u "$ES_USER:$ES_PASS") INDICES=$(curl -s "${AUTH_ARGS[@]}" "http://${ES_HOST}/_cat/indices/${INDEX_PREFIX}*?h=index&format=text" || true) if [ -z "$INDICES" ]; then echo "[INFO] 没有匹配到任何索引,退出" exit 0 fi echo "=== 待筛选索引列表 ===" echo "$INDICES" # 遍历索引,找出日期小于截止日期的 for idx in $INDICES; do # 提取最后一段日期,格式为 YYYY.MM.DD DATE_PART=$(echo "$idx" | grep -oE '[0-9]{4}\.[0-9]{2}\.[0-9]{2}$' || true) if [ -z "$DATE_PART" ]; then echo "[WARN] 索引 $idx 无法解析日期,跳过" continue fi # 字符串比较日期大小,YYYY.MM.DD格式可直接比较 if [[ "$DATE_PART" < "$CUTOFF_DATE" ]]; then if [ "$DRY_RUN" == "true" ]; then echo "[DRY-RUN] 将删除索引: $idx (日期: $DATE_PART)" else echo "[DELETE] 正在删除索引: $idx" curl -s "${AUTH_ARGS[@]}" -X DELETE "http://${ES_HOST}/${idx}" echo "" fi else echo "[SKIP] 保留索引: $idx (日期: $DATE_PART)" fi done if [ "$DRY_RUN" == "true" ]; then echo "" echo "[INFO] 以上为试运行结果,如需真正执行请追加 --confirm 参数" fi

每段逻辑我拆开讲:

日期解析为什么要用grep -oE '[0-9]{4}\.[0-9]{2}\.[0-9]{2}$'?因为索引前缀可能本身含数字,比如user-2025-log-2025.01.01,直接用cut -d. -f3截取会出错。用正则匹配末尾的日期串更可靠,匹配不到说明索引命名不规范,跳过而不是误删。

日期比较为什么直接字符串比较?因为YYYY.MM.DD这种格式的字符串字典序就是时间顺序,2024.12.31 < 2025.01.01为true,这比转换成时间戳再比较更简单也更安全。但前提是你必须保证日期格式固定是两位数月份、两位数日期,如果哪天有人创建了2025.1.1这种索引,比较就会出错。

这里还有一个性能隐患:curl -s ... _cat/indices/${INDEX_PREFIX}*一次性拿回所有索引名,如果索引有上万个,输出会非常大。这时候建议分页或者用ES的索引过滤能力,不过对绝大多数中小集群来说够用了。

4.3 模板三:条件删除索引内文档(_delete_by_query实战)

有些场景不能删索引,只能删文档。比如你有个业务索引存了半年的数据,但结构还要继续用,或者索引和Kafka同步位点绑定不能重建。这种情况要用_delete_by_query。

#!/bin/bash # 用途:按时间字段删除索引内过期文档 # 依赖:索引体内存在时间字段,比如 @timestamp 或 log_date set -euo pipefail ES_HOST="${ES_HOST:-http://127.0.0.1:9200}" ES_USER="${ES_USER:-elastic}" ES_PASS="${ES_PASS:-changeme}" INDEX_NAME="${1:-app-log}" TIMESTAMP_FIELD="${2:-@timestamp}" RETAIN_DAYS="${3:-15}" CUTOFF_EPOCH=$(date -d "-${RETAIN_DAYS} days" +%s) CUTOFF_TIME=$(date -d @"${CUTOFF_EPOCH}" +%Y-%m-%dT%H:%M:%S.%3NZ) echo "[INFO] 准备删除 ${INDEX_NAME} 中 ${TIMESTAMP_FIELD} < ${CUTOFF_TIME} 的文档" # 构造删除查询体 QUERY_BODY=$(cat <<EOF { "query": { "range": { "${TIMESTAMP_FIELD}": { "lt": "${CUTOFF_TIME}", "format": "strict_date_optional_time_nanos" } } } } EOF ) # 执行删除 RESPONSE=$(curl -s -u "${ES_USER}:${ES_PASS}" -X POST \ "http://${ES_HOST}/${INDEX_NAME}/_delete_by_query?wait_for_completion=false&conflicts=proceed&scroll_size=5000" \ -H "Content-Type: application/json" \ -d "$QUERY_BODY") echo "$RESPONSE" | tee -a "${LOG_FILE}"

这段脚本有几个设计亮点和必须注意的坑:

wait_for_completion=false是什么意思?_delete_by_query默认同步等待执行完毕,索引大、数据多时这个HTTP请求会挂很久,极易超时。设置为false后,ES会立刻返回一个taskId,删除在后台运行。但这样的话,脚本拿不到“最终结果”,只能拿到“任务已提交”,所以你后面还要轮询task状态。我一般写一个配套check脚本:

TASK_ID=$(echo "$RESPONSE" | grep -oE '"task":"[^"]*"' | cut -d'"' -f4) # 轮询任务状态 while true; do TASK_STATUS=$(curl -s -u "$ES_USER:$ES_PASS" "http://${ES_HOST}/_tasks/${TASK_ID}?pretty") COMPLETED=$(echo "$TASK_STATUS" | grep -oE '"completed":true' || true) if [ -n "$COMPLETED" ]; then echo "任务完成" break fi echo "[INFO] 任务 ${TASK_ID} 仍在执行,10秒后重试..." sleep 10 done

conflicts=proceed这个参数解决的是版本冲突问题。多个副本同时更新文档时,删除操作可能遇到版本冲突,默认行为是中止任务,设置成proceed会跳过冲突文档继续删除。业务上有实时写入的索引,这个参数建议加上,不然任务很容易半途失败。

scroll_size=5000是每批处理的文档数。这个值不是越大越好,太大容易触发大内存请求,太小又会增加任务时长。5k是我在多个集群上试下来比较稳的值,如果你机器内存小可以降到1000,但要注意任务总时长可能变长好几倍。

还有个大坑:_delete_by_query使用的是ES查询上下文,如果索引映射里没有你指定的时间字段,或者字段是keyword类型而不是date类型,range查询会直接报错。所以执行前一定要先确认:

curl -s -u "$ES_USER:$ES_PASS" "http://${ES_HOST}/${INDEX_NAME}/_mapping" | grep -A5 "$TIMESTAMP_FIELD"

4.4 模板四:通配符批量清理(含二次确认保护)

前面说过的“删前缀匹配所有索引”是最高危的操作,我把带保护逻辑的完整版贴出来,你能直接改成自己的场景。

#!/bin/bash # 用途:按前缀批量清理索引,支持排除列表 # 用法:./clean_by_prefix.sh --prefix=tmp-test --exclude=tmp-test-keep --confirm set -euo pipefail ES_HOST="${ES_HOST:-http://127.0.0.1:9200}" ES_USER="${ES_USER:-}" ES_PASS="${ES_PASS:-}" for arg in "$@"; do case "$arg" in --prefix=*) INDEX_PREFIX="${arg#*=}" ;; --exclude=*) EXCLUDE_PATTERN="${arg#*=}" ;; --confirm) DRY_RUN=false ;; esac done # 安全断言:前缀不允许为空 if [[ -z "${INDEX_PREFIX:-}" ]]; then echo "[ERROR] 前缀不能为空,禁止直接操作全索引" exit 1 fi # 安全断言:前缀不能是通配符本身 if [[ "$INDEX_PREFIX" == "*" ]]; then echo "[ERROR] 禁止删除全量索引,如果需要清空请考虑关闭索引后操作" exit 1 fi AUTH_ARGS=() [ -n "$ES_USER" ] && AUTH_ARGS=(-u "$ES_USER:$ES_PASS") INDICES=$(curl -s "${AUTH_ARGS[@]}" "http://${ES_HOST}/_cat/indices/${INDEX_PREFIX}*?h=index&format=text" || true) if [ -z "$INDICES" ]; then echo "[INFO] 没有匹配到的索引,退出" exit 0 fi # 过滤排除项 if [ -n "${EXCLUDE_PATTERN:-}" ]; then INDICES=$(echo "$INDICES" | grep -v "$EXCLUDE_PATTERN" || true) fi COUNT=$(echo "$INDICES" | grep -c . || true) echo "即将删除索引数量: $COUNT" echo "=== 索引清单 ===" echo "$INDICES" if [ "${DRY_RUN:-true}" == "true" ]; then echo "[DRY-RUN] 添加 --confirm 参数以真正执行" exit 0 fi # 二次确认 if [ "${FORCE:-0}" != "1" ]; then read -p "确认删除以上 $COUNT 个索引?输入 YES 继续: " CONFIRM if [ "$CONFIRM" != "YES" ]; then echo "用户取消操作" exit 0 fi fi # 真正执行 for idx in $INDICES; do echo "[DELETE] $idx" curl -s "${AUTH_ARGS[@]}" -X DELETE "http://${ES_HOST}/${idx}" echo "" done

这套模板的核心价值不在删除,而在保护。

--exclude排除参数很实用,比如你想清理app-log-*但保留app-log-prod-*,一个正则就能解决。我实际用下来发现这个参数至少救过我两次,尤其是有些索引名看起来像日志前缀,但其实是核心业务数据。

二次确认设计成必须输入大写的YES而不是回车确认,是故意的。回车太容易误触了,脑子里想着事情手上就按了回车,等你反应过来索引已经没了。强制输入完整单词,至少能强迫你读一遍屏幕上的索引列表。

FORCE=1环境变量是在定时任务场景下跳过交互用的,但它的存在默认是禁止的,只有手动设置才生效。注意脚本里没有写死这个判断值,而是通过环境变量控制,因为crontab里的环境变量默认是没有的,误触概率极低。

5. 细节避坑与运维经验:脚本跑通只是开始

5.1 索引删了磁盘空间为什么没降

这是同类操作里被问得最多的一个问题。原因在Lucene的存储机制:ES的文档不是立即物理删除,而是标记删除。当删除量达到一定阈值,后台segment merge会创建新段、丢掉旧段,之后才会真正释放磁盘。所以DELETE索引后空间一般立刻释放(因为整个索引目录都被移除了),但_delete_by_query删文档后空间不会立刻下降。

如果你用_delete_by_query清理完数据发现磁盘没变,不要慌,触发一下段合并:

curl -s -u "$ES_USER:$ES_PASS" -X POST "http://${ES_HOST}/${INDEX_NAME}/_forcemerge?max_num_segments=1"

注意这是一个高资源消耗操作,尽量避免在业务高峰期执行,否则CPU、IO都会被打满。merge期间查询性能也会受到影响,建议安排在凌晨窗口。

5.2 curl返回000、HTTP 403、连接拒绝三件套

我把自己踩过的坑整理成了速查表:

现象大概率原因解决方式
curl返回000网络不通、超时、证书校验失败先curl -v去掉-s看详细内容,确认端口可达、TLS证书是否有效
HTTP 401用户名密码错误用head插件或Kibana测试确认凭据,检查是不是密码旋转周期到期
HTTP 403权限不够,没有manage或delete权限给用户分配权限,或者检查是不是走了只读角色
HTTP 404索引不存在、别名不支持DELETE确认索引名正确,通配符模式是否被action.destructive_requires_name限制
连接拒绝ES端口没监听或防火墙拦截telnet ip 9200测试,确认ES进程还在
证书错误ES配置了TLS但curl没带ca或用了http内网信任环境用--insecure,生产建议配置CA路径

这里特别说一个ES 7.0之后的新变化:ES 7.0开始默认开启action.destructive_requires_name,也就是说DELETE /index_*这种通配符删除默认是被拒绝的,必须配置action.destructive_requires_name: false才允许通配符删除。但这个行为可以在elasticsearch.yml改,也可以通过集群设置动态改。我写脚本时不会依赖这个配置,而是用前面说的循环遍历方式逐个删除,这样既不违反安全设置,也能达到批量删除的目的。

5.3 定时任务化的血泪教训

写好脚本之后大家都想到要接入crontab,但我必须分享几个踩过的坑:

第一个坑是crontab环境变量。crontab执行时PATH是精简过的,可能找不到curl、date这些命令。所以脚本里要么用绝对路径,要么脚本开头显式声明:

export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

第二个坑是日志输出。直接在crontab里写*/5 * * * * /opt/scripts/clean_es.sh的话,crontab的邮件系统会把STDOUT发到本地邮箱,这没意义。正确姿势是重定向日志:

*/5 * * * * /opt/scripts/clean_es.sh >> /var/log/es_clean_cron.log 2>&1

第三个坑是时区偏差。ES按UTC存时间戳,但你的业务时间和系统时区可能不一致。脚本里算截止日期用的date -d "-15 days"是按系统时区算的,如果你的ES存储的是UTC时间,而这个“15天前”只是约等于,在高频写入场景下可能会出现边界数据被保留到16天才清理的情况。严谨做法是用UTC时区计算截止日期:

CUTOFF_DATE=$(date -u -d "-${RETAIN_DAYS} days" +%Y.%m.%d)

5.4 防止误删的终极兜底手段

脚本写得再好,也怕突发情况。我现在的习惯是:正式清理前,先用快照或者reindex把要删的数据备份一份到其他目录。成本高一点的用ES快照到OSS或者NAS,成本低的也可以只备份元数据。特别是删除关键业务索引之前,这个动作别省。

还有一种更稳妥的做法:不直接删索引,而是先把索引关闭(close),观察一段时间确认没问题再真正删除。对日志型数据,可以先把最新分区关掉等老索引过期;对业务型数据,先关闭可以防止被实时查询引用,等确认无依赖再删:

curl -s -X POST "http://${ES_HOST}/${INDEX_NAME}/_close"

关闭后索引不占内存但占磁盘,确认没问题后再:

curl -s -X DELETE "http://${ES_HOST}/${INDEX_NAME}"

这一步在慢节奏业务下尤其有用,相当于给删除操作加了一个可回滚的缓冲期。

5.5 日志审计:脚本没有日志等于没有执行

我见过很多同事的清理脚本不写日志,出了问题根本没法回溯。我现在固定要求每个脚本至少输出三样信息:执行时间、操作模式、影响范围。

echo "$(date '+%Y-%m-%d %H:%M:%S') [START] 清理任务启动,模式: ${MODE:-dry-run}" >> "$LOG_FILE" # ... 每个删除动作 echo "$(date '+%Y-%m-%d %H:%M:%S') [DELETE] ${idx}" >> "$LOG_FILE" # ... 结束 echo "$(date '+%Y-%m-%d %H:%M:%S') [DONE] 本次清理完成,共删除 ${DELETED_COUNT} 个索引" >> "$LOG_FILE"

日志统一写到/var/log/下,按月份切分。这样出问题后翻日志能直接定位到哪次任务、哪些索引、执行结果,省去“我记得我好像跑过”这种没营养的排查。

6. 常见问题与排查技巧实录

6.1 速查表:十分钟定位问题

我把运维过程中遇到的高频问题整理成了一张速查表,基本覆盖日常大部分场景:

问题现象排查路径解决参考
删除索引时提示index_not_found_exception索引没有前缀匹配到,或用HEAD先验证用_cat/indices确认索引名是否拼写错误
删除大量索引后集群黄/红删了主分片副本丢失,或正在恢复等集群自愈,看看是不是还有副本未分配,用_cluster/health确认
_delete_by_query执行超时数据量大、集群资源紧张加wait_for_completion=false,分批删,限流requests_per_second
删除之后文档还在段合并还没完成,或者用了软删除强制merge或等待后台任务完成
脚本删除失败但没报错curl返回码为0但HTTP码非2xx脚本全部检查HTTP_CODE,不要只看退出码
磁盘空间没释放删的是文档不是索引_forcemerge或在规划阶段就想清楚场景
crontab不执行脚本PATH问题或权限问题脚本内显式声明PATH,检查执行权限和环境变量

6.2 疑难杂症:delete_by_query导致的集群抖动

这个值得单独拿出来说。某次我处理一个数据清理任务,索引有1.2亿文档,用_delete_by_query删掉其中5000万条,结果删除期间集群查询延迟飙到3秒以上,CPU直接拉满,差点把线上业务拖垮。问题的根源是删除操作触发了大量的段合并,Lucene的写入线程和查询线程争抢CPU资源。

解决方案是在删除查询里限流。ES原生支持requests_per_second参数,默认无限制,可以设置成比如:

curl -s -X POST "http://${ES_HOST}/${INDEX_NAME}/_delete_by_query?wait_for_completion=false&scroll_size=5000&requests_per_second=500" \ -H "Content-Type: application/json" -d "$QUERY_BODY"

它的含义是每秒允许处理500个批量请求,每批5000个文档,也就是每秒最多处理250万文档。这个数值可以根据磁盘IO和CPU情况动态调整,确认对线上影响可控之后逐渐加大。我做过测试,从500调高到2000后CPU占用明显上升,但删除速度提升有限,瓶颈不在CPU而在磁盘IO。所以说,限流不是给自己添麻烦,是保护集群的命脉。

另一个技巧是尽量指定routing字段或者用_delete_by_query的routing参数,按分片批量提交,减少分布式协调开销。不过这个需要你对索引的routing策略有把握,否则慎用。

6.3 权限和角色问题排查思路

ES 7.0之后的权限体系是RBAC(基于角色的访问控制),删除操作需要indices:admin/delete权限。排查这类问题最好的途径是Kibana的Security -> Roles界面,而不是直接改配置文件。如果脚本用的账号是某个业务账号,不要去给它加所有索引的管理权限,建议创建专用清理角色,只授权特定前缀的索引。

给一个最小权限的参考Role定义:

{ "indices": [ { "names": ["app-log-*", "tmp-*"], "privileges": ["delete", "manage"], "allow_restricted_indices": false } ] }

然后分配专门用户,脚本里只使用这个低权限账号跑清理任务。这样做的好处是即使脚本匹配出重大错误,它也没有权限删其他前缀的索引,相当于二次防火墙。

6.4 最后再分享两个小技巧

第一个技巧:把_cat/indices的输出完整保留到日志。它看起来只是一串索引名,但对事后回顾非常有价值——哪些索引被删了、哪些保留、它们的文档数和占用空间都能看到,做目录历史记录再合适不过。

第二个技巧:脚本执行完后主动做一次集群健康检查,而不是等监控告警。我在清理脚本末尾会加一段:

HEALTH_STATUS=$(curl -s "${AUTH_ARGS[@]}" "http://${ES_HOST}/_cluster/health" | grep -oE '"status":"[^"]*"' | cut -d'"' -f4) echo "[INFO] 当前集群状态: $HEALTH_STATUS"

如果清理导致集群变红(主分片缺失),立刻在脚本里感知到并响应。大多数时候清理操作不会弄红集群,但万一索引删得比较多且副本没来得及恢复,这个健康检查能让你第一时间知道。

根据我自己的经验,shell删除ES数据这个操作,难点永远不在ES的API,而在你对自己的数据、自己的脚本、自己的集群状态有多少把握。真正成熟的清理方案一定不是找到一个能用的脚本就完事,而是把dry-run、二次确认、日志审计、限流保护、权限隔离全部串起来形成一套可重复执行的机制。在把这套机制跑稳之前,宁可手动操作也不要盲目自动跑。毕竟删数据这种事情,出一次事就够你记一辈子。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 2:49:16

AI辅助文献综述写作实战:三步生成高质量学术初稿

写文献综述&#xff0c;尤其是毕业论文里那章两万字的文献综述&#xff0c;基本是每个研究生都绕不开的坎。我记得自己当年写综述那会儿&#xff0c;光是整理文献就整理了将近一个月&#xff0c;Excel里塞了上百条笔记&#xff0c;Word里贴了一堆PDF高亮截图&#xff0c;结果真…

作者头像 李华
网站建设 2026/10/19 2:49:14

STM32 HAL库驱动SSD1306 OLED点不亮?时序适配与SPI/I2C握手修复指南

简介&#xff1a;本资源是面向STM32嵌入式初学者与进阶开发者的0.96寸OLED显示屏HAL库驱动移植方案&#xff0c;聚焦解决OLED在STM32F1系列&#xff08;如F103C8T6&#xff09;平台上的快速适配与图形化显示难题。压缩包共151个文件&#xff0c;含47个头文件&#xff08;.h&…

作者头像 李华
网站建设 2026/10/19 2:48:27

AI写作“机器味”太重?降AI率工具实测与去AI味四步法

老读者都知道&#xff0c;我平时写文章有个固定流程&#xff1a;初稿阶段会让AI参与&#xff0c;出框架、找资料、扩写案例&#xff0c;效率确实能翻好几倍。但慢慢我发现一个问题&#xff0c;凡是我偷懒没做二次加工的段落&#xff0c;读者一眼就能看出来&#xff0c;评论区甚…

作者头像 李华
网站建设 2026/10/19 2:48:16

MATLAB实现孤立字语音识别:HMM建模与MFCC特征工程

简介&#xff1a;本资源是一套基于隐马尔可夫模型&#xff08;HMM&#xff09;实现孤立字语音识别的完整MATLAB代码实现&#xff0c;面向语音信号处理初学者、模式识别课程学习者及低起点科研实践者&#xff0c;解决从理论建模到代码落地的关键断层问题。压缩包共15个文件&…

作者头像 李华
网站建设 2026/10/19 2:48:16

AI赋能应用实践指南:从场景选型到RAG与Agent落地

1. 项目全貌与落地思路拆解1.1 说的"AI赋能"到底赋的是什么能2026年这个时间点谈AI赋能&#xff0c;和两三年前完全是两码事。早些年大家聊AI&#xff0c;大多还是"搞个聊天机器人""做个OCR识别"这种单点尝鲜&#xff0c;现在再提赋能&#xff0…

作者头像 李华
网站建设 2026/10/19 2:47:59

数据结构PTA备考全攻略:选择题与主观题考点拆解与刷题策略

6. 写在最后的一点心里话每次期末看到同学们抱着厚厚的数据结构教材&#xff0c;对着PTA上的选择题题库一遍遍刷&#xff0c;主观题却连题目都读不下去&#xff0c;我都很理解那种焦虑。数据结构这门课难就难在——它既不靠死记硬背&#xff0c;也不是光看懂就能拿分&#xff0…

作者头像 李华