服务器磁盘爆满排查记录:Docker 与 Systemd 日志的临时止血与永久根治

服务器磁盘爆满排查实录: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 容器进程仍持有文件句柄,磁盘空间不会立即释放,dfdu 还会出现"显示不一致"的诡异现象。除非重启容器(生产环境等于业务中断)。

正确做法是截断

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不影响其他容器

配置优先级composelogging > 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 控制)

八、总结与思考

  1. dfdu 不一致?必有进程持有已删除文件。 排查命令 lsof +L1;处理时尽量用 truncate -s 0 代替 rm,避免触发句柄残留。

  2. 临时方案只救火,永久方案才治病。 vacuumtruncate 不写入任何配置,重启后"上限"自动归零------下次还会撑爆。

  3. Docker 日志是服务器磁盘爆满的"头号惯犯"。 初始化服务器时务必第一时间配置 /etc/docker/daemon.json

  4. 生产环境重启 Docker = 全量容器中断,务必在维护窗口或业务低峰期操作;推荐用 compose 项目级配置降低影响面。

  5. 建立"发现即根治"的运维意识。 单次清理只能解燃眉之急,把限制写入文件 + 巡检脚本双保险,才能彻底告别每周告警的噩梦。

附录:速查手册

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.ymllogging 字段 同上

所有的磁盘爆满,都是日志在无声地呐喊:你忘记给我设定边界了。