ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Elasticsearch数据清理实战:Shell脚本与索引生命周期管理

Elasticsearch数据清理实战:Shell脚本与索引生命周期管理 如果你在搜索引擎里敲下“shell删除es数据”八成是遇到两种情况要么ES集群磁盘告警要腾空间要么测试环境里灌了一堆脏数据想快速清理。别看这个需求听起来简单实际操作中翻车的人不少。有人用一条DELETE /*把整个集群的索引全删了有人辛辛苦苦跑了半天_delete_by_query却发现磁盘空间一点没少还有人直接rm掉了ES的数据目录导致节点起不来。这篇文章不打算复读官方文档而是把我实际处理ES数据清理时的完整思路、能直接抄的shell脚本以及踩过的坑一次性讲清楚。适合运维、后端开发和所有需要跟ES打交道的人。1. 先分清你要删的是“索引”还是“文档”两种清理思路差别很大很多人第一次接触ES清理时脑子里想的还是MySQL那种DELETE FROM table WHERE ...的逻辑。但ES的存储架构决定了删文档和删索引是两条完全不同的路线性能和底层行为也天差地别。1.1 为什么说删除文档不等于释放空间ES底层是Lucene数据是按“段segment”组织的。段文件一旦写入就是只读的后续的更新删除操作不会直接改动段文件而是靠标记删除.del文件来记录。这么说吧删除一条文档其实只是给这条数据打了一个墓碑标记它占的空间还在那里。真正的空间释放要等段合并merge发生把有删除标记的段文件重写一遍去掉那些死数据才可能回收空间。但merge本身需要额外资源ES不会因为你删了几条小数据就去触发全局merge。这就是为什么你跑了delete_by_query返回的一堆任务都显示成功但磁盘剩余空间几乎没变化的根本原因。反观删除索引它是直接把整个索引的全部分片文件都unreference掉数据段文件从磁盘上彻底摘除。在大多数情况下ES删除索引后空间会立即释放或者很快进入可复用状态。我对这个问题的态度很简单如果只是要清理过期数据删索引永远优先于删文档除非业务对“索引还得保留”这件事有硬性要求必须按条件剔除部分数据。1.2 什么时候该删整索引什么时候该按条件删我平时判断的标准大概是这样清理场景推荐手段原因日志型索引过期logstash-2024.01.01删除整索引索引维度天然按时间段切分删起来干净利落空间释放彻底同一索引内混有多个业务的数据只需清掉某一小块按查询删除_delete_by_query无法按索引隔离只能按条件删但要接受“空间不是立即释放”测试环境灌了一堆随机数据删除整索引并重建比delete_by_query快得多还能顺便重置mapping需要保留索引结构但删除其中大量历史数据新建索引 reindex然后切换别名删索引再重建比在原索引上delete_by_query更可控看了这个表格你应该能理解删整个索引是ES亲儿子操作delete_by_query更多是无奈之举。千万不要图省事在日志型索引里执行按查询删除那相当于“用大炮打蚊子”既慢又费资源。2. 动手前的三道“安全锁”备份、权限、影响面确认我见过最离谱的一次事故就是某运维同学连备份都没做直接按通配符删了索引结果发现通配符写窄了一位日期删的数据比预期多了一个月。所以不管你有多急下面三步别跳过。2.1 先看一眼集群健康再把快照备好动手前先确认集群状态是不是健康的。curl -s http://localhost:9200/_cluster/health?pretty | jq .status返回green才能放心干。如果返回yellow意味着一定有副本分片没分配这时候删除索引会让问题更复杂。如果返回red那更要先解决深层次问题别急着清理数据。接着做快照。已经注册过快照仓库的话直接执行curl -X PUT http://localhost:9200/_snapshot/backup_repo/snapshot_before_cleanup_$(date %Y%m%d%H%M) \ -H Content-Type: application/json \ -d {indices:app-log-2024.01.*}没注册过仓库的话先注册一个本地文件系统仓库curl -X PUT http://localhost:9200/_snapshot/backup_repo \ -H Content-Type: application/json \ -d {type:fs,settings:{location:/data/es_snapshots}}快照耗时跟数据量成正比。几十GB的索引一般也就几分钟到十几分钟值得等。哪怕是测试环境我也建议快照做一遍再开删至少给自己留条后路。2.2 权限和索引匹配半径的确认如果ES集群开了安全认证用curl请求时要带用户信息curl -s -u elastic:yourpassword http://localhost:9200/_cat/indices?v删索引需要delete_index权限执行_delete_by_query需要对应的索引写权限。实际工作中遇到最多的就是权限不足导致返回403排查起来又慢又烦。所以先随手验证一下你当前账号能不能删curl -s -u elastic:yourpassword -X DELETE http://localhost:9200/app-log-2024.01.15如果是403别挣扎了去找管理员给权限。注意删除动作不可逆建议新建一个只读账号先做查询验证确认清楚再换有权限的账号执行删除。这里还有一个容易忽略的点。ES有很多点号开头的系统索引比如.kibana_*、.security-*、.monitoring-es-*。这些索引被隐藏保护但通配符包含.开头时也可能被误匹配到。你手动删除时觉得自己写的是业务前缀万一前缀写错成了*系统索引很难闪开。2.3 通配符杀伤半径宁可多打几次列出命令ES的索引通配符设计得很强大*、?都可以用。但也正因为强大它成了误删事故的头号来源。我养成一个习惯执行删除前先用通配符把所有会匹配到的索引列出来看一遍。curl -s http://localhost:9200/_cat/indices/app-log-2024.01.*?hindex,store.size,docs.countformatjson只看名字不过瘾还可以加jq过滤把点开头的系统索引排除掉curl -s http://localhost:9200/_cat/indices?formatjson \ | jq -r .[] | select(.index | startswith(.) | not) | [.index, .store.size] | tsv这一步花不了半分钟但能避免所有关于“到底匹配到了什么”的焦虑。确认无误后再走删除动作。那些敢直接执行DELETE /*的同学一旦在生产集群上跑出来那种惊悚感会让你终身难忘。3. 按索引删除一个能上生产环境的清理脚本清了思路也加了安全锁接下来进入正题写一个真正能用的shell清理脚本。3.1 先手动删单个索引与通配符单个索引直接删命令简单到不像话curl -s -u elastic:yourpassword -X DELETE http://localhost:9200/app-log-2024.01.15批量用通配符的情况比如要删掉2024年1月所有日志索引curl -s -u elastic:yourpassword -X DELETE http://localhost:9200/app-log-2024.01.*这条命令会删除所有匹配的索引。ES的通配符默认会匹配所有分片状态为open的索引正常业务下足够了。不过我严重不建议你在生产环境直接这么干除非你已经百分之百确认过匹配范围。更稳妥的是用下面的脚本先算清楚再删。3.2 按保留天数批量清理脚本逻辑详解生产环境最常见的需求就一句话app-log-开头的索引只保留最近7天之前的一律删除。下面这个脚本我一直在用逻辑不算复杂但把名字解析、系统索引过滤、dry-run都考虑进去了。#!/usr/bin/env bash set -euo pipefail ES_HOST${ES_HOST:-http://localhost:9200} ES_USER${ES_USER:-elastic} ES_PASS${ES_PASS:-yourpassword} INDEX_PREFIX${INDEX_PREFIX:-app-log-} RETAIN_DAYS${RETAIN_DAYS:-7} DRY_RUN${DRY_RUN:-1} # 默认打印不删除确认没问题再置0 # 取当前时间戳 cutoff_timestamp$(date -d -${RETAIN_DAYS} days %s) # 用creation.date来判断比解析索引名更稳 curl -s -u $ES_USER:$ES_PASS \ $ES_HOST/${INDEX_PREFIX}*/_settings?flat_settingstrueexpand_wildcardsopen \ | jq -r to_entries[] | select(.key | startswith(.) | not) | [.key, .value[index.creation_date]] | tsv \ | while IFS$\t read -r idx created_date_ms; do created_ts$((created_date_ms / 1000)) if (( created_ts cutoff_timestamp )); then if [[ $DRY_RUN 1 ]]; then echo [DRY RUN] would delete index: $idx else echo [DELETE] deleting index: $idx curl -s -u $ES_USER:$ES_PASS -X DELETE $ES_HOST/$idx echo fi else echo [KEEP] index: $idx fi done echo Cleanup job finished.脚本里几个值得琢磨的点为什么用index.creation_date而不是解析索引名里的日期因为索引命名不一定规整有的人用点分隔有的人用横杠分隔还有人干脆不带日期。creation_date是ES创建索引时打的时间戳不会骗人判断是否超期更可靠。为什么开头就置DRY_RUN1先跑一遍看它打印哪些[DRY RUN] would delete index人工核对无误再设置DRY_RUN0执行真实删除。这个习惯帮我挡掉了很多次“本来只想删30天前结果把昨天的也算进了删除范围”的乌龙。如果你确定索引名里就是标准日期格式也可以直接用索引名解析速度更快。下面这个函数兼容app-log-2024.01.15和app-log-2024-01-15两种分隔方式parse_ts_from_name() { local name$1 local date_part${name#$INDEX_PREFIX} date_part${date_part//./-} date -d $date_part %s 2/dev/null || return 1 }把它替换掉脚本中的creation_date判断也可以。不过我还是更推荐前者少绕弯子。3.3 实测删除后磁盘空间多久释放我在一套模拟的日志集群上做过测试索引总量80GB左右一共120个分片分布在三台数据节点上。执行完删除命令后我从_cat/allocation观察磁盘变化curl -s http://localhost:9200/_cat/allocation?v结果是这样的删除命令返回后每个节点上对应的分片文件开始清理。大约几十秒到一两分钟磁盘剩余空间明显回升。这说明删除整索引的空间释放基本是即时的不需要额外触发merge。但有一点要注意如果集群有自动rebalance默认开启某些节点的分片会重新平衡空间占用会有波动。比如你删了A节点上的大索引B节点可能马上迁过来几个分片导致看起来空间并没有完全腾出来这是正常现象。4. 按查询条件删数据_delete_by_query的完整用法与调优总有一些数据没办法按索引整体删除比如用户误产生了大量错误文档或者测试数据混进了核心索引但你又不想伤及整个索引。这时就要用_delete_by_query。4.1 同步删与异步删小任务和大任务要分开处理最基本的请求是这样curl -s -u elastic:yourpassword -X POST \ http://localhost:9200/app-log-2024.01/_delete_by_query \ -H Content-Type: application/json \ -d { query: { range: { timestamp: { lt: 2024-01-10T00:00:00 } } } }小数据量几百MB内这么干没问题请求会同步等待完成返回后你能直接看到删除了多少条。但如果是几十GB甚至更大的数据量同步等待会卡住HTTP连接经常超时。这时候要改成异步模式curl -s -u elastic:yourpassword -X POST \ http://localhost:9200/app-log-2024.01*/_delete_by_query?wait_for_completionfalsescroll_size5000slices4 \ -H Content-Type: application/json \ -d { query: { range: { timestamp: { lt: now-7d } } } } \ | jq -r .task加了wait_for_completionfalse之后请求会立刻返回一个task id真正的删除动作在后台执行。返回的task id长这样{ task: oGuC7ABgQqO4Zb9CQ6DvVQ:123456 }拿这个task id你就可以随时查看任务的执行进度。4.2 观察任务进度不是用直觉而是查_tasks后台任务跑起来之后总有同事问“删完了吗”“怎么还在删”。别看感觉直接查curl -s -u elastic:yourpassword http://localhost:9200/_tasks/oGuC7ABgQqO4Zb9CQ6DvVQ:123456?pretty | jq .task.statusstatus里会有这些关键字段total本次删除需要处理的总文档数updated更新过的文档数deleted已经删除的文档数batches完成了多少批version_conflicts版本冲突数noops空操作数按照deleted/total就能算出进度条。还有更高阶的curl -s -u elastic:yourpassword http://localhost:9200/_tasks?actions*delete*detailed | jq .nodes这条命令能列出集群内所有正在执行的删除任务适合多任务并发时统一排查。4.3 Scroll、slices、refresh这几个参数到底怎么设_delete_by_query底层靠scroll游标分批扫数据再删。参数没设好轻则慢重则把集群拖垮。我调过不少集群常用经验如下参数建议值理由scroll_size500010000默认1000偏保守数据量大时批次太小网络交互次数太多slices24或auto并发切片能明显加速但不建议超过索引分片总数开太多反而调度开销大refreshfalsedelete_by_query默认就是false不用额外设置但你要知道它不会像普通删单条那样触发refreshconflictsproceed慎用有并发写入时大概率出现版本冲突proceed会跳过冲突继续删但可能导致部分文档漏删requests_per_second-1不限速生产集群谨慎不限速建议设置合理的值例如5001000给业务留点IO当数据量真的很大比如索引本身已经几十上百GB我不会直接在原索引上跑_delete_by_query。正确做法是新建一个干净索引用reindex把需要保留的数据迁过去然后切换别名最后删除旧索引。这样做的效率远高于在原索引上delete还能顺便压缩段文件。我的个人判断阈值是这样的删除量小于索引总量的20%可以考虑_delete_by_query超过20%就值得走“reindex 替换别名”的路线。别嫌麻烦删几十GB的同段数据所消耗的IO和CPU比你想象的大得多。5. 定时自动清理cron方案和ILM方案怎么选一次性清理做完还得考虑以后怎么防不然磁盘再过一个月又要报警。自动清理有两条路一条是shell cron简单直接另一条是ES自带的ILMIndex Lifecycle Management更加优雅。两条路我都部署过说下差异。5.1 cron shell适合“想尽快看结果”的清理把上一节的脚本保存到/opt/scripts/es_cleanup.sh加个cron任务0 3 * * * /opt/scripts/es_cleanup.sh /var/log/es_cleanup.log 21每天凌晨3点跑一次把过期索引删掉日志记录到/var/log/。选凌晨是因为业务低谷IO压力小。cron方案的好处是直观出了什么问题看日志就行。坏处是它不知道ES的分片状态也不了解数据生命周期。而且脚本里如果有bug第二天可能就把不该删的也删了。所以这个方案适合数据量可控、索引命名规律、不复杂的环境。5.2 ILM适合长期稳定写入的日志型索引ILM是ES官方的生命周期管理功能。定义一个30天后删除的策略curl -X PUT http://localhost:9200/_ilm/policy/log_30d_policy \ -H Content-Type: application/json \ -d { policy: { phases: { hot: { min_age: 0d, actions: {} }, delete: { min_age: 30d, actions: { delete: {} } } } } }然后把这个策略挂到索引模板上后续新索引自动套用curl -X PUT http://localhost:9200/_index_template/app-log-template \ -H Content-Type: application/json \ -d { index_patterns: [app-log-*], template: { settings: { index.lifecycle.name: log_30d_policy } } }ILM的优势很明显不需要额外脚本ES自己会在分片层面执行滚动和删除。缺点是策略本身有一定学习成本而且遇到已经存在的历史索引需要手动POST /app-log-2024.01.01/_ilm/explain确认它有没有被正确应用。我的建议是持续写入的日志型数据上ILM才是正路一次性清理历史脏数据用shell脚本更顺手。两者并不矛盾可以同时存在。5.3 兜底写一个删除前的“最后确认”无论哪种自动方式我都强烈建议加上一道“最后确认”的兜底逻辑。在脚本里加一个环境变量ALLOW_DELETEif [[ ${ALLOW_DELETE:-0} ! 1 ]]; then echo Deletion is not allowed. Set ALLOW_DELETE1 to execute. exit 1 ficron任务默认不传这个变量所以即使脚本被误触发也只会打印日志而不真正删除。只有人工手动确认后才会带着ALLOW_DELETE1执行。这个设计可能看起来多此一举但在生产集群上救过我的命。6. 踩过的坑别踩第二遍6.1 误删全集群索引的一场“演练”有次某同事给测试环境写清理脚本想删掉前缀为test-的索引结果命令写成这样curl -X DELETE http://localhost:9200/test*这个写在命令里没问题问题是当时测试环境里其他团队也创建了testing-、test-data-等前缀的索引甚至还创建了.test-infra这种系统索引。通配符一下把所有匹配到的全删了。恢复过程非常痛苦幸好有快照但复盘时有个问题其实很容易提前规避在删除命令里同时排除非业务前缀。我个人后来的约定是生产环境脚本里永远不用裸通配符删索引只用脚本内的INDEX_PREFIX拼接匹配范围并且跑之前先打印所有要删的索引名。6.2 rm -rf 数据目录的严重性这个坑我必须重点提。某同学觉得ES索引删得慢直接登录数据节点把data/nodes/下的索引目录rm -rf了。结果节点进程还活着Lucene文件却被物理删除索引状态直接变得一团糟部分分片怎么都恢复不了最后还是靠快照才把数据追回来。ES的数据目录坚决不能直接动。它跟普通文件完全不一样Lucene在节点上维护着大量的文件句柄和内存映射rm删除之后进程虽然不会立刻崩但后续写入操作会在底层数据不完整的情况下继续运行最终大概率导致集群red状态。如果你真的遇到“删不掉、磁盘要爆”的情况别走极端。优先考虑关掉rebalance、先把数据迁到其他节点再删除索引。如果集群已经无法响应重启节点后再说。6.3 删除后副本迁移引发雪崩很多人不会想到这个删除一个大索引后集群的自动rebalance会被触发。假设一个节点上原有一大片分片现在空出来了ES会把其他节点的分片迁移过来补充平衡。这本身是好事但如果一次性删了多个大索引rebalance会在短时间内大量迁移分片IOPS飙升反而拖慢了业务查询。我的处理办法是删除大索引之前在集群范围暂时关闭rebalancecurl -X PUT http://localhost:9200/_cluster/settings \ -H Content-Type: application/json \ -d { transient: { cluster.routing.rebalance.enable: none } }等删除完成、分片状态稳定下来再恢复curl -X PUT http://localhost:9200/_cluster/settings \ -H Content-Type: application/json \ -d { transient: { cluster.routing.rebalance.enable: all } }实测这个方法能显著减少删除大索引后的集群波动。7. 一点操作心得写到这里把我在实际中摸索出来的几条心得分享一下。第一条还是那句老话能用删索引解决的事千万不要用删文档解决。ES的段合并机制决定了文档删除的空间释放是滞后的删索引才是干净利落的做法。第二条是把所有清理脚本都做成“默认dry-run、显式确认才执行”的模式。初期可能觉得多此一举但当你凌晨三点被报警电话吵醒半睡半醒之间还能靠这层机制避免事故的时候你就知道它有多重要。最后一个小技巧无论用哪种方式清理记得在清理结束后顺手看一眼_cluster/health确认集群状态恢复为green。如果返回的是黄色大概率有副本分片还没分配这时候别急着关监控等几分钟再查一次。数据清理看似只是几条curl命令的事但背后涉及索引生命周期、段合并、分片分配这些机制。摸熟了这些机制你再遇到ES磁盘告警就不会慌按本文的流程走有备份、有确认、有脚本基本不会出大问题。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表