ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux磁盘告警排查与治理:journald、logrotate与rsync备份

Linux磁盘告警排查与治理:journald、logrotate与rsync备份

整理2026年1月7日的Linux笔记时,翻到了当天下午处理一次磁盘告警的完整过程:从df排查、du定位,到揪出journald日志黑洞,再到顺手补上logrotate、临时目录清理和rsync异地备份。事情本身不复杂,但把整条链路串起来以后,对“Linux服务器上磁盘到底是怎么被占满的”这件事有了更整体的认识。这篇笔记拆解开写,既是给自己留档,也给同样做运维、折腾自建服务器的朋友一个可直接照抄的排查思路。

1. 从磁盘告警开始:df、du、inode三维定位

1.1 接到告警后,先看清楚“满的是哪个分区”

2026年1月7日午休刚过,值班群里弹出一条告警:web-01的/data分区使用率已超过90%。我上服务器后的第一反应不是直接去翻监控图表,而是先执行了df -hT,确认现在是哪个文件系统、哪个挂载点在报警。

df -hT

输出大概长这样:

文件系统 类型 容量 已用 可用 已用% 挂载点 /dev/sda2 xfs 200G 180G 20G 91% /data

这里有两个信息值得留意:一是文件系统类型,xfs和ext4在后续扩容、备份策略上差别很大;二是挂载点,很多人只看“磁盘满了”,却不看是哪个分区满了,导致把日志清理工具跑错地方。比如这次报警明确是/data分区,系统盘/还有剩余空间,那么mlocate、apt缓存这些占系统盘的东西就不该优先排查。

很多监控系统只报了“磁盘使用率”,没报inode、没报具体目录。所以我建议每次告警都保留一个固定动作:df -hT和df -i两行连敲,先把“容量”和“文件数量”两个维度都看清楚,避免被表象误导。这也是后面踩坑的主要教训。

1.2 du逐级下钻,把最大的目录找出来

确认是/data分区吃紧后,我用了du从根上往下钻。平时排查我习惯先看一级目录的总量,再决定往哪里走,而不是一上来就对所有文件做全盘扫描。

du -x -h --max-depth=1 /data 2>/dev/null | sort -rh | head -20

-x的作用是不要跨越文件系统,防止把其他挂载点重复统计进来;2>/dev/null是因为有些目录没有读取权限,报错信息会干扰排序输出。第一次执行后,最扎眼的是/data/docker,占了差不多140G,其次是/data/logs,约30G。

继续往/data/docker里钻,发现大部分空间是容器日志和镜像层文件占掉的。一个容器如果在启动时没有显式挂载日志目录,docker默认会把日志写到/var/lib/docker/containers/<容器ID>/*-json.log,这是非常经典的隐藏空间杀手。

到这里,我基本判断出问题有两层:大文件撑满了容量,大量小文件则可能撑爆inode。所以我没有急着删东西,而是先查了文件数量的水位。

1.3 意外发现:inode使用率已经到了99%

df -i这个命令很容易被新手忽略,但生产环境里“容量没满、却写不进文件”的诡异故障,十有八九和它有关。

df -i /data

看到结果是:

文件系统 Inodes 已用(I) 可用(I) 已用(I)% 挂载点 /dev/sda2 3000000 2970000 30000 99% /data

inode可以理解成文件系统里每一条文件的“索引卡”。一个分区能创建多少个文件,在上层看是无限,实际上受inode数量限制。就像停车场已经停满了车,入口显示屏再写“剩余车位0”,外面还有空地的意思一样——不是没有空间,是没法再放进新的车了。

当时/data下的小文件大多集中在应用缓存目录里,像是图片缩略图、临时生成的Session文件、未清理的任务队列数据。这也解释了为什么du看容量没那么吓人,但应用却报“No space left on device”。

2. 真正的元凶:systemd-journald日志积压

2.1 journald为什么能安安静静吃掉几十G

在很多人印象里,用systemd的系统日志都在journalctl里管着,平时命令敲两下能查到就行,很少去管它占多大地方。但journald的默认策略其实是“按磁盘空间比例兜底”,不是“按日志天数兜底”。

如果你的/var/log/journal目录被独立放在哪个分区里,或者根分区比较大,journald日志甚至可能积压到数十GB。它默认允许使用磁盘空间的10%(略保守的说,不同发行版默认值有差异,但整体思路一致),而且没有设置“保留天数”时,老日志就像旧报纸一样,只进不出。

我在web-01上执行了这条命令确认现状:

journalctl --disk-usage

返回的数字是4.2G。放到整个/data分区里看不算特别夸张,但它属于“今天不处理、明天继续涨”的持续性问题,必须设一个天花板。

2.2 现场清理:vacuum是真能立刻释放空间的

要临时腾出空间,可以直接用journalctl --vacuum-size或--vacuum-time让日志“瘦身”。

journalctl --vacuum-size=200M journalctl --vacuum-time=7d

--vacuum-size=200M的意思是:不管现在日志多大,只保留最近写入的200MB,其余全部删掉。--vacuum-time=7d则是按时间线清理,只保留最近7天以内的日志。对一台业务服务器来说,保留7天通常足够回溯问题,超过7天的日志价值很低,而且以后也很难有人真的去翻。

执行完后,磁盘立刻释放了大约3.9G。如果想确认是不是真的成功,再跑一次journalctl --disk-usage,看到数值掉到符合预期的范围内即可。

2.3 改配置文件,给日志总量装上“天花板”

临时清理只能救急,真正避免以后再犯,是把journald的容量上限写进配置里。我当时编辑了/etc/systemd/journald.conf:

[Journal] Storage=auto Compress=yes SystemMaxUse=200M RuntimeMaxUse=100M MaxRetentionSec=7day MaxFileSec=1day

简单解释一下这几个参数的含义:

  • Storage=auto:日志优先写入内存,目录空间紧张时才持久化到磁盘,比较适合服务器场景。
  • SystemMaxUse=200M:持久化日志总大小上限。所有journal文件加起来超过200MB,就自动清理旧日志。
  • RuntimeMaxUse=100M:内存日志上限。如果服务器不常重启,这个值甚至可以再小一点。
  • MaxRetentionSec=7day:超过7天的日志,一旦触发写入就自动删除。
  • MaxFileSec=1day:单个日志文件最多写一天就切换新文件,方便以后按天排查。

修改配置后重启服务:

systemctl restart systemd-journald

重启后再次确认:

journalctl --disk-usage

只要配置生效,日志总量就永远被压在200MB以内。这一步比任何“今天手动删一下”都关键——不设上限,下次还是会在某个月圆之夜把磁盘塞满。

3. 清理动作之外,顺手补上的三道防线

3.1 logrotate接管应用日志,别再让日志裸奔

清完journald,我顺便检查了应用日志目录,发现/data/logs下面很多日志文件还在裸奔:没有轮转、没有压缩,一个Tomcat应用的catalina.out已经写到了4G多。这种情况光靠手动清理根本不现实,应该交给logrotate。

我在/etc/logrotate.d/下写了一个专门针对该应用的配置:

/data/logs/app/*.log { daily rotate 14 compress delaycompress missingok notifempty copytruncate }

这里有几个参数值得展开:

  • daily:每天轮转一次。
  • rotate 14:保留14份历史日志。
  • compress:轮转后的日志用gzip压缩。
  • delaycompress:轮转后不立即压缩昨天的日志,因为应用可能还在写文件句柄,晚一天压缩更保险。
  • copytruncate:复制当前日志内容后清空原文件,适合应用无法通过信号重开日志文件的情况,Tomcat这类进程就很适用。

配置完成后,建议先“空跑”验证一下:

logrotate -d /etc/logrotate.d/app

-d是debug模式,只打印要做什么,不真正执行。确认无误后,再手动强制执行一次:

logrotate -f /etc/logrotate.d/app

执行完就会发现那些“年份级”的巨型日志被切割成一天一个文件,磁盘占用立刻下降。

3.2 临时目录清理:systemd-tmpfiles的默认规则

磁盘清理的第二道防线,是处理/tmp和/var/tmp这类临时目录。很多人只知道/tmp会定期被清,但清多久一次、由谁清,基本不清楚。

systemd里有systemd-tmpfiles-clean.timer这个定时器,它按照/usr/lib/tmpfiles.d/tmp.conf里的规则定期清理临时目录。默认策略大致是:/tmp下超过10天没动过的文件会被删,/var/tmp则放宽到30天,因为有些程序(比如编辑器恢复文件、安装包解压残留)生命周期比较长。

这次我在服务器上手动扫了一圈,把已经超过30天且明显没用的zip包、安装脚本、临时导出文件列出来:

find /var/tmp -xdev -type f -mtime +30 -exec ls -lh {} \; 2>/dev/null

先看清单,确认没问题再执行删除。我不建议直接find -delete,万一误删了另一个服务正在读取的文件,代价会很大。

3.3 用tar做一次配置基线快照

磁盘清理过程中改了不少配置,包括journald.conf、logrotate规则,还有应用启动脚本。按照我的习惯,凡是涉及“动配置”的维护操作,做之前都要先留下一份可回滚的快照。

考虑到web-01的配置目录都不大,我选了tar打包的方式,把关键目录存到另一个独立目录里:

mkdir -p /data/backup/manual_20260107 tar czvf /data/backup/manual_20260107/web01_etc_$(date +%F).tar.gz \ /etc/systemd/journald.conf \ /etc/logrotate.d/app \ /etc/nginx/nginx.conf \ /etc/nginx/conf.d/ \ /etc/app/app.properties

注意,这个快照没有放在/data根目录下,而是放进了独立的/data/backup目录。如果快照和原数据在同一个分区,磁盘满了的时候连备份也写不进去,就失去了兜底的意义。

4. 异地备份:rsync增量同步与定时任务

4.1 为什么选rsync而不是cp

磁盘清理完之后,我发现web-01缺少一套“自动到异地”的数据备份方案。以前是每周有人手动复制一次,不仅麻烦,而且容易漏。这次我直接把备份方案升级为rsync增量同步。

rsync和cp最本质的区别在于增量同步。第一次全量传完数据之后,第二次再跑的时候,它只传输有变化的部分,而且会对比文件大小、修改时间,跳过没改过的文件。对大目录来说,这个优势非常明显,第一次可能传了2小时,第二次往往只需要几十秒。

推荐的基础参数组合:

rsync -avz -H --numeric-ids \ --delete \ -e "ssh -p 22022" \ /data/backup/web01/ \ backup@192.168.8.20:/backup/web01/

参数含义拆开看:

  • -a:归档模式,保留权限、属主、时间戳、软链接等属性。没有这个参数,备份过去只是“文件内容一样”,权限和属主可能全乱。
  • -v:显示传输过程。
  • -z:传输时压缩,适合文本类文件。
  • -H:保留硬链接,防止目录里的硬链接关系被复制成多个独立文件。
  • --numeric-ids:以数字UID/GID传输,避免两端/etc/passwd不一致时属主错乱。
  • --delete:让目标目录与源目录保持完全一致,源端删除的文件,目标端也会删。
  • -e "ssh -p 22022":指定SSH端口,避免默认端口被安全策略挡掉。

执行前--delete要极度谨慎。如果源目录路径写错,比如少了结尾的/、漏了一层子目录,rsync会把目标目录里不该删的文件也删掉。我的习惯是先加--dry-run -v跑一遍看输出,确认删除名单里没有意外项,再正式执行。

4.2 备份之后要验证,别让备份变成“心理安慰”

rsync跑完不等于备份完成。我见过不少人备份脚本写得很好,但恢复的时候才发现备份目录是空的、半截的、权限不对的。

那次我做了两件验证的事:

先做一次“差异对账”,用rsync的dry-run模式看两端还有哪些差异:

rsync -avn --delete /data/backup/web01/ backup@192.168.8.20:/backup/web01/

命令里的-n只做模拟运行,不实际传输。输出里如果还有文件列表,说明两端的同步还没真正完成;没有文件列表,则说明已经一致。

再抽查几个关键文件的md5值:

md5sum /data/backup/web01/app/etc/app.properties ssh backup@192.168.8.20 "md5sum /backup/web01/app/etc/app.properties"

两端MD5一致,才能确认文件传过去之后没有损坏。这一步看起来繁琐,但真正遇到“备份回滚”的时候,能救命。

4.3 crontab定时任务:把备份变成自动化

手动同步一次只能代表当天做了,以后每天都要做,就得交给cron。

我先写了一个备份脚本/root/scripts/backup_web01.sh:

#!/bin/bash export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LOCK_FILE="/var/lock/backup_web01.lock" exec 9>"$LOCK_FILE" flock -n 9 || exit 1 rsync -avz -H --numeric-ids --delete \ -e "ssh -p 22022" \ /data/backup/web01/ \ backup@192.168.8.20:/backup/web01/ >> /var/log/backup_web01.log 2>&1

脚本里用flock做互斥锁,防止上一次rsync还没跑完、下一次任务又被触发。如果网络很慢或者数据量突然变大,两轮任务重跑会互相干扰,甚至产生错乱数据。

然后把定时任务写进crontab:

30 1 * * * /root/scripts/backup_web01.sh

意思是每天凌晨1:30执行一次。日志统一写到/var/log/backup_web01.log,方便第二天检查。

这里需要特别提醒:cron环境里的PATH很干净。如果之前手动在终端能执行rsync,但在cron里报rsync: command not found,基本就是PATH没带上。脚本开头export PATH或者调用时写绝对路径,都能解决这个问题。

5. 复盘:这次踩过的三个坑和监控改进

5.1 容量还有5GB,应用却报“磁盘满”

这次排查里最浪费时间的一步,是我在du阶段花了很久去找大文件,却没一开始就意识到web-01的问题是inode耗尽。当时应用已经报No space left on device了,可df -h明明显示/data还有5GB可用空间。

问题的本质之前说过:分区里的文件数是有限制的,不能因为容量没满就以为万事大吉。后来我用du --inodes重新定位了一下:

du --inodes -x /data 2>/dev/null | sort -rh | head -15

这行命令会按“目录下的inode数量”排序,很快就能看出哪个目录堆积了海量小文件。那次定位到的是一个消息队列的持久化目录,里面上百万个任务数据文件,每个只有几十字节,但占满了inode索引。

建议所有运维同学,把df -i的检查加入日常巡检脚本。不要等到“容量尚可、但写不进文件”才发现,那通常已经影响业务了。

5.2 手贱用rm删日志,真正的磁盘占用一点没降

清理应用日志时,我踩过一个典型坑:直接用rm -rf /data/logs/app/catalina.out删掉日志文件,结果df -h一看,磁盘占用几乎没变化。

原因是:进程还在继续往这个文件里写。Linux下如果一个文件被进程打开,通过rm删除后,文件并不会立刻消失,空间要等进程关闭文件句柄或进程退出后才释放。这种场景下,更稳妥的做法是用truncate清空文件内容:

truncate -s 0 /data/logs/app/catalina.out

truncate保留文件本身和文件句柄,只是把内容清空,进程可以继续写,磁盘空间也立刻释放。如果应用支持重开日志文件,比如nginx,还可以用信号通知它重新打开文件句柄,然后旧的日志再交给logrotate归档。

5.3 把本次经验固化到巡检脚本里

处理完整台机器,我顺手把这次的经验做成一个小巡检脚本/root/scripts/disk_health_check.sh,以后每周自动跑一遍。核心就是把手动敲的命令串起来:

#!/bin/bash echo "===== $(date '+%F %T') =====" df -hT /data df -i /data journalctl --disk-usage 2>/dev/null find /data -xdev -type f -size +500M -exec ls -lh {} \; 2>/dev/null | head -20

我看的不仅是输出,还有阈值。比如inode使用率超过85%就会列为预警,容量使用率超过80%就进入重点排查名单。监控系统里我也额外增加了两个自定义指标:/data分区inode使用率和journald日志目录大小,这两项是这次事故的直接导火索,以前完全没盯着。

顺便说一句,这类巡检脚本不用写得花哨,核心是每次执行都能稳定跑完、输出一眼能看懂。我见过有人把脚本写得很长很复杂,结果半路报错,后面所有检查项全部挂掉,反而成了新的告警源。

这次处理下来,我最深的体会是:磁盘问题从来不是“删几个文件”这么简单,它是一个需要容量、inode、日志策略、备份链路整体考虑的长期工程。df和du只是入口,真正的坑都在“默认配置”里。最值钱的不是当天腾出了几个G,而是把journald、logrotate、定时备份这些机制一次性补齐了,以后同类问题能被自动挡在门外。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进