2023 年,一台服务器的磁盘把我整不会了。半夜告警:根分区使用率 95%。我登上去 df -h 一看,/ 确实快满了。我心想,简单,无非是哪个日志没轮转,撑大了。我去翻应用的日志目录 /var/log/myapp/——很干净,日志轮转配置好好的,每个文件都不大,加起来也就几百兆。我又去翻 /tmp、翻用户家目录、翻各种我能想到的"爱长胖"的地方,全都正常。可 df 的那个数字明晃晃地挂在那:磁盘,就是满了。这就怪了——我把所有"我知道会变大的东西"都查了一遍,没有一个能解释这几十 GB 的占用。磁盘上明明堆着几十个 GB 的东西,可我对它们【一无所知】,我甚至想不出它们【可能是什么】。后来我用 du 老老实实地从根目录往下一层层扒,扒到 /var/log/journal/ 这个目录时,数字跳出来:22G。我愣住了。这个目录,我从来没有主动往里写过一个字节,我甚至不记得我"创建"过它,这么多年我从来没有打开看过它一眼。它就这么安安静静地待在那儿,以一种我完全没有察觉的节奏,一天天地变大,直到有一天,它把整块磁盘吃到见底。我盯着那个 22G,第一次开始意识到:一台服务器上,原来还跑着一些【我从没下令、却一直在消耗资源】的东西。这件事逼着我把 systemd 的 journald、二进制日志、日志的上限配置这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7 / systemd 系统,根分区告警 95%
事故现象:
- df 显示根分区快满,使用率 95%
- ★ 应用日志、/tmp、家目录都查过,都正常,不大
- ★ 几十 GB 的占用,完全不知道是什么
现场排查:
# 1. 确认是哪个分区满了
$ df -h
文件系统 容量 已用 可用 已用% 挂载点
/dev/vda1 40G 38G 1.6G 96% / # ★ 根分区
/dev/vdb1 100G 12G 83G 13% /data
# 2. ★ 从根目录往下,逐层 du 找大目录
$ du -h --max-depth=1 / 2>/dev/null | sort -rh | head
22G /var
12G /usr
3.1G /home
...
$ du -h --max-depth=1 /var 2>/dev/null | sort -rh | head
22G /var/log # ★ 在 /var/log
$ du -h --max-depth=1 /var/log 2>/dev/null | sort -rh | head
22G /var/log/journal # ★ 就是它!
180M /var/log/myapp
12M /var/log/messages
# 3. ★ 看 journal 到底占了多少
$ journalctl --disk-usage
Archived and active journals take up 22.0G in the file system.
# ^^^^^ ★ systemd 日志吃了 22G
# 4. ★ 看 journald 的配置
$ cat /etc/systemd/journald.conf | grep -v '^#' | grep -v '^$'
[Journal]
# ★ 空的!所有配置项都是注释掉的 = 全用默认值
# 5. 看 journal 日志的时间跨度
$ journalctl --list-boots | head
... 一直列到几百天前 ... # ★ 几百天的日志全堆着
根因(后来想清楚的):
1. ★ systemd 系统上,有一个【系统级的日志服务】
叫 journald。它默默地把系统里几乎所有东西的
日志(内核、服务、登录、cron...)都收集起来,
存进 /var/log/journal/ 这个目录。
2. ★ 这个目录,不是我创建的,日志也不是我写的。
是 journald 这个服务【自动】在做的事。我从来
没下过命令,它一直在后台默默地记录、默默地攒。
3. ★ journald 存日志,本该有"上限"。但上限的
默认值,是【按磁盘容量百分比算】的(默认可
占到文件系统的 10%),而且老系统配置经常是
空的、纯默认 —— 上限要么很大,要么没拦住。
4. 于是几百天的系统日志,一点点累积,从几百 MB
长到 22G,把根分区吃光。它涨得很慢,慢到
监控和我都没注意,直到逼近 100%。
5. ★ 我一直在找"我知道的会变大的东西",可这个
东西,我【根本不知道它的存在】 —— 这才是我
查了一圈一无所获的原因。
不是磁盘被谁恶意占了,是一个我不知道的系统服务,在默默攒日志。
修复 1:磁盘满了——先定位,别瞎猜
# === ★ 磁盘满,有一套固定的定位流程,别凭印象猜 ===
# === ★ 第一步:df —— 确认是哪个分区满了 ===
$ df -h
文件系统 容量 已用 可用 已用% 挂载点
/dev/vda1 40G 38G 1.6G 96% /
# ★ 看清楚是【哪个挂载点】满了。是 / 还是 /data
# 还是 /var —— 这决定了你接下来该去哪里找。
# ★ 别一上来就 du,先用 df 锁定分区。
# === ★ 第二步:du 逐层下钻,找出大目录 ===
# 从那个满了的挂载点开始,一层层往下扒:
$ du -h --max-depth=1 / 2>/dev/null | sort -rh | head
# --max-depth=1 :只看当前这一层的各个子目录大小
# sort -rh :按人类可读的大小【倒序】排
# 2>/dev/null :忽略 /proc 之类的权限报错
# ★ 看排第一的是谁,然后 cd 进去,再 du 一次,
# 一层一层逼近,直到找到那个"罪魁"目录。
$ du -h --max-depth=1 /var 2>/dev/null | sort -rh | head
$ du -h --max-depth=1 /var/log 2>/dev/null | sort -rh | head
# ★ 这样一路下钻,精准、不漏。
# === ★ 第三步:如果是日志类目录,确认是不是 journal ===
$ journalctl --disk-usage
Archived and active journals take up 22.0G ...
# ★ 这条命令直接告诉你 systemd 日志占了多少。
# 一上来就该想到它 —— 它是个很容易被忽略的大户。
# === ★ 一个容易踩的坑:df 和 du 对不上 ===
# 如果 df 说满了,du 加起来却小很多 —— 那是另一个
# 问题:有【已删除但仍被进程占用】的文件。查:
$ lsof +L1 2>/dev/null | grep -i deleted
# ★ 本文这次不是它,本文 du 是能对上的 —— 就是
# journal 实打实地占了 22G。
# === ★ 别忘了还有 inode 耗尽这种"另类满" ===
$ df -i
# ★ 如果 df -h 显示有空间,却报 No space left,
# 看 df -i,可能是 inode 用光了(海量小文件)。
# === 认知 ===
# ★ 磁盘满的定位流程是固定的:df 锁分区 -> du 逐层
# 下钻找大目录 -> 对症处理。不要靠"我觉得是哪个
# 日志"去猜 —— 真正吃掉磁盘的,往往是你压根没
# 想到、甚至不知道存在的东西。
修复 2:journald 是什么——一个你没留意的日志服务
# === ★ 理解 journald:systemd 自带的系统日志中枢 ===
# === ★ journald 是什么 ===
# 在用 systemd 的现代 Linux 上,有一个系统服务叫
# systemd-journald。它的职责是:
# ★ 【自动收集】系统里几乎所有的日志 —— 内核
# 消息、各个服务的输出、用户登录、cron 任务、
# systemd 自己的事件 —— 全部汇总到一处。
$ systemctl status systemd-journald
● systemd-journald.service - Journal Service
Active: active (running) ... # ★ 它一直在跑
# === ★ 你平时用的 journalctl,看的就是它 ===
$ journalctl -u nginx # 看 nginx 服务的日志
$ journalctl -k # 看内核日志
$ journalctl -f # 实时跟踪(类似 tail -f)
# ★ journalctl 是"读" journald 收集的日志的工具。
# 你用它的时候,其实就在和 journald 打交道。
# === ★ 关键:它把日志存成【二进制文件】 ===
# journald 的日志,不是 /var/log/messages 那种
# 纯文本。它是【二进制格式】,存在:
$ ls /var/log/journal/
<一串机器ID>/
$ ls /var/log/journal/*/
system.journal user-1000.journal
system@xxxx.journal ... # ★ 一堆 .journal 文件
# ★ 这些 .journal 文件,你 cat 它是乱码 —— 必须
# 用 journalctl 来读。它们攒起来,就是那 22G。
# === ★ 它和 /var/log/messages 是什么关系 ===
# 老系统的 rsyslog 写纯文本(/var/log/messages 等)。
# 现代 systemd 系统上,常常是【两套并存】:
# - journald:收集所有日志,存二进制(/var/log/journal)。
# - rsyslog :从 journald 拿一份,再写成传统纯文本。
# ★ 所以同一条日志,可能在两个地方各存了一份 ——
# 两边都可能占空间,都要管。
# === ★ 存哪里,取决于 Storage 配置 ===
# journald 的日志存在哪,看 journald.conf 的 Storage:
# - persistent:存到磁盘 /var/log/journal/ —— ★ 会
# 持久累积,就是本文这次。
# - volatile :只存内存 /run/log/journal/,重启就没,
# 【不吃磁盘】。
# - auto(默认):/var/log/journal/ 目录存在就持久,
# 不存在就 volatile。
# ★ 很多系统这个目录一旦被建出来,就一路 persistent
# 地涨下去了。
# === 认知 ===
# ★ journald 是 systemd 系统上一个【一直在后台
# 默默工作】的日志服务,它把系统日志存成二进制、
# 攒在 /var/log/journal/。你没配置它,不代表它
# 没在干活 —— 它一直在,而且一直在变大。
修复 3:它为什么会无限膨胀
# === ★ 搞清楚:journal 凭什么能涨到 22G ===
# === ★ journald 本来是有"上限"机制的 ===
# 它不是完全不管大小。它有一套配额参数(下一节细讲),
# 会在超过上限时自动删掉最老的日志。
# ★ 所以理论上它不该撑爆磁盘。问题在于:这个上限的
# 【默认值】,常常拦不住。
# === ★ 默认上限是"按磁盘比例"算的 ===
# 关键参数 SystemMaxUse 的默认值不是一个固定数字,
# 而是:★【该文件系统总容量的 10%】,但最多 4G。
# 听起来有上限,但:
# - 大磁盘上,10% 也是很大一个数。
# - ★ 更要命的:它算的是 journal 所在分区的容量。
# 如果 /var/log/journal 和 / 是同一个分区,
# "10%" 是按整个根分区算 —— 而根分区上还有
# 系统、应用、其他一切。journal 占满它那 10%
# /4G 时,可能已经把本就紧张的根分区压垮了。
# === ★ 老系统的配置文件,往往是【全空的】 ===
$ grep -v '^#' /etc/systemd/journald.conf | grep -v '^$'
[Journal]
# ★ 除了一个 [Journal] 段名,什么都没有 —— 所有
# 配置项都还是被注释掉的状态,全部走默认值。
# ★ 也就是说:从装好系统那天起,就【没人管过】
# 这个日志的大小,它一直按那个宽松的默认在涨。
# === ★ 为什么涨这么慢却没人发现 ===
# journal 不是一天暴涨的,它是【涓涓细流】:
# - 每天系统、服务产生的日志,可能就几十 MB。
# - 一个月下来一两个 G,一年下来一二十个 G。
# ★ 涨得太慢、太稳,慢到磁盘监控的"日增长"几乎
# 看不出异常,慢到你以为磁盘占用"一直就这样"。
# 直到某天它逼近 100%,才突然引爆。
# === ★ 看 journal 跨了多长时间 ===
$ journalctl --list-boots | wc -l
247 # ★ 存了 247 次开机以来的日志
$ journalctl | head -1
-- Logs begin at Mon 2022-09-xx ... # ★ 一年多前的日志还在!
# ★ 一年多的日志,一条没删,全堆着 —— 这就是 22G
# 的来历。绝大多数早就没有任何排查价值了。
# === 认知 ===
# ★ journal 膨胀,不是因为它"没有上限",而是因为
# 默认上限太宽松(按磁盘 10% 算)、加上老系统
# 根本没配过 journald.conf。它涨得极慢极稳,
# 藏在监控的噪声里,直到撑爆磁盘那天才现身。
修复 4:立刻止血——清掉过量的 journal 日志
# === ★ 磁盘告警时,先把空间腾出来(止血) ===
# === ★ 先看清现状 ===
$ journalctl --disk-usage
Archived and active journals take up 22.0G ...
# === ★ 止血法 1:按大小清——只保留指定大小 ===
$ journalctl --vacuum-size=500M
Deleted archived journal .../system@xxx.journal (128.0M)
Deleted archived journal .../system@yyy.journal (128.0M)
...
Vacuuming done, freed 21.5G of archived journals.
# ★ --vacuum-size=500M:把 journal 总大小压到 500M
# 以内,多出来的、最老的,直接删掉。立竿见影。
# === ★ 止血法 2:按时间清——只保留近 N 天 ===
$ journalctl --vacuum-time=7d
# ★ 删掉 7 天以前的所有 journal 日志。
# 2d / 1weeks / 1months 都行。
# === ★ 止血法 3:按文件数清 ===
$ journalctl --vacuum-files=10
# ★ 只保留最近的 10 个 journal 文件。
# === ★ 清完验证 ===
$ journalctl --disk-usage
Archived and active journals take up 480.0M ... # ★ 降下来了
$ df -h /
/dev/vda1 40G 17G 23G 43% / # ★ 根分区松快了
# === ★ 注意:vacuum 只删【已归档(archived)】的 ===
# journal 文件分两种:
# - active(活跃):当前正在写的,vacuum 不会删它。
# - archived(已归档):写满封存的旧文件,vacuum 删的是这些。
# ★ 所以清完之后,--disk-usage 可能还有几十~上百 MB,
# 那是 active 文件,正常,不用管。
# === ★ 一个常被忘记的"另一半":rsyslog 的纯文本日志 ===
# 如果系统还跑着 rsyslog,/var/log/ 下还有一堆纯文本:
$ ls -lhS /var/log/*.log /var/log/messages* 2>/dev/null | head
# ★ messages、secure 这些也可能很大。它们由
# logrotate 管,检查 /etc/logrotate.d/ 的轮转配置。
# 清 journal 的同时,顺手看一眼这边。
# === 认知 ===
# ★ journalctl --vacuum-size / --vacuum-time 是
# 磁盘告警时的"急救包",几秒钟就能腾出几十 G。
# 但这只是【止血】—— 不配上限,它还会再涨回来。
# 接下来必须治本。
修复 5:治本——给 journald 配上限
# === ★ 治本:改 journald.conf,让它自己管住自己 ===
# === ★ 配置文件:/etc/systemd/journald.conf ===
$ vim /etc/systemd/journald.conf
# ★ 把 [Journal] 段下相关项的注释去掉,填上值:
[Journal]
# --- ★ 核心:限制总大小 ---
SystemMaxUse=1G
# ★ journal 持久日志(/var/log/journal)总共最多
# 占 1G。超了就自动删最老的。这是最关键的一条。
SystemKeepFree=2G
# ★ 另一重保险:不管 journal 多大,都要给文件系统
# 【留出至少 2G 空闲】。两个条件取更严格的那个。
# --- ★ 限制单个 journal 文件大小 ---
SystemMaxFileSize=128M
# ★ 单个 .journal 文件最大 128M,写满就归档、开新的。
# --- ★ 按时间限制保留期 ---
MaxRetentionSec=2week
# ★ 超过 2 周的日志,自动清掉。按需调(1month 等)。
# --- ★ 存储方式 ---
Storage=persistent
# ★ persistent=持久存磁盘。如果这台机器【根本不
# 需要】持久日志(比如无状态节点),可设
# Storage=volatile —— 日志只进内存,完全不吃磁盘。
# === ★ 改完,重启 journald 生效 ===
$ systemctl restart systemd-journald
$ systemctl status systemd-journald # 确认正常 running
# === ★ 验证配置真的生效了 ===
$ journalctl --disk-usage
# ★ 之后它会自动维持在 SystemMaxUse 之内,不再
# 无限涨。可以过几天再来看一眼确认。
# === ★ 如果 journal 和系统挤在同一分区:考虑迁走 ===
# 最稳妥的做法之一:让 journal 别和根分区抢空间。
# - 给 /var 或 /var/log 单独挂一个分区/盘;
# - 或把 Storage 设 volatile(不落盘);
# - 或确保 SystemMaxUse 设得足够小、SystemKeepFree
# 足够大,守住根分区。
# === ★ 顺手:logrotate 管好 rsyslog 那一摊 ===
# 传统纯文本日志由 logrotate 轮转,检查并测试:
$ cat /etc/logrotate.conf # 全局轮转策略
$ logrotate -d /etc/logrotate.conf # -d 演习,只看不动手
# ★ 确保 messages、secure 等也有合理的轮转 + 保留期。
# === ★ 把"日志目录大小"纳入监控 ===
# 别再让它涨到 95% 才告警。主动监控:
$ du -sh /var/log/journal # journal 当前大小
$ df -h /var # /var 所在分区
# ★ 监控里加一条:journal 目录大小、/var 使用率,
# 涨势不对就提前告警 —— 把"突然引爆"变成"提前发现"。
# === 验证 ===
$ df -h / # ★ 根分区使用率回落、稳定
$ journalctl --disk-usage # ★ 维持在 SystemMaxUse 内
$ grep -v '^#' /etc/systemd/journald.conf | grep -v '^$'
[Journal]
SystemMaxUse=1G # ★ 配置不再是空的了
...
# ★ 磁盘降下来 + journald.conf 配好上限 + 监控加上 ——
# 才算真修好。
口诀放进脑子:磁盘满先 df 锁分区 du 找大目录,journal 大就 --vacuum 再配 SystemMaxUse。
修复 6:磁盘与系统日志排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ 磁盘满:固定流程 df 锁分区 -> du 逐层下钻 -> 对症处理,别凭印象猜 ===
$ df -h
$ du -h --max-depth=1 /路径 2>/dev/null | sort -rh | head
# === 2. ★ systemd 系统上有个 journald 服务,默默把系统日志攒在 /var/log/journal ===
# === 3. ★ 查日志占用第一时间想到 journalctl --disk-usage ===
# === 4. journal 是二进制文件,cat 是乱码,要用 journalctl 读 ===
# === 5. ★ journal 默认上限按磁盘 10% 算,加老系统没配过 journald.conf,容易膨胀 ===
# === 6. ★ 止血:journalctl --vacuum-size=500M 或 --vacuum-time=7d ===
# === 7. ★ 治本:journald.conf 配 SystemMaxUse / SystemKeepFree / MaxRetentionSec ===
# === 8. 无状态节点可把 Storage 设 volatile,日志只进内存不吃磁盘 ===
# === 9. rsyslog 的纯文本日志(messages/secure)是另一摊,由 logrotate 管,一并检查 ===
# === 10. 排查"磁盘被不明占用吃满"的步骤链 ===
$ df -h # ① 哪个分区满
$ du -h --max-depth=1 ... | sort -rh # ② 逐层找大目录
$ journalctl --disk-usage # ③ 是不是 journal
$ journalctl --vacuum-size=500M # ④ 止血
$ 配 journald.conf SystemMaxUse # ⑤ 治本 + 加监控
# 按这个顺序,"不明不白满了的磁盘"基本能定位、能根治。
命令速查
需求 命令
=============================================================
看各分区使用率 df -h
看 inode 使用率 df -i
逐层找大目录 du -h --max-depth=1 /路径 | sort -rh | head
看 journal 占多少磁盘 journalctl --disk-usage
按大小清 journal journalctl --vacuum-size=500M
按时间清 journal journalctl --vacuum-time=7d
按文件数清 journal journalctl --vacuum-files=10
看 journal 配置 cat /etc/systemd/journald.conf
journald 配置文件 /etc/systemd/journald.conf
改完重启 journald systemctl restart systemd-journald
查已删除但被占用的文件 lsof +L1 | grep deleted
演习 logrotate logrotate -d /etc/logrotate.conf
口诀:磁盘满先 df 锁分区,再 du --max-depth 一层层下钻找大目录
/var/log/journal 大,先 --vacuum 止血,再 journald.conf 配 SystemMaxUse 治本
避坑清单
- 磁盘满了走固定流程,先 df 锁定哪个分区满,再 du --max-depth=1 逐层下钻找大目录
- systemd 系统上有个 journald 服务,默默把内核服务登录 cron 等所有日志攒在 /var/log/journal
- 查日志占用第一时间用 journalctl --disk-usage,它直接告诉你 systemd 日志占了多少
- journal 是二进制文件,直接 cat 是乱码,必须用 journalctl 来读
- journal 默认上限 SystemMaxUse 按磁盘容量 10% 算,大磁盘上这个数很大,容易撑爆同分区
- 老系统的 journald.conf 经常是全空的纯默认,从装机起就没人管过日志大小
- journal 涨得极慢极稳藏在监控噪声里,直到逼近 100% 才突然引爆
- 磁盘告警止血用 journalctl --vacuum-size=500M 或 --vacuum-time=7d,几秒腾出几十 G
- 治本改 journald.conf 配 SystemMaxUse SystemKeepFree MaxRetentionSec,改完重启 systemd-journald
- rsyslog 的纯文本日志 messages secure 是另一摊由 logrotate 管,清 journal 时一并检查
总结
这次"磁盘被一个我不知道的目录悄悄吃满"的事故,纠正了我一个关于"我的系统"的、近乎自负的错觉。在我心里,这台服务器,是【我的】。我装的系统,我部署的应用,我写的脚本,我配的日志——上面跑的一切,我默认都是我亲手放上去的,都在我的认知地图里有一个明确的坐标。所以当磁盘告警时,我的排查方式,本质上是在我那张【自以为完整的地图】上,挨个点名:应用日志?查过,正常。临时文件?查过,正常。家目录?查过,正常。我把地图上所有的点都点了一遍,没有一个对得上那几十个 G,于是我卡住了——我得出的结论是"没有任何东西能解释它",可这个结论真正的含义其实是:"在我的地图上,没有任何东西能解释它。"我从来没有怀疑过,会不会是我的【地图本身】,漏画了一大块。复盘到根上,我才明白,那 22G 的 journal,恰恰是一个【从来不在我地图上】的东西。它不是我创建的,不是我配置的,日志不是我写的,我甚至想不起来我"知道"过它的存在。它是操作系统自带的一个服务,从我装好系统、敲下第一条命令的那一刻起,它就已经在那里,默默地开始工作了。它不需要我下令,不需要我同意,它就是 systemd 这套体系运转的一部分。我一直以为,一台服务器上发生的事,要么是我主动做的,要么是我部署的程序做的——总之,源头都能追溯到"我"。可这件事让我看清:一个系统,从它被创建出来的那一刻起,就【自带】了一大批我没有参与、却一直在运转的部件。它们消耗着 CPU、内存、磁盘,它们产生着文件、写着日志,它们有自己的默认行为和默认配额——而这一切,都发生在我的视野之外,因为我从来没有想过要去看它们。我对"我的系统",其实只了解我亲手添上去的那薄薄一层;那层底下,是一整个我既没参与建造、也从未巡视过的地基。这次最大的收获,是我对"我不知道的东西"这件事,生出了一种新的谦卑。过去我排查问题,潜意识里把范围划定在"我知道的事物"之内——我会非常努力地、反复地排查我认识的每一个嫌疑人,却从没想过,真正的元凶,可能压根不在我的嫌疑人名单上,因为我根本不知道还有它这么个"人"。我查得那么用力却一无所获,不是因为我不够仔细,而是因为我用尽全力地、仔细地,搜查了一个【不包含答案的范围】。所以下一次,当我把"我知道的"都查遍、却依然对不上时,我不会再把这当成"无解",而是把它当成一个明确的信号:这件事的答案,在我的认知地图之外——是时候停止在已知里打转,转而老老实实地、不带预设地,用 du 这样的工具,从根目录一层层地、把这个系统【真实的样子】重新走一遍了。很多时候,我们找不到答案,不是因为答案藏得太深,而是因为我们打着手电,只在自己脚下那一小圈熟悉的光亮里反复地找;而那个东西,一直就待在光圈之外的黑暗里,不远,只是我们从没把手电,往那个方向照过去。
—— 别看了 · 2026