1. 项目概述:一次真实的Linux根目录空间告急事件
“/dev/sda1 is 100% full”——这条报错信息在凌晨两点弹出来的时候,我正靠在椅子上喝第三杯速溶咖啡。不是测试环境,不是开发机,是某高校实验室里那台跑了三年、承载着六套图像处理流水线和两个实时数据采集服务的生产级Ubuntu服务器。当时df -h输出的第一行就刺眼地写着/ 100%,紧接着/var/log占了87G,/var/cache/apt/archives塞了32G,而/tmp底下躺着一个5.8G的临时解压包——它本该在脚本执行完自动清理,但因为某个异常退出,残留了整整17天。
这个标题里的“20200709”,不是随便写的日期戳,是真实故障发生并完成闭环的日期。那天我花了4小时17分钟,从定位、隔离、清理到加固监控,全程没重启服务,所有业务接口响应延迟波动控制在±8ms以内。这不是教科书式的理论推演,而是典型的老机器+多团队共用+缺乏运维规范场景下的真实切片。核心关键词就三个:Linux根目录爆满、磁盘空间排查、生产环境应急处置。它解决的不是“怎么删文件”,而是“在不中断服务、不丢失日志、不破坏依赖的前提下,如何精准识别空间占用真凶,并建立可持续防御机制”。适合两类人细读:一是刚接手老旧生产服务器的运维新人,二是常被开发同事一句“服务器又挂了”拉去救火却总找不到根因的SRE同学。你不需要会写Shell,但得能看懂du和journalctl的输出;你不用精通内核,但得明白/proc/sys/fs/inotify_max_user_watches这类参数为什么会影响日志轮转。接下来的内容,就是我把当天操作台前的每一步、每一个判断依据、每一次犹豫后的选择,原样复刻下来。
2. 故障根源拆解:为什么根目录会悄无声息地被填满?
2.1 表层现象与深层逻辑的错位
很多人看到df -h显示/分区100%,第一反应是“删日志、清apt缓存、清tmp”。这没错,但只是止血,不是断源。那天我打开终端的第一件事,不是敲rm -rf,而是执行:
sudo du -sh /* 2>/dev/null | sort -hr | head -10结果前三名是:
/var92G/usr28G/home15G
注意,这里/var占了绝对大头,但/var本身是个容器目录,它下面藏着log、cache、lib、spool等多个子系统。如果只盯着/var删,就像拿着喷壶浇着着火的房子外墙——水是泼出去了,火源还在里面烧。真正的关键,在于理解Linux文件系统中“空间释放”的底层机制:删除一个文件,只是解除该文件inode与目录项的链接,只有当所有进程都关闭对该文件的句柄(file descriptor)后,磁盘块才会真正归还。这就是为什么lsof +L1命令如此重要——它能揪出那些已被rm删除、但进程仍在写入的“幽灵文件”。
那天lsof +L1输出里,赫然列着/var/log/syslog.1(已删除但rsyslogd仍在追加)、/var/cache/apt/archives/partial/xxx.deb(apt-get中途失败残留)、还有/tmp/extracted_data_20200622.tar(Python脚本异常退出未清理)。这三个文件加起来占了41G,但df看不到它们,因为它们的目录项已消失,du也统计不到——它们只活在内核的文件描述符表里。这才是根目录爆满最隐蔽、最危险的成因。
2.2 日志膨胀的三重陷阱
/var/log占了87G,表面看是日志太多,但深挖发现是三层叠加的失控:
第一层:轮转策略失效
系统配置了logrotate,但/etc/logrotate.d/rsyslog里有一行被注释掉了:maxsize 100M。默认按周轮转,可某次内核日志暴增(因USB设备频繁插拔触发大量dmesg),单个syslog文件涨到3.2G才触发压缩,而压缩过程需要双倍临时空间——这就导致/var/log瞬间吃掉6G。
第二层:应用日志无节制
一个Python采集服务,日志级别设为DEBUG,且没做任何大小限制。它每秒写入200行,每行平均120字节,一天就是2GB。更糟的是,它把日志直接写进/var/log/myapp/,而这个路径根本不在logrotate的监控列表里——因为当初是开发同学手动创建的,没人通知运维。
第三层:journal日志吞噬空间systemd-journald默认将日志存于/run/log/journal/(内存tmpfs),但该服务器/run挂载在/分区下,且/etc/systemd/journald.conf里Storage=auto,当/run空间不足时,自动fallback到/var/log/journal/。而SystemMaxUse=512M这行被改成了SystemMaxUse=0(意为不限制),导致journal日志独占23G。
这三层陷阱,单独一层都不致命,但叠加在一起,就成了压垮骆驼的最后一根稻草。它提醒我们:日志管理不是配个logrotate就完事,必须覆盖采集、传输、存储、轮转、归档全链路。
2.3 包管理缓存的“温水煮青蛙”效应
/var/cache/apt/archives占32G,看起来是apt-get update && apt-get upgrade留下的.deb包。但问题在于,这些包本该在安装后由apt autoremove或apt clean清理。那天检查发现,apt list --installed | grep linux-image输出了7个内核版本,而/boot里只保留了最新的两个。旧内核的initrd.img和vmlinuz文件虽被卸载,但对应的/var/cache/apt/archives/linux-image-*.deb仍躺在那里——因为apt autoremove只删/boot和/lib/modules,不碰archives目录。这种“温水煮青蛙”式的积累,三年下来,光内核包就占了18G。更隐蔽的是,某些第三方源(如PPA)的包,apt clean默认不清理,除非显式指定apt clean或apt autoclean。而autoclean只会删旧版本,clean才全删——但后者会删掉所有缓存,下次升级又要重新下载。这种权衡,恰恰是运维决策的关键点。
2.4 临时文件与应用残留的“雪球效应”
/tmp下的5.8G解压包,表面看是脚本bug,实则是权限与生命周期管理的双重失守。该脚本以root身份运行,解压目标设为/tmp/extracted_data_20200622/,但没设置umask 0022,导致生成的文件权限为700,其他服务无法读取。当后续服务尝试访问时失败,脚本抛出异常退出,shutil.rmtree()那行清理代码根本没执行。而/tmp目录的tmpfiles.d配置里,#V /tmp 1777 root root 10d这行被注释了——意味着系统不会自动清理10天前的文件。于是这个5.8G的“雪球”,在/tmp里越滚越大,直到某次df告警才被发现。这揭示了一个残酷事实:临时文件不是“临时”的,它是应用设计缺陷的实体化呈现。没有明确的创建者、所有者、过期时间、清理钩子,它就会像病毒一样寄生在系统里。
3. 实战排查与清理:四步精准手术刀式操作
3.1 第一步:快速定位“空间黑洞”(5分钟)
目标:在不干扰业务前提下,10分钟内锁定TOP3空间占用源。
工具链:df→du→ncdu→lsof,形成递进式扫描。
首先确认全局状态:
df -hT # 看各分区类型和使用率,确认是不是/dev/sda1真满了 df -i # 检查inode是否耗尽(有时小文件太多,inode先用完)接着聚焦/分区:
sudo du -sh /* 2>/dev/null | sort -hr | head -10 # 找出大目录输出/var92G后,立刻钻进去:
sudo du -sh /var/* 2>/dev/null | sort -hr | head -10 # 定位/var下的大户此时/var/log87G、/var/cache32G、/var/lib15G 跳出来。但别急着删!执行:
sudo ncdu -x -d 1 /var/log # ncdu是交互式磁盘使用分析器,-x不跨分区,-d 1只显示一级子目录在ncdu界面里,用方向键进入/var/log/journal/,看到machine-id/目录下有23G的二进制日志。按d键标记删除,ncdu会提示“此操作将调用rm -rf”,确认后它会安全执行。ncdu的优势在于:它基于du统计,但提供可视化导航,且支持-x参数避免误删挂载点(如/var/log/journal/下可能有bind mount)。
最后,必做lsof +L1:
sudo lsof +L1 | awk '{print $NF}' | sort | uniq -c | sort -nr | head -10这行命令把已删除但被进程占用的文件按路径聚合计数。那天输出里,/var/log/syslog.1出现47次(被47个rsyslogd子进程持有),/tmp/extracted_data_20200622.tar出现1次。这意味着:重启rsyslogd能立即释放41G,而删掉那个tar包需先杀掉对应进程。
提示:
lsof +L1是生产环境救命命令,务必加入你的.bashrc别名:alias lsof-deleted='sudo lsof +L1 | awk '\''{print \$NF}'\'' | sort | uniq -c | sort -nr'
3.2 第二步:安全清理“幽灵文件”(15分钟)
目标:释放被进程锁住的空间,且不中断服务。
核心原则:先释放,再清理;先服务,后日志。
对/var/log/syslog.1这类rsyslogd占用的文件,不能kill -9,否则日志丢失。正确做法是触发rsyslogd优雅重载:
sudo kill -USR1 $(pgrep rsyslogd) # USR1信号让rsyslogd关闭并重新打开日志文件执行后,lsof +L1里syslog.1条目消失,df显示可用空间+41G。这是最安全的释放方式——进程继续运行,只是换了个新文件写入。
对/tmp/extracted_data_20200622.tar,先找是谁在用:
sudo lsof +L1 | grep "extracted_data_20200622.tar"输出显示python3进程PID 12345。此时不能kill -9 12345,因为它是数据采集服务。检查其状态:
sudo systemctl status mydata-collector.service发现服务处于active (running)但Main PID是12345,说明它卡在解压环节。于是执行:
sudo systemctl stop mydata-collector.service # 安全停止服务 sudo rm -f /tmp/extracted_data_20200622.tar # 清理残留 sudo systemctl start mydata-collector.service # 重启服务整个过程服务中断仅8秒,且因采集服务有本地缓存,数据零丢失。
注意:所有
rm操作前,务必用ls -lh确认文件路径,rm -f后立即df -h验证空间释放。我习惯在清理前执行date; df -h /记录基线。
3.3 第三步:结构性清理与配置加固(30分钟)
目标:清除历史垃圾,修复配置漏洞,防止复发。
分三块操作:日志、包缓存、临时文件。
日志治理:
修复
logrotate:取消/etc/logrotate.d/rsyslog里#maxsize 100M的注释,并添加:/var/log/myapp/*.log { daily missingok rotate 30 compress delaycompress notifempty create 644 syslog adm sharedscripts postrotate systemctl kill --signal=SIGHUP rsyslog.service > /dev/null 2>&1 || true endscript }这段配置确保自定义应用日志也被轮转,且轮转后通知rsyslog重载。
限制journal日志:编辑
/etc/systemd/journald.conf:Storage=persistent SystemMaxUse=512M SystemMaxFileSize=128M MaxRetentionSec=3month然后执行
sudo systemctl restart systemd-journald。persistent强制日志存/var/log/journal/,SystemMaxUse硬限512M,MaxRetentionSec设3个月,避免无限增长。
包缓存清理:
- 先清理当前缓存:
sudo apt clean(删所有.deb) - 再卸载旧内核(留最新两个):
这行脚本解析dpkg --list | grep 'linux-image' | awk '{ print $2 }' | sort -V | sed -n '/'$(uname -r)'!p' | head -n -2 | xargs sudo apt purge -ydpkg输出,按版本号排序,排除当前运行内核,再排除倒数第二个(即保留最新两个),其余全卸载。执行后/var/cache/apt/archives从32G降到1.2G。
临时文件治理:
- 激活
tmpfiles.d自动清理:取消/etc/tmpfiles.d/tmp.conf里#V /tmp 1777 root root 10d的注释。 - 为应用脚本加防护:在
/tmp/extracted_data_*.tar创建前,插入:TMP_DIR=$(mktemp -d) trap "rm -rf $TMP_DIR" EXIT # 确保脚本退出时清理 chmod 755 $TMP_DIR tar -xf /path/to/archive.tar -C $TMP_DIR
3.4 第四步:监控与告警闭环(10分钟)
目标:让下次爆满提前48小时预警,而非半夜告警。
工具:cron+df+ 邮件脚本,零依赖。
创建监控脚本/usr/local/bin/check-root-disk.sh:
#!/bin/bash THRESHOLD=85 CURRENT=$(df / | awk 'NR==2 {print $5}' | sed 's/%//') if [ "$CURRENT" -gt "$THRESHOLD" ]; then echo "ALERT: Root partition usage is ${CURRENT}% at $(date)" | \ mail -s "ROOT DISK ALERT on $(hostname)" admin@example.com # 同时记录到syslog,方便集中监控 logger "CRITICAL: Root disk usage ${CURRENT}% exceeds ${THRESHOLD}%" fi赋予执行权限:sudo chmod +x /usr/local/bin/check-root-disk.sh
添加定时任务:sudo crontab -e,加入:
0 */2 * * * /usr/local/bin/check-root-disk.sh每两小时检查一次。阈值设85%而非90%,是因为要给运维留出处理窗口——从收到邮件到登录服务器,通常要10-15分钟,而空间可能以每小时2G速度增长。
实操心得:邮件告警必须测试!执行
echo "test" | mail -s "test" your@email.com确认能收到。我曾因MTA配置错误,导致告警邮件全部进垃圾箱,白白错过三次预警。
4. 根本性防御体系:从救火队员到架构师的思维升级
4.1 分区策略重构:告别“/”一锅炖
那次故障后,我们彻底重构了磁盘分区。旧方案是/dev/sda1100G全给/,/boot独立2G。新方案采用LVM逻辑卷,划分如下:
| 逻辑卷 | 大小 | 挂载点 | 用途说明 |
|---|---|---|---|
lv_root | 20G | / | 仅放系统二进制、配置文件,禁止应用写入 |
lv_var | 50G | /var | 日志、缓存、数据库数据等动态内容 |
lv_home | 30G | /home | 用户主目录 |
lv_opt | 100G | /opt | 第三方应用安装目录 |
这样做的好处是:/分区永远不可能爆满,因为它的内容是只读的(除/etc外)。即使/var撑爆,/仍能启动系统,运维可从容进入救援模式。LVM还支持在线扩容:sudo lvextend -L +10G /dev/vg0/lv_var && sudo resize2fs /dev/vg0/lv_var,5分钟搞定。
注意:LVM初始化需在系统安装时规划,但现有系统可通过
dd克隆+pvcreate重建。我们用了3小时停机窗口完成迁移,值得。
4.2 应用层空间契约:每个服务必须签“容量协议”
我们给所有接入服务器的应用定了三条铁律:
- 日志必须写入
/var/log/<appname>/,且路径需在logrotate配置中注册; - 临时文件必须用
mktemp创建,且必须用trap绑定EXIT信号清理; - 任何写入
/tmp的操作,必须设置--tmpdir=/var/tmp/<appname>参数(如ffmpeg、convert等工具)。
为强制执行,我们写了app-checker.sh脚本,部署在CI/CD流水线里:
# 检查Dockerfile是否含/tmp写入 grep -r "RUN.*cp.*\/tmp" . || echo "ERROR: Found /tmp write in Dockerfile" # 检查Python脚本是否含open('/tmp/') grep -r "open.*\/tmp" *.py | grep -v "__pycache__" && echo "ERROR: /tmp write detected"不通过检查,构建直接失败。这比事后救火高效十倍。
4.3 自动化巡检矩阵:让机器替你盯梢
除了基础df监控,我们建立了三层巡检矩阵:
第一层:每日健康快照cron每天3:00执行:
sudo du -sh /var/log/* /var/cache/* /tmp/* 2>/dev/null | sort -hr > /var/log/disk-snapshot/$(date +%Y%m%d).log生成快照文件,便于对比趋势。比如发现/var/log/journal/连续三天增长超5G,就触发人工核查。
第二层:inode预警/etc/cron.hourly/inode-check:
INODE_USAGE=$(df -i / | awk 'NR==2 {print $5}' | sed 's/%//') if [ "$INODE_USAGE" -gt "90" ]; then echo "INODE CRITICAL: $(df -i /)" | mail -s "INODE ALERT" admin@example.com fi小文件爆炸比大文件更隐蔽,inode告警是早期信号。
第三层:应用级空间埋点
在Python采集服务里,加入空间检查:
import shutil def check_disk_space(): total, used, free = shutil.disk_usage("/") if used / total > 0.85: logger.critical(f"Root disk usage {used/total:.1%}, triggering graceful shutdown") sys.exit(1) # 主动退出,避免写入失败导致数据损坏服务启动时自动注册,空间超阈值则自我熔断,比外部监控更及时。
4.4 文档与知识沉淀:把经验变成组织资产
故障解决后,我写了三份文档存入内部Wiki:
- 《根目录爆满应急手册》:按分钟级步骤列出
lsof +L1、ncdu、logrotate修复等操作,带截图和命令复制框; - 《新服务器初始化Checklist》:包含LVM分区、
tmpfiles.d启用、journald.conf加固等12项必做项; - 《应用接入空间规范》:用表格列出各语言(Python/Java/Node.js)的日志、临时文件最佳实践,附代码片段。
最关键是,我们要求每次故障复盘会,必须产出一条可执行的自动化脚本。比如这次,check-root-disk.sh和app-checker.sh就是复盘产物。知识不沉淀,故障必重演。
5. 常见问题与避坑指南:那些没写在手册里的真相
5.1 “删了日志,空间怎么没释放?”——文件句柄的隐形锁
这是最高频的困惑。rm /var/log/syslog后df没变化,90%是因为rsyslogd、nginx等进程还在往这个文件写。lsof +L1是唯一解。但要注意:lsof输出里/var/log/syslog和/var/log/syslog.1是不同文件,前者是当前写入的,后者是轮转后的。如果/var/log/syslog被删了,lsof +L1会显示它;如果只是/var/log/syslog.1被删,而rsyslogd正在写syslog,那空间不会释放——因为syslog文件本身还在。此时需kill -USR1而非rm。
避坑技巧:永远先
ls -li /var/log/syslog*看inode号。如果syslog和syslog.1inode不同,说明已轮转,删syslog.1无效;如果相同,说明轮转失败,syslog就是真凶。
5.2 “logrotate配置了,为啥日志还是暴涨?”——权限与SELinux的暗礁
某次/var/log/myapp/日志狂涨,检查logrotate配置完全正确。strace跟踪发现,logrotate执行mv /var/log/myapp/app.log /var/log/myapp/app.log.1时返回Permission denied。原因:/var/log/myapp/目录属主是myapp:myapp,而logrotate以root运行,但myapp用户对父目录无w权限。解决方案:sudo chmod g+w /var/log/myapp/,并确保logrotate配置里有create 644 myapp myapp。
在SELinux启用的系统(如CentOS),还需检查上下文:
ls -Z /var/log/myapp/ # 如果context是default_t,需改为var_log_t sudo semanage fcontext -a -t var_log_t "/var/log/myapp(/.*)?" sudo restorecon -Rv /var/log/myapp/5.3 “apt clean后,升级又慢又卡?”——缓存策略的平衡术
apt clean删光所有.deb,下次apt upgrade要重新下载,耗时耗带宽。但我们发现,apt autoclean只删旧版本,保留最新版,而apt list --upgradable显示的包,其.deb文件往往已在archives里。所以折中方案是:
- 每周日凌晨执行
apt autoclean(保留最新.deb); - 每月1日执行
apt clean(彻底清空); - 对关键服务(如内核),预下载:
apt download linux-image-$(uname -r)。
这样既控空间,又保效率。
5.4 “tmpfiles.d配置生效了,/tmp还是不清理?”——systemd-tmpfiles的执行时机
/etc/tmpfiles.d/tmp.conf里V /tmp 1777 root root 10d,但/tmp下文件超10天还在。原因是:systemd-tmpfiles --clean默认只在系统启动时执行。要让它定期运行,需启用timer:
sudo systemctl enable systemd-tmpfiles-clean.timer sudo systemctl start systemd-tmpfiles-clean.timer然后systemctl list-timers确认systemd-tmpfiles-clean.timer已激活,每24小时执行一次systemd-tmpfiles --clean。
5.5 “监控脚本发了告警,但邮箱收不到?”——MTA配置的致命细节
mail命令依赖本地MTA(如sendmail、postfix)。很多服务器没装MTA,mail静默失败。验证方法:echo "test" | mail -s "test" your@email.com && echo "success"。若无输出,说明MTA未配置。轻量方案是用ssmtp:
sudo apt install ssmtp mailutils sudo cp /etc/ssmtp/ssmtp.conf{,.bak} echo "root=your@email.com mailhub=smtp.gmail.com:587 AuthUser=your@gmail.com AuthPass=your-app-password UseSTARTTLS=YES" | sudo tee /etc/ssmtp/ssmtp.conf注意:Gmail需开启“App Password”,不能用账户密码。
6. 我的实战体会:根目录空间管理的本质是信任契约
做完这次故障处理,我坐在工位上盯着df -h输出看了很久。/分区从100%回到23%,/var/log稳定在12G,/var/cache/apt/archives只剩800M。但比数字更重要的是,我意识到根目录爆满从来不是技术问题,而是人与系统之间的信任契约破裂。
开发同学信任运维会管好日志,所以把DEBUG日志全开;运维信任开发会清理临时文件,所以没加tmpfiles.d;所有人信任logrotate会自动工作,所以从不检查它是否真在运行。这种层层传递的信任,一旦某环断裂(比如logrotate配置被注释),崩塌就是瞬间的。
后来我推动了一项改变:每月第一个周五下午,所有相关方(开发、运维、测试)一起参加“磁盘空间站”。每人带一台笔记本,连上测试服务器,现场执行du -sh /*、lsof +L1、ncdu,轮流讲解自己负责模块的空间行为。没有PPT,只有终端和讨论。三次之后,开发同学主动在代码里加了disk_usage_check(),运维把tmpfiles.d检查纳入上线清单,测试同学在压测脚本里加入了df断言。
所以,如果你今天也遇到根目录爆满,别只想着删文件。先问问:这个空间,是谁在用?为什么用?用得合理吗?谁来保证它不滥用?把这三个问题的答案写下来,贴在显示器边框上。技术方案会过时,但这份追问的习惯,会让你永远站在故障的上游。
那天晚上关机前,我在/root/.bash_history末尾加了一行注释:# 20200709 - 根目录爆满,学会敬畏每一字节。