凌晨三点告警响了:/var/lib/docker 分区使用率 100%,业务写入失败报 no space left on device。登上机器一查,某个 Java 容器因为一个打印堆栈的死循环,三天写了 200GB 日志。这类事故在 Docker 环境里极其常见,根源在于一个容易被忽略的默认行为:Docker 默认的日志驱动 json-file 不做任何轮转,也没有大小上限,日志会一直写到磁盘写满为止。本文讲清楚如何快速止损、如何给单个容器和全局配置日志轮转、以及怎么建立长效的容量管控机制。
一、确认磁盘到底被谁吃了
不要一上来就删文件。先确认是日志、镜像还是容器可写层占了空间,三者的处理方式完全不同。
# 1. 总体镜像、容器、卷、构建缓存占用概览
docker system df
# 详细到每个对象
docker system df -v
# 2. 确认 Docker 数据根目录(非默认路径时很重要)
docker info --format '{{.DockerRootDir}}'
# 通常为 /var/lib/docker;rootless 模式为 ~/.local/share/docker
# 3. 逐层定位大目录
du -xh --max-depth=1 /var/lib/docker 2>/dev/null | sort -h
du -xh --max-depth=1 /var/lib/docker/containers 2>/dev/null | sort -h
# 4. 找出超过 100MB 的日志文件(-json.log 为默认 json-file 驱动产物)
find /var/lib/docker/containers -name '*-json.log' -size +100M -exec ls -lh {} \; | sort -k5 -h
# 5. 汇总所有容器日志总大小
find /var/lib/docker/containers -name '*-json.log' -printf '%s\n' | \
awk '{s+=$1} END {printf "总日志大小: %.2f GB\n", s/1024/1024/1024}'
# 6. 查看某个容器当前的日志驱动与日志路径
docker inspect <容器> --format '{{.HostConfig.LogConfig.Type}}'
docker inspect <容器> --format '{{.LogPath}}'
# 典型路径:/var/lib/docker/containers/<容器完整ID>/<容器完整ID>-json.log
注意区分 du 和 df 的差异:如果容器日志文件被删除但进程仍持有句柄,du 看不到而 df 仍显示占满。用下面命令找出这种"已删除但未释放"的文件:
# 查找被删除但仍被进程占用的文件(第 7 列为 (deleted)) lsof +L1 2>/dev/null | grep deleted lsof 2>/dev/null | grep 'containers.*json.log' # 这种情况下只能重启容器释放句柄,或者用 truncate 清空而非 rm truncate -s 0 /proc//fd/ # 极端情况下的救命手段
二、紧急止损:在线清空日志而不停容器
磁盘已满、业务还在跑,这时候最怕的就是 rm -f。不要直接删除 json.log 文件:Docker 守护进程持有该文件的写句柄,删除后空间不会立即释放,而且容器在重启前会一直往已删除的 inode 里写,导致 df 显示占用 100% 但 du 找不到文件,排查难度陡增。
正确做法是用 truncate 把文件截断为 0,文件 inode 不变,Docker 继续正常写入:
# 清空单个容器日志(推荐方式)
truncate -s 0 $(docker inspect --format='{{.LogPath}}' <容器名>)
# 批量清空所有运行中容器的日志
docker ps -q | while read cid; do
lp=$(docker inspect --format='{{.LogPath}}' "$cid")
[ -n "$lp" ] && [ -f "$lp" ] && truncate -s 0 "$lp"
done
# 清理完先确认空间已释放
df -h /var/lib/docker
# 如果不想全部清空,只截断某容器最后 100MB(保留近期日志用于排查)
# 思路:把文件按偏移量拷贝后截断,这里推荐先备份再清空
cp $(docker inspect --format='{{.LogPath}}' <容器>) /tmp/app.log.bak
truncate -s 0 $(docker inspect --format='{{.LogPath}}' <容器>)
tail -1000 /tmp/app.log.bak
清空之前最好先看一眼日志内容,确认是什么在刷屏,否则日志清理只是把问题往后推:
# 看最近的错误高频模式
docker logs --tail 200 --timestamps <容器> 2>&1 | sort | uniq -c | sort -rn | head -20
# 按秒统计日志速率,量化日志量(例如每秒输出多少行)
docker logs --tail 10000 --timestamps <容器> 2>&1 | awk '{print $2}' | cut -d'.' -f1 | uniq -c | tail -20
三、根本解决:配置日志轮转
止损之后必须配置轮转,否则几天后会原样复发。有两种作用范围:全局(对新创建容器生效)和单容器(优先级最高)。
3.1 全局配置(推荐,一次配好所有新容器)
编辑宿主机上的 /etc/docker/daemon.json(不存在则新建):
cat > /etc/docker/daemon.json <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3",
"compress": "true"
}
}
EOF
# 校验 JSON 合法性(这一步能挡掉 90% 的 Docker 启动失败)
python -m json.tool /etc/docker/daemon.json
# 重载配置并重启守护进程
systemctl daemon-reload
systemctl restart docker
# 确认新容器已应用配置
docker run -d --name test-log nginx:1.25
docker inspect test-log --format '{{json .HostConfig.LogConfig}}'
参数说明:
max-size:单个日志文件的触发轮转阈值,单位支持 k / m / g,不带单位会被当作字节,这是个常见踩坑点。max-file:保留的历史文件数量,必须大于等于 1,默认值就是 1(即不保留历史,只留当前文件)。compress:轮转后的历史文件用 gzip 压缩,对文本日志通常能省 80%-95% 空间。注意当前正在写的文件不会被压缩。
容量计算公式很简单:单容器日志占用上限 ≈ max-size × max-file。上面配置即每个容器最多约 50m × 3 = 150MB。据此可以反推:如果宿主机给 Docker 预留 10GB 空间,跑 20 个容器,那么每个容器的上限应控制在 500MB 以内比较安全。
3.2 单容器配置(覆盖全局,优先级更高)
# docker run 时指定 docker run -d --name app \ --log-driver=json-file \ --log-opt max-size=10m \ --log-opt max-file=5 \ --log-opt compress=true \ nginx:1.25 # docker-compose.yml 中的写法 # services: # app: # image: nginx:1.25 # logging: # driver: "json-file" # options: # max-size: "10m" # max-file: "5" # compress: "true" # Docker Swarm service 写法 docker service update --log-driver json-file \ --log-opt max-size=10m --log-opt max-file=5 web_app
3.3 日志驱动选型
| 驱动 | 是否支持轮转 | 存储位置 | 适用场景 |
|---|---|---|---|
| json-file(默认) | 支持,需配 max-size/max-file | /var/lib/docker/containers/ | 通用,单机自建 |
| local | 默认自动轮转,无需配 size | /var/lib/docker/containers/ | 只想落盘、不走集中采集 |
| journald | 由 systemd-journald 管理 | /var/log/journal/ | 已用 journalctl 的体系 |
| syslog | 不支持 Docker 侧轮转 | 远端 syslog 服务器 | 已有 syslog 基础设施 |
| fluentd / gelf | 不支持 | 远端日志服务 | ELK / EFK 集中式日志 |
| none | 不适用 | 不落盘 | 日志通过 stdout 之外途径输出 |
如果日志已经接入 ELK/EFK 这类集中式系统,本地只是临时缓冲,强烈建议把 max-size 调到很小(如 10m、max-file 2),因为本地日志的留存价值很低,纯粹是磁盘负担。
# 使用 journald 驱动时的查看方式完全不同 docker run -d --log-driver=journald --name app nginx journalctl CONTAINER_NAME=app -f journalctl -u docker --since "1 hour ago" # journald 自身的容量限制也要配(/etc/systemd/journald.conf) # SystemMaxUse=2G SystemKeepFree=15% MaxRetentionSec=7day mkdir -p /etc/systemd/journald.conf.d cat > /etc/systemd/journald.conf.d/00-limit.conf <<'EOF' [Journal] SystemMaxUse=2G SystemKeepFree=15% MaxRetentionSec=7day EOF systemctl restart systemd-journald
3.5 日志暴涨的根因治理
轮转只是限定了上限,真正让磁盘不再告警的是解决"为什么会产生这么多日志"。实践中最常见的四类根因,按出现频率排序如下:
- 应用层的异常堆栈循环。代码里捕获异常后在同一处反复重试并打印完整堆栈,一次失败可能输出几十 KB。典型特征:日志里同一个
Exception堆栈重复出现,且时间戳密集。这类问题需要与开发协同修复,运维侧的临时手段是把该服务的日志级别调到 WARN 或 ERROR。 - 健康检查与访问日志打满。Nginx/Envoy 的 access log、K8s 的存活探测日志,在高 QPS 下每小时可达数 GB。建议把访问类日志单独输出到挂载卷或直接旁路到采集端,不要让它们进入 stdout。
- 日志级别配置过低。生产环境跑着 DEBUG 级别,框架会把每一条 SQL、每一次 RPC 全部打印。上线前应统一检查配置中心的日志级别。
- 容器反复重启导致日志叠加。容器正常重启时日志会被保留(旧文件 container-stopped 后仍占用空间),频繁重启的容器即使单条日志不多,累积起来也很可观。
一个实用的容量规划口径:按"每秒日志行数 × 平均单行字节数 × 86400 秒"估算单容器日增量,再乘以保留天数,就能得到合理的 max-size × max-file 组合。例如某项服务日志速率为 20 行/秒、单行平均 500 字节,日增量约 864MB,若只保留 1 天,则 max-size 设 100m、max-file 设 10 比较合适;若希望保留 3 天,则应改用 journald 或远端日志服务,而不是继续放大本地文件。
3.6 Kubernetes 环境的对应配置
K8s 节点上容器日志同样会写满磁盘,配置位置在 kubelet 而非 Docker:
# kubelet 配置(新版用 config 文件:/var/lib/kubelet/config.yaml) cat /var/lib/kubelet/config.yaml | grep -A5 'containerLog' # containerLogMaxSize: 50Mi # containerLogMaxFiles: 5 # 旧版 kubelet 启动参数 ps aux | grep kubelet | grep -oE '\-\-container-log-max-[a-z]+=[^ ]+' # --container-log-max-size=50Mi --container-log-max-files=5 # K8s 节点日志路径同样在 containers 目录下 ls /var/lib/docker/containers/ # Docker 运行时 ls /var/lib/containerd/ # containerd 运行时 # 查看某 Pod 的日志文件大小 du -sh /var/log/pods/*/*/*.log 2>/dev/null | sort -h | tail -10 # 修改后重启 kubelet systemctl daemon-reload systemctl restart kubelet systemctl status kubelet
K8s 还有一个重要机制:kubelet 会按 imageGCHighThresholdPercent(默认 85%)和 imageGCLowThresholdPercent(默认 80%)自动清理镜像,以及 evictionHard 中的 nodefs.available(默认 10%)触发 Pod 驱逐。但这只处理镜像和驱逐 Pod,不会帮你轮转已有容器的日志文件,所以日志上限仍必须配。
四、长效治理:脚本 + 监控 + 定期巡检
- 为所有新容器设置全局默认值:
/etc/docker/daemon.json配置max-size与max-file。 - 对日志量大的容器(Java 堆栈、Nginx 访问日志)单独调小
max-size或改用 volume 挂载宿主机日志目录统一管理。 - 存量容器必须重建才能应用新的 log-opts;重建前先确认
docker-compose.yml里也写好了 logging 配置。 - 编写定时巡检脚本,每天输出日志占用 Top 10 的容器。
- 给
/var/lib/docker所在分区配置监控告警,建议阈值:使用率 > 80% 告警,> 90% 严重告警。 - 考虑把
/var/lib/docker挂到独立分区或独立磁盘(通过daemon.json的data-root指定),避免日志写满影响根分区。 - 定期执行
docker system prune清理停止的容器、无用网络和悬空镜像。
# 巡检脚本:列出日志占用 Top 10 容器
cat > /usr/local/bin/docker-log-report.sh <<'EOF'
#!/bin/bash
docker ps -a --format '{{.Names}}\t{{.ID}}' | while IFS=$'\t' read name cid; do
lp=$(docker inspect --format='{{.LogPath}}' "$cid" 2>/dev/null)
[ -z "$lp" ] || [ ! -f "$lp" ] && continue
sz=$(stat -c %s "$lp" 2>/dev/null || echo 0)
echo "$((sz/1024/1024))MB\t$name"
done | sort -rn | head -10
EOF
chmod +x /usr/local/bin/docker-log-report.sh
/usr/local/bin/docker-log-report.sh
# 加入 crontab,每天 8 点巡检
# 0 8 * * * /usr/local/bin/docker-log-report.sh | mail -s "Docker日志巡检" ops@example.com
# 定期清理无用对象(-f 不提示,生产环境建议先不用 -a)
docker system prune -f
# 谨慎使用:下面的命令会删除所有未被使用的镜像
docker system prune -a -f
# 如需把 Docker 数据目录迁到独立磁盘,修改 daemon.json
# { "data-root": "/data/docker" }
五、建立日志容量规范
单点修好一台机器价值有限,真正见效的是把规范写进交付标准和镜像构建流程。建议按下面三档制定基线:
| 容器类型 | max-size | max-file | 单容器上限 | 说明 |
|---|---|---|---|---|
| 核心业务服务 | 50m | 3 | 约 150MB | 需保留少量现场用于排障 |
| 访问类日志(Nginx/Envoy) | 20m | 2 | 约 40MB | 已旁路到采集端,本地仅兜底 |
| 批处理/定时任务 | 100m | 2 | 约 200MB | 单次输出量大,执行完应归档 |
告警阈值建议设置为两个层级:宿主机 /var/lib/docker 分区使用率超过 80% 触发提醒,超过 90% 触发严重告警并自动执行 docker system prune。同时把"单容器日志文件 > 1GB"也做一项巡检规则——正常情况下配置了轮转就不该出现这么大的文件,一旦出现说明要么该容器没生效配置,要么它在无视轮转机制往挂载卷里写,两者都需要人工介入确认。
小结
Docker 容器日志占满磁盘这件事,技术手段非常简单——配好 max-size 和 max-file 就解决了九成问题——难的是把它作为规范固化下来,让每一台新交付的机器都默认带上。记住核心公式:单容器日志上限 ≈ max-size × max-file,据此按宿主机磁盘容量和容器数量反推参数。紧急情况用 truncate 清空而不是 rm,避免句柄未释放导致空间不回收。
避坑建议:
- 永远不要
rm -f容器 json.log,用truncate -s 0;删除后如遇句柄未释放,仍需重启容器。 max-size必须带单位(k/m/g),只写数字会被当成字节数。max-file要 ≥ 1,默认值为 1,很多人误以为配了 max-size 就会自动多留几个文件。- 改 daemon.json 只影响新建容器,存量容器必须
docker rm后重建,且 compose 文件要同步改。 - daemon.json 改完必须校验 JSON 合法性,语法错误会让 Docker 守护进程直接起不来。
- 已经用 ELK/EFK 的集群,本地日志要尽量小,否则一份日志存两处,纯属浪费。
- 把
/var/lib/docker单独分区,是最省心的兜底方案,日志写满也不会拖垮根分区和系统进程。 - K8s 环境配 kubelet 的 containerLogMaxSize,配 Docker 的 daemon.json 对 Pod 无效。
延伸阅读:日志问题解决后,容器网络的连通性与 DNS 配置是另一类高频故障,两者常常同时爆发,建议一并纳入巡检。
企业QQ咨询




