服务器磁盘爆满排查实录:Docker 与 Systemd 日志的临时止血与永久根治
概述
问题 :服务器磁盘每周增长几个 G,df -h 告警根分区仅剩 1.4GB。
目标 :定位"空间刺客",分场景处理 Docker 与 Systemd 日志,从临时止血到永久根治。
适合谁看 :负责 Linux 服务器运维的开发者、SRE、运维工程师。
前置条件 :能 SSH 到目标机器,具备 sudo 权限。
一、故障现象:根分区 97%,Docker 容器告警
bash
df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 40G 36G 1.4G 97% /
overlay 40G 36G 1.4G 97% /var/lib/docker/overlay2/...
可用空间仅剩 1.4GB,一连串 overlay 挂载点表明 Docker 容器正在运行。任何一次日志写入都可能直接触发 No space left on device。
二、定位"空间刺客":从 / 逐级下钻
df 告诉你"满了",du 告诉你"谁"满的。逐级下钻是定位大文件的标准动作。
2.1 根目录排序
bash
du -sh /* 2>/dev/null | sort -rh | head -n 10
| 目录 | 大小 |
|---|---|
/var |
23G |
/www |
7.2G |
/usr |
4.5G |
/swap.img |
2.1G |
/var 独占 23GB,首要怀疑对象。
2.2 进入 /var
bash
du -sh /var/* 2>/dev/null | sort -rh | head -n 10
| 子目录 | 大小 |
|---|---|
/var/lib |
17G |
/var/log |
5.1G |
/var/cache |
127M |
两个重灾区:/var/lib(持久化数据)与 /var/log(日志)。
2.3 精准区分 /var/log 里的两类"住客"
/var/log 下住着两种本质完全不同的日志,要用对应命令才能区分:
| 类别 | 路径 | 查大小命令 |
|---|---|---|
| Systemd 统一日志 | /var/log/journal/ |
journalctl --disk-usage |
| 应用 / Docker 容器日志 | /var/log/*.log、/var/lib/docker/containers/*-json.log |
ls -lhS /var/log/ |
本次排查发现:journalctl --disk-usage 只有 450MB,真正的元凶是 Docker 容器下一个 5GB 的 -json.log。
三、两大元凶的本质:为什么日志会无限膨胀?
#mermaid-svg-CH8CYljxSsiYEHWP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CH8CYljxSsiYEHWP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CH8CYljxSsiYEHWP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CH8CYljxSsiYEHWP .error-icon{fill:#552222;}#mermaid-svg-CH8CYljxSsiYEHWP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CH8CYljxSsiYEHWP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CH8CYljxSsiYEHWP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CH8CYljxSsiYEHWP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CH8CYljxSsiYEHWP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CH8CYljxSsiYEHWP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CH8CYljxSsiYEHWP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CH8CYljxSsiYEHWP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CH8CYljxSsiYEHWP .marker.cross{stroke:#333333;}#mermaid-svg-CH8CYljxSsiYEHWP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CH8CYljxSsiYEHWP p{margin:0;}#mermaid-svg-CH8CYljxSsiYEHWP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CH8CYljxSsiYEHWP .cluster-label text{fill:#333;}#mermaid-svg-CH8CYljxSsiYEHWP .cluster-label span{color:#333;}#mermaid-svg-CH8CYljxSsiYEHWP .cluster-label span p{background-color:transparent;}#mermaid-svg-CH8CYljxSsiYEHWP .label text,#mermaid-svg-CH8CYljxSsiYEHWP span{fill:#333;color:#333;}#mermaid-svg-CH8CYljxSsiYEHWP .node rect,#mermaid-svg-CH8CYljxSsiYEHWP .node circle,#mermaid-svg-CH8CYljxSsiYEHWP .node ellipse,#mermaid-svg-CH8CYljxSsiYEHWP .node polygon,#mermaid-svg-CH8CYljxSsiYEHWP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CH8CYljxSsiYEHWP .rough-node .label text,#mermaid-svg-CH8CYljxSsiYEHWP .node .label text,#mermaid-svg-CH8CYljxSsiYEHWP .image-shape .label,#mermaid-svg-CH8CYljxSsiYEHWP .icon-shape .label{text-anchor:middle;}#mermaid-svg-CH8CYljxSsiYEHWP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CH8CYljxSsiYEHWP .rough-node .label,#mermaid-svg-CH8CYljxSsiYEHWP .node .label,#mermaid-svg-CH8CYljxSsiYEHWP .image-shape .label,#mermaid-svg-CH8CYljxSsiYEHWP .icon-shape .label{text-align:center;}#mermaid-svg-CH8CYljxSsiYEHWP .node.clickable{cursor:pointer;}#mermaid-svg-CH8CYljxSsiYEHWP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CH8CYljxSsiYEHWP .arrowheadPath{fill:#333333;}#mermaid-svg-CH8CYljxSsiYEHWP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CH8CYljxSsiYEHWP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CH8CYljxSsiYEHWP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CH8CYljxSsiYEHWP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CH8CYljxSsiYEHWP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CH8CYljxSsiYEHWP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CH8CYljxSsiYEHWP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CH8CYljxSsiYEHWP .cluster text{fill:#333;}#mermaid-svg-CH8CYljxSsiYEHWP .cluster span{color:#333;}#mermaid-svg-CH8CYljxSsiYEHWP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CH8CYljxSsiYEHWP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CH8CYljxSsiYEHWP rect.text{fill:none;stroke-width:0;}#mermaid-svg-CH8CYljxSsiYEHWP .icon-shape,#mermaid-svg-CH8CYljxSsiYEHWP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CH8CYljxSsiYEHWP .icon-shape p,#mermaid-svg-CH8CYljxSsiYEHWP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CH8CYljxSsiYEHWP .icon-shape .label rect,#mermaid-svg-CH8CYljxSsiYEHWP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CH8CYljxSsiYEHWP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CH8CYljxSsiYEHWP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CH8CYljxSsiYEHWP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
/var/log 5G+
超过 1GB
正常
/var/lib 17G+
磁盘使用率 > 90% 告警
df -h 确认根分区
du -sh /* 定位大目录
是否 /var?
du -sh /var/*
/var/log 还是 /var/lib?
journalctl --disk-usage
Journald 日志问题
find 查找 *-json.log
Docker 容器日志问题
du -sh /var/lib/docker/*
临时方案: journalctl --vacuum
临时方案: truncate -s 0
永久方案: journald.conf
永久方案: daemon.json
配置巡检脚本
df -h 验证 ≤ 80%
3.1 元凶一:Systemd Journald
systemd-journald 接管了内核与系统服务(SSH、Cron 等)的日志。默认配置下不限制总容量上限,文件会一直追加直到撑爆磁盘。
判定标准 :journalctl --disk-usage 超过 1GB 即可确认。
3.2 元凶二:Docker json-file 日志驱动
容器内应用把日志输出到 stdout/stderr,Docker 用 json-file 驱动接收并落到 /var/lib/docker/containers/<id>/<id>-json.log。默认同样无大小限制------一个高频输出的容器几天内能写出几十 GB。
判定标准 :journalctl --disk-usage 正常,但单个 *-json.log 达数 GB。
这两类日志"默认无界"的设计,是 Linux 服务器磁盘爆满的头号通缉犯。
四、应急止血:临时方案(秒级生效)
业务告急时,先把空间抢回来,再谈根治。临时方案的本质:手动清扫垃圾,但不限制"垃圾桶容量",重启后限制规则会失效。
4.1 清理 Journald 日志
bash
# 限制总容量为 200MB(推荐)
sudo journalctl --vacuum-size=200M
# 或保留最近 7 天
sudo journalctl --vacuum-time=7d
执行完毕立即生效,df -h 可见空间释放。无需重启任何服务。
4.2 清理 Docker 容器日志
⚠️ 千万不要
rm -f *.log! 容器进程仍持有文件句柄,磁盘空间不会立即释放,df与du还会出现"显示不一致"的诡异现象。除非重启容器(生产环境等于业务中断)。
正确做法是截断:
bash
find /var/lib/docker/containers/ -name "*.log" -exec truncate -s 0 {} \;
容器不重启,docker logs 仍能查看新产生的日志,5GB 空间秒级归还。
4.3 临时方案 vs 永久方案:一张表看懂
| 维度 | 临时方案(止血) | 永久方案(根治) |
|---|---|---|
| 生效速度 | 毫秒级 | 需重启服务(journald 不影响业务;docker 中断容器) |
| 重启后限制规则 | 失效 | 永久生效 |
| 适用场景 | 磁盘已满(Use% ≥ 95%)的紧急救火 | 日常预防与固化 |
| 核心价值 | 快速止血,保住业务 | 彻底根治,告别复发 |
运维黄金法则 :先用临时方案秒级止血保住业务,再配置永久方案杜绝复发。两者缺一不可。
五、永久根治:配置固化
临时方案只能救一时,写入配置文件才能让限制规则在重启后依然生效。
5.1 固化 Journald:限制 500MB
为什么用 override 目录而不是直接改主配置? 升级系统时主配置文件可能被覆盖,override 目录的优先级最高且独立维护,符合生产环境的配置规范。
bash
sudo mkdir -p /etc/systemd/journald.conf.d
sudo tee /etc/systemd/journald.conf.d/override.conf > /dev/null <<EOF
[Journal]
SystemMaxUse=500M
SystemMaxFileSize=100M
EOF
sudo systemctl restart systemd-journald
影响 :仅重启 journald 服务,业务零感知。
5.2 固化 Docker:限制单文件 100MB × 3 个
为什么选 100MB × 3? 单文件 100MB 足够保留完整上下文,3 个轮转文件满足大多数排障需求,总上限 300MB 是一个平衡点------既不浪费磁盘,又不会因为轮转过频丢失关键日志。
方案 A:全局配置(推荐)
一次性解决所有容器,无需逐个项目配置。
bash
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
EOF
sudo systemctl restart docker
⚠️
systemctl restart docker会中断所有运行中的容器,请在业务低峰期操作。
方案 B:项目级配置(docker-compose.yml,更精细)
yaml
services:
library-service:
image: library:0.0.1
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
只需 docker compose up -d --force-recreate,不影响其他容器。
配置优先级 :compose 的 logging > daemon.json 全局配置。
六、长效预防:自动巡检脚本
把每周清理设为兜底任务,即使永久配置失效也不会爆盘:
bash
#!/bin/bash
# /root/cleanup_logs.sh ------ 每周巡检清理
LOG=/var/log/cleanup.log
echo "[$(date '+%F %T')] 开始巡检..." >> $LOG
# 1. Journald 超过 1GB 则压缩到 200MB
if [ "$(journalctl --disk-usage | awk '{print $3}' | sed 's/[^0-9]//g')" -gt 1000 ]; then
journalctl --vacuum-size=200M >> $LOG 2>&1
fi
# 2. Docker 单文件超过 1GB 则截断
find /var/lib/docker/containers/ -name "*.log" -size +1G \
-exec truncate -s 0 {} \; >> $LOG 2>&1
# 3. 清理 24 小时前的构建缓存
docker builder prune -f --filter "until=24h" >> $LOG 2>&1
df -h / >> $LOG
加入 crontab,每周日 03:00 执行:
cron
0 3 * * 0 /root/cleanup_logs.sh
七、重启验证清单
完成所有配置后,建议进行一次重启验证:
bash
# 1. 记录清理后的磁盘使用率
df -h / > /tmp/disk_before.txt
# 2. 重启服务器
sudo reboot
# 3. 重启后再次检查
df -h /
journalctl --disk-usage
find /var/lib/docker/containers/ -name "*.log" -exec ls -lh {} \; | head -10
| 检查项 | 预期结果 |
|---|---|
| 根分区使用率 | 仍保持在 80% 以下(与重启前一致) |
| Systemd 日志大小 | ≤ 500MB(由 SystemMaxUse 控制) |
| Docker 日志文件 | 单个 ≤ 100MB(由 max-size 控制) |
八、总结与思考
-
df与du不一致?必有进程持有已删除文件。 排查命令lsof +L1;处理时尽量用truncate -s 0代替rm,避免触发句柄残留。 -
临时方案只救火,永久方案才治病。
vacuum和truncate不写入任何配置,重启后"上限"自动归零------下次还会撑爆。 -
Docker 日志是服务器磁盘爆满的"头号惯犯"。 初始化服务器时务必第一时间配置
/etc/docker/daemon.json。 -
生产环境重启 Docker = 全量容器中断,务必在维护窗口或业务低峰期操作;推荐用 compose 项目级配置降低影响面。
-
建立"发现即根治"的运维意识。 单次清理只能解燃眉之急,把限制写入文件 + 巡检脚本双保险,才能彻底告别每周告警的噩梦。
附录:速查手册
A.1 常用命令
| 目的 | 命令 |
|---|---|
| 查看磁盘 | df -h |
| 定位大目录 | `du -sh /* |
| Journald 占用 | journalctl --disk-usage |
| Docker 占用明细 | docker system df |
| 单容器日志大小 | find /var/lib/docker/containers/ -name "*.log" -exec ls -lh {} \; |
| 截断容器日志 | find /var/lib/docker/containers/ -name "*.log" -exec truncate -s 0 {} \; |
A.2 关键配置
| 组件 | 路径 | 核心参数 |
|---|---|---|
| Journald | /etc/systemd/journald.conf.d/override.conf |
SystemMaxUse=500M |
| Docker 全局 | /etc/docker/daemon.json |
max-size=100m, max-file=3 |
| Compose 项目 | docker-compose.yml 的 logging 字段 |
同上 |
所有的磁盘爆满,都是日志在无声地呐喊:你忘记给我设定边界了。