news 2026/10/1 4:20:38

从脚本到生产级工具:磁盘巡检与日志清理的迭代实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从脚本到生产级工具:磁盘巡检与日志清理的迭代实战

tuowei2这个代号,第一次听的人都会问一句:啥意思?其实它是我本地维护的一套服务器磁盘巡检与日志清理工具,tuowei是“拓位”的拼音,第二版。工具本身不复杂,但迭代到这个版本的过程中踩了不少值得记录的坑,也让我把“能跑的脚本”和“扛得住的生产级工具”之间的差距彻底想明白了。

这套东西解决的是所有后端都会遇到的痛:日志文件只增不减,磁盘总有写满的一天。tuowei2会自动扫描各个目录的占用,按策略清理过期日志,生成报表,并在磁盘水位超过阈值时发出告警。如果你是那种被磁盘告警在凌晨叫醒的人,或者你的服务经常因为日志把磁盘写满而卡死,这篇文章应该能帮上忙。我会把tuowei2从设计思路、核心代码、上线前后踩坑的完整过程摊开来说,既有可以直接抄走的片段,也有只有跑过生产环境才懂的细节。

1. 两个版本的分水岭:v1从能用到难用的全过程

1.1 一切的起点:凌晨两点的磁盘告警

做v1的动机非常朴素。有一年春天,我维护的一个运营后台开始频繁打磁盘告警,查下来是业务日志和Nginx access log疯涨,单台机器每天新增几个GB,根分区只剩不到10%。当时图省事,我写了大概50行shell脚本:find指定目录按mtime删除超过7天的日志,再用du统计剩余空间,如果超过80%就给运维群发一条curl通知。脚本丢进crontab里,每天凌晨1点跑一次。

当时觉得这事已经“解决”了。但一个工具能跑和能用是两码事,v1坚持了不到半年,就暴露出一堆只在生产环境才显现的问题。最典型的场景是:你明明每天在删日志,磁盘占用却还在涨,因为业务临时文件、导出文件、备份文件都不在清理规则里,它们会悄悄把磁盘吃满。

1.2 v1的三条致命短板

第一个短板是清理策略写死在代码里。目录列表、保留天数、触发阈值全是一堆变量塞在脚本顶部,每次要调整都得ssh登录服务器改脚本,改完还要手工重跑一遍验证。岗位交接的时候,新同事看那脚本一头雾水,注释只有一行:“删除7天前的日志”。

第二个短板是没有任何前置检查。有一次日志目录下出现了大量临时导出文件,占了好几十GB,但它们不是日志,mtime只有3天,find命令根本不碰它们。结果磁盘还是飙到95%,服务开始报no space left on device,用户那边直接看到白屏。事后排查才发现,清理逻辑只认文件年龄,根本不看磁盘真实水位,也不看目录里到底堆了什么。

第三个短板是误删风险不可控。find + rm的组合在路径写错的时候非常致命,尤其当变量拼接路径时如果某个目录为空字符串,rm -rf可能会落在意想不到的位置。虽然v1没有真正删错东西,但那种“不知道自己会不会删错”的感觉,比磁盘告警还让人焦虑。每天凌晨cron跑完之后,我都要爬起来瞄一眼日志,确认没有把不该删的东西删掉。

1.3 为什么选择重写而不是接着打补丁

其实在v1后期,我已经往里面塞了各种补丁:加了磁盘水位预判、加了排除目录、加了日志输出。但每次改完都觉得脚本越来越像一堆临时方案叠在一起。当时给了我自己两个选择:继续在shell脚本上打补丁,或者用Python重写一版,把配置、执行、报表、告警拆开。

我选了重写,理由有三条。第一,shell做简单的find和rm够用,但要处理遍历性能、并发锁、幂等重放、结构化报表,成本会越来越高,Python在这些方面有现成的标准库。第二,配置外置是刚需,我不希望下一次调整策略还要进服务器改脚本。第三,v1最欠缺的不是功能,而是可观测性——跑完之后发生了什么、删了什么、还剩多少,这些信息必须能追溯,而这正是shell脚本最薄弱的地方。工具一旦不可观测,出了问题就只能靠猜。

2. tuowei2的核心设计:从脚本思维到任务编排思维

2.1 先把职责拆清楚:四个模块各管一段

tuowei2的整体结构非常简单,简单到再复杂的场景也是围绕同一套骨架扩展:

  • collector:扫描指定目录,统计占用,产出结构化数据。
  • cleaner:基于策略执行清理,但所有删除操作前先进入“演练模式”。
  • reporter:把扫描和清理结果渲染成报表。
  • notifier:通过Webhook把报表和告警发出去。

四个模块通过一个主流程串起来,各自只依赖配置,不互相调用内部函数。这样做的直接好处是:想单独跑一遍扫描不清理,或者只重发一次报表,都不需要动逻辑,命令行指定模块即可。我曾经把collector单独接进一个数据看板,让它每10分钟跑一次,清理还是按天跑,两个流程互不干扰。这种“模块边界清晰”带来的自由度,是v1那种一个脚本从头跑到尾的结构没法给的。

2.2 配置外置:改策略不用碰代码

我选择用YAML做配置,因为它的可读性对不写代码的人(比如接班的同事)最友好。配置片段大致是这样:

scan: - path: /var/log/app name: app_logs clean: enabled: true max_age_days: 7 max_dir_size_mb: 4096 - path: /data/export name: exports clean: enabled: false global: disk_threshold_percent: 80 dry_run: true pid_lock: /var/run/tuowei2.pid timezone: Asia/Shanghai

关键点是:每条扫描路径都可以独立设置保留天数、大小上限、是否参与删除。比如导出目录只统计不删除,应用日志既按天数又按大小双条件清理。所有策略改动都只发生在YAML里,脚本本身不碰业务逻辑。之前v1要改一个阈值需要进服务器操作,现在只需要在一个公共的配置文件里改值,然后reload一下就生效,这对我这种同时维护多台机器的人来说,体验是质的飞跃。

2.3 任务状态机与失败重试

主流程我设计成一个小状态机:pending、scanning、cleaning、reporting、done,以及两个失败态cleaning_failed和reporting_failed。整个流程是顺序的,但clean失败不会让report和notify中断——宁可先把问题暴露出来,也不能让一次清理故障吞掉告警信息。这句话说起来容易,实际设计时我反复纠结过:cleaning失败之后到底要不要继续report?后来想明白了,必须继续,因为report里会带上cleaning失败的明细,这本身就是最有价值的告警内容。

重试只针对notifier。Webhook偶发超时很常见,所以发送带三次重试,每次间隔指数退避。扫描和清理不自动重试,因为重复扫描代价高、重复清理可能出问题,这类任务应该让人去看日志,而不是让机器反复尝试。如果你在做类似的运维工具,我建议你也遵循这个原则:凡是有副作用的操作,失败了一定要停下来等人处理,不要自作主张重试。

2.4 幂等设计:重复执行也要安全

这是我这次重写最看重的一点。v1最大的心理负担就是手误重跑一遍会不会出问题。tuowei2对每个清理动作都做记录:删除前先把目标文件的path、size、mtime写入历史库(一个本地SQLite文件),再执行删除。万一action日志和实际删除不同步,也能通过SQLite完整回溯。

加上dry_run参数,默认首次部署时强制开启。dry_run模式下collector完整执行,cleaner只打印“将要删除”清单,不真正删文件。确认清单没问题之后,把dry_run改成false跑一遍,再检查一次实际结果,整个过程都是可回滚的。这套逻辑说白了就是把“手抖重跑”从“事故”变成“无害操作”,因为每次删除都有记录,重复执行时能通过历史库判断这个文件是不是已经处理过了,已经处理过的就直接跳过。

3. 三个直接可抄的代码模块:扫描、清理、告警

3.1 模块一:目录占用扫描器

真正扫描的时候,用os.scandir比os.walk和pathlib.rglob要更稳,因为rglob会先构建整个目录树,遇到海量小文件时内存飙升;os.scandir是边遍历边统计,而且可以follow_symlinks=False安全跳过软链接。我第一版用的是rglob,在单目录十万个文件的情况下内存占用涨了快一个GB,后来改成os.scandir才降下来。

# collector.py 目录占用扫描(核心简化版) import os def scan_dir_size(root: str, max_depth: int = 5) -> dict: """递归统计目录占用,超过max_depth不再下探。""" result = {"path": root, "size": 0, "child": []} try: with os.scandir(root) as it: for entry in it: if entry.is_symlink(): continue if entry.is_file(follow_symlinks=False): try: result["size"] += entry.stat(follow_symlinks=False).st_size except OSError: continue elif entry.is_dir(follow_symlinks=False): if max_depth > 0: sub = scan_dir_size(entry.path, max_depth - 1) result["size"] += sub["size"] result["child"].append(sub) except PermissionError: pass return result

两点说明:一是max_depth必须有限制,否则即使跳过软链接,遇到嵌套很深的目录也会白白消耗大量时间;二是PermissionError要吞掉并继续,很多系统目录对普通用户就是只读的,工具没必要因为扫不到某个目录就整体失败。扫描结果里的size单位是字节,后面报表里统一转成GB或MB展示。

3.2 模块二:带安全护栏的清理器

清理器的核心不是“能删文件”,而是“知道哪些能删哪些不能删”。我把它拆成三层检查:

# cleaner.py 清理动作的安全检查链路(核心简化版) from pathlib import Path def safe_delete(path: Path, allowed_roots: set) -> bool: # 第一道护栏:软链接直接拒绝 if path.is_symlink(): return False # 第二道护栏:目录必须落在允许清理的根路径之下 try: abs_path = path.resolve() except OSError: return False if not any(abs_path.is_relative_to(Path(root)) for root in allowed_roots): return False return True

这里allowed_roots来自配置里clean.enabled的目录,本质上是一张白名单。任何不在白名单内的路径,即使因为配置写错了被传给清理器,也会被挡下来。另外还有一个容易被忽略的细节:resolve()之后的路径是绝对路径,配置里的路径最好也统一转成绝对路径再比较,不然“相对路径能不能删”这种问题会让你debug到怀疑人生。

清理策略部分,我同时支持“保留天数”和“目录大小上限”。当天数条件命中但目录总大小仍然很大时,按文件mtime从旧到新继续删,直到目录大小降到目标值以下。这条逻辑用一句话概括:先保证数据不过期,再保证磁盘不爆满。实际执行时,我会先按mtime排序取最旧的一批文件,模拟删除看会释放多少空间,如果不够再扩大范围,避免一次性删太多影响业务侧的数据追溯需求。

3.3 模块三:报表生成与告警触发

报表我直接生成Markdown格式的表格,因为运维群普遍支持Markdown渲染,贴进去就能看:

# report.py 报表生成(核心逻辑示意) def render_inventory(data: dict) -> str: rows = [] rows.append("| 目录 | 占用大小 | 删除文件数 | 释放空间 |") rows.append("| --- | --- | --- | --- |") for item in data: rows.append(f"| {item['path']} | {item['size']} | {item['deleted']} | {item['released']} |") return "\n".join(rows)

告警触发条件是全局磁盘使用率超过阈值,或者某一个被标记为critical的目录剩余空间不足。通知走Webhook,发送函数带超时和重试,我在使用中给Webhook加了5秒超时,避免notifier线程卡死影响主流程。这里有个经验之谈:告警文案一定要带上这台机器的主机名和具体目录路径,否则收到告警的人还要去查“是哪台机器在报警”,这一步非常影响响应速度。

4. 上线前的应激测试与三个坑的完整排查链路

4.1 坑一:软链接目录差点清空根分区

第一次在预发布环境做演练时,我把/var/log挂进扫描列表,然后发现扫描结果里出现了一个异常大的目录:/var/log/oldlogs。我第一反应是“业务还留了这么一大坨日志?”,正准备把清理策略加上去,结果同事提醒我看看那是不是软链接。

一查,果然是:/var/log/oldlogs -> /。也就是说扫描器如果没跳过软链接,会把整个根文件系统的内容都算进/var/log/oldlogs的占用里;如果当时我把清理策略误加到这条路径上,后果就是cleaner认为根目录下所有文件都“过期”了,整个删下去,运维事故直接上演。这就是为什么我说一定要用独立的环境先跑演练,这种级别的错误在预发布环境暴露出来是幸运,暴露在生产环境就是事故。

排查链路其实不算复杂,但暴露了一个设计缺口:我在collector里跳过了软链接,但在cleaner的第一版里没有对软链接做防御。也就是说,即使collector不统计软链接目录,如果有人在配置里手动加了一条指向软链接的路径,cleaner照样会傻乎乎地执行删除。修复就是在safe_delete里加第一道护栏:path.is_symlink()直接拒绝,任何情况下都不允许删除软链接本身,也不允许跟随软链接去删目标。

4.2 坑二:清理任务互相打架引发的文件竞争

上线一段时间后,我注意到一个奇怪现象:偶尔会有几条告警说“清理任务异常退出”,但看日志却找不到明显的报错。后来我把cron的执行时间拉长来看,发现是任务运行时超过了预期,下一个周期的任务又启动了,两个进程同时扫同一个目录,同时去删同一批文件,结果一个进程删除成功后,另一个进程在stat文件时拿到了None,直接抛了异常。

这个坑的本质是任务没有防重入。修复方式非常传统但非常有效:文件锁。我在启动时用fcntl.flock拿一个排他锁,如果锁拿不到,说明已经有一个tuowei2在跑,直接退出并输出“previous instance still running”。

# lock.py 单实例锁(Linux环境) import fcntl class SingleInstanceLock: def __init__(self, lock_path): self.lock_file = open(lock_path, "w") try: fcntl.flock(self.lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) self.acquired = True except OSError: self.acquired = False def release(self): if self.acquired: fcntl.flock(self.lock_file, fcntl.LOCK_UN) self.lock_file.close()

从此之后,cron纵使配置成每5分钟跑一次,也不会出现两个实例互相踩踏的情况。这个坑也让我意识到:任何定时任务,只要执行时间不稳定,就必须假设它会和下一个实例重叠,防重入不是可选项,是必选项。

4.3 坑三:报表时间凭空少了8小时

有段时间群里收到的报表,完成时间总是显示的凌晨,可cron明明配的上午9点跑。第一反应是cron时区变了,查了一圈发现系统时区根本没问题。后来才想起,Python的datetime.now()返回的是系统本地时间,而系统本地时间是UTC,报表渲染又没做时区转换,所以显示的是UTC时间,反映过来就是比预期少了8小时。

修复方式不复杂,但能体现工具的成熟度:配置里加了一个timezone字段,所有时间字符串渲染和日志记录统一走zoneinfo,不用默认的本地时区。并且在报表末尾附带一行“时区:Asia/Shanghai”,杜绝以后再出现类似误判。如果你也在这类工具里格式化时间,我建议直接把时间统一成带时区的ISO格式,比如2025-05-21T09:30:00+08:00,这样无论谁看到都不会产生歧义。

4.4 止血与自检清单

踩完这三个坑,我把“上线前自检清单”固化到了项目README里:

  1. dry_run模式必须开启,至少完整跑三天演练,看清理清单是否符合预期。
  2. 把所有配置路径过一遍safe_delete白名单,确认没有软链接目录进入可删除集合。
  3. 人工触发两次并发实例,确认单实例锁生效。
  4. 检查报表中的时间字样是否带时区说明。
  5. 在测试环境故意放一个超大文件,验证大小阈值清理确实能把目录压到目标水位。

这条清单后来成了我部署到其他机器的标准操作流程。有了它,新环境的上线时间从半天压缩到半小时。为了更直观,我把三个核心问题的排查结果整理成了下表:

坑根因修复
软链接目录误入清理范围cleaner缺少软链接防御safe_delete首道护栏is_symlink拒绝
并发实例互相踩踏缺少单实例锁fcntl.flock排他锁
报表时间偏差8小时使用默认UTC本地时间配置timezone字段统一渲染

5. 运行半年后的效果复盘与下一步改造方向

5.1 半年来的实际运行数据

我给tuowei2做了简单的统计:部署覆盖了6台服务器,平均每天清理掉大约20GB的过期日志,磁盘使用率从长期90%以上回落到60%-70%波动。最直观的变化是磁盘告警从以前每周至少一次,变成了半年几乎没有误报级别的告警,再也不用半夜爬起来处理“磁盘满了”这种问题。

人工介入次数这个指标更能说明问题:v1时代差不多每个月都要登服务器看一次日志,确认脚本有没有异常;tuowei2上线后,我只需要每周看一眼群里的报表,确认数值在合理区间内。有一次业务方做活动,日志量翻了3倍,tuowei2自动把清理量也翻了3倍,磁盘水位稳定在75%以下,全程没有人工干预。对我来说,这就是一个自动化工具该有的状态——平时感觉不到它的存在,但它在默默兜底。

5.2 哪些设计经受住了考验

回看这半年,有三件事被证明是对的。

第一是白名单护栏。即便有一次我在新服务器上配置路径时手滑写错了根目录,safe_delete依然把所有删除请求挡下了,只报了“路径不在白名单”的警告,没有造成任何损失。那一刻我非常庆幸自己把护栏放在cleaner的最前面,而不是依赖外部调用方的自觉。

第二是dry_run机制。它不只是第一次上线的保险,后来每次调整保留天数或新增扫描路径,我都会先开着dry_run跑一天,看清单和预期一致再切正式模式。这个习惯帮我避开了至少三次配置错误,其中一次是把保留天数从30改成3,dry_run跑出来的清单显示会一次性删掉半个月的日志,我赶紧把配置改了回去。

第三是SQLite历史记录。有一次同事问“上周清理了多少文件”,我直接查库给出一份精确清单,他当时还挺意外。操作的可追溯性,在多人协作环境里比其他所有特性都值钱。工具可以出错,但不能让人查不到是哪里出的错。

5.3 下一步:从清理工具走向容量预测

tuowei2目前的定位还是一个“事后处理”工具——等到磁盘快满了再去清。但日志增长是有规律可循的,我接下来想给collector加一个简单的时间序列分析:记录每个目录每天的增量,用滑动平均预测未来N天的占用,当预测值超过阈值时提前告警,把“等它满了再清”变成“提前规划容量”。

另外一个是清理策略的细化:现在的双条件(天数+大小)对大多数场景够用,但有些业务文件要求按版本保留最新几个,有些要求按月份保留,这需要引入更灵活的文件保留模式。我计划在配置里给cleaner增加一个rule字段,按glob模式做更细粒度的匹配。比如data/backup/*.tar.gz这条规则可以配置成“保留最近3个版本”,而不是单纯按天数切。刚才提到的SQLite历史记录,也可以顺势扩展成趋势分析的底表,这样每个目录的占用变化就变成可查询的历史序列了。

这些改造不一定都落地,但tuowei2这个项目让我深刻意识到一件事:很多运维问题不是靠一个绝妙的脚本解决的,而是靠一次次踩坑、一版版迭代,把防御性的逻辑一层层加进去才变稳的。我现在写完这类工具的代码,第一件事永远不是测业务逻辑通不通,而是先想“它出错的时候会怎样”——出错时是否安全、是否可控、是否能被快速定位。工具真正成熟的标准不是功能多,而是出错时它的反应依然可控。

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

CrewAI多智能体实战:从环境配置到生产级客服分诊系统

1. 为什么是CrewAI?——从5.9万Star看多智能体落地的真正卡点你刷到“开源社区5.9万Star!多智能体框架中文上手教程”这个标题时,第一反应可能是:又一个被营销号带节奏的AI项目?毕竟GitHub上标着“Agent”“Multi-Agen…

作者头像 李华
网站建设 2026/10/1 4:18:52

Go 1.15证书校验变化:从CN到SAN,解决x509报错与自签证书问题

先讲个真实场景:早上刚到工位,组里同事就甩过来一条报错,说 Go 写的内部工具连不上新部署的服务,日志里就这么一句话:verify certificate: x509: certificate relies on legacy Common Name field, use SANs instead这…

作者头像 李华
网站建设 2026/10/1 4:18:46

STM32F103开发全栈指南:从烧录失败到外设精准控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 4:17:58

基于MPC的混合储能微电网双层能量管理:从原理到Matlab实现

这两年做微电网能量管理系统,我最大的感受是:储能配置不是电池越多越好,运行策略再复杂也架不住现场工况多变,而遇到带约束、多目标、时间耦合的优化问题,模型预测控制(MPC)确实比传统PID和规则…

作者头像 李华
网站建设 2026/10/1 4:15:23

最小可用MPC钱包实战:Rust内核与Java编排实现两方签名闭环

做MPC钱包的人,第一句想对同行说的往往是:钱包不是钱包,是把私钥拆了。真正动手实现之后,你还会发现另一件事——把协议讲清楚的人很多,把工程竖起来的人很少。这篇实战文章就是用 Rust 做密码学核心、用 Java 做业务协…

作者头像 李华
网站建设 2026/10/1 4:15:20

石墨烯钙钛矿太阳能电池COMSOL光电热耦合仿真建模全解析

去年我接了一个钙钛矿太阳能电池的仿真项目,一开始只做了单纯的半导体光电模型,J-V曲线算出来看着还行,但把器件放到65度环境下再测,效率掉得比实验快很多,我当时以为是边界条件没设对,反复调了很久也没改善…

作者头像 李华