适用场景

Docker 默认使用 json-file 日志驱动。运行一段时间后,宿主机 /var/lib/docker 持续变大,最终出现磁盘告警,甚至应用写文件、创建临时目录或重启容器都失败。本文适用于单机 Docker、Docker Compose 以及未由集中日志系统完全接管容器标准输出的场景。

现象描述

一次线上告警中,业务分区使用率从 78% 在两小时内升到 98%。df 显示根分区空间不足,但按项目目录统计并没有对应的大文件;docker ps 中几个 API 容器仍在持续输出调试日志。更容易误判的是:即使删除了应用自己的日志文件,空间也没有立即回收。

典型影响包括:

  • 容器启动时报 no space left on device
  • 数据库或队列组件无法落盘;
  • 日志采集、镜像拉取和 docker compose up 失败;
  • 宿主机根分区满后,SSH 登录和 systemd 服务也可能异常。

先确认空间到底被谁占用

先确认 Docker 数据目录和实际的大文件,不要一上来就删除目录:

df -hT
docker system df -v
sudo du -xh --max-depth=2 /var/lib/docker | sort -h | tail -n 20
sudo find /var/lib/docker/containers -name '*-json.log' -type f -printf '%s %p\\n' | sort -nr | head -n 20

docker system df -v 能区分镜像、容器、卷和构建缓存;最后一条会按字节列出最大的容器标准输出日志。若最大的文件路径类似下面这样,基本可以确认是 json-file 日志:

/var/lib/docker/containers/<container-id>/<container-id>-json.log

再将容器 ID 映射为服务名,避免清理错对象:

docker ps -a --no-trunc --format 'table {{.ID}}\\t{{.Names}}\\t{{.Image}}'
docker inspect -f '{{.Name}} driver={{.HostConfig.LogConfig.Type}} config={{json .HostConfig.LogConfig.Config}}' <container-id>

如果 driver=json-file 且配置为空,说明该容器继承 Docker 默认配置,并没有设置轮转上限。

为什么删了文件,空间仍不回来

应用日志与容器标准输出日志不是同一件事。应用把日志写到 stdout/stderr 后,Docker 会写入 *-json.log。常见的两个坑是:

  1. 只在应用容器内删除文件,却没有处理宿主机上的 json.log
  2. rm 删除一个仍被进程打开的日志文件。文件目录项消失了,但已打开的文件描述符仍占用磁盘块。

可用下面的命令检查“已删除但仍被占用”的文件:

sudo lsof +L1 | grep -E 'docker|containerd'

输出中的 NLINK 为 0 表示文件已删除;SIZE/OFF 很大时,必须让持有该描述符的进程重开日志或重启,空间才会释放。

应急恢复:只截断已确认的大日志

生产应急时,不要删除 /var/lib/docker,也不要直接执行会影响所有业务的 docker system prune -a --volumes。确认目标日志对应的容器后,可截断该单个文件:

sudo truncate -s 0 /var/lib/docker/containers/<container-id>/<container-id>-json.log
df -h /var/lib/docker

truncate -s 0 保留原文件 inode,Docker 仍可继续写入,适合先快速释放空间。操作后检查容器是否仍健康:

docker ps --filter "id=<container-id>"
docker logs --tail 50 <container-id>

若日志增长极快,应同时将应用日志级别从 debug 调整为 infowarn,否则几分钟内可能再次写满。

永久修复:设置 Docker 默认日志轮转

/etc/docker/daemon.json 中配置默认日志驱动和轮转策略。已有文件时应合并 JSON,不能覆盖其中的 registry、data-root 等配置。

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "5"
  }
}

含义是单个容器的当前日志最多 100 MB,保留当前文件加 4 个轮转文件,单容器日志约占用 500 MB。配置后先验证 JSON 格式并重启 Docker:

sudo python3 -m json.tool /etc/docker/daemon.json >/dev/null
sudo systemctl restart docker
docker info --format 'Logging driver: {{.LoggingDriver}}'

注意:Docker 的默认日志配置只对新创建的容器生效。使用 Compose 时必须重建容器:

docker compose up -d --force-recreate
docker inspect -f '{{json .HostConfig.LogConfig}}' <new-container-id>

对于需要更严格限制的高频日志服务,可以在 compose.yaml 里显式覆盖:

services:
  api:
    image: example/api:stable
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "3"

这里 max-sizemax-file 必须写成字符串;Compose 会将它们传给 Docker 日志驱动。不要只改 daemon.json 而不重建旧容器,否则检查时仍会看到旧容器的 LogConfig 为空。

验证轮转是否真正工作

不要只看配置文件。可对一个保留的测试容器连续写入日志,再观察文件数量与总大小:

docker run -d --name log-rotate-check \\
  --log-opt max-size=1m --log-opt max-file=3 \\
  alpine sh -c 'i=0; while [ $i -lt 50000 ]; do echo "$(date) test-log-$i $(head -c 200 /dev/zero | tr "\\0" x)"; i=$((i+1)); done; sleep 3600'
docker inspect -f '{{.Id}}' log-rotate-check
sudo ls -lh /var/lib/docker/containers/<returned-id>/
docker rm -f log-rotate-check

实际验证中,目录内不应无限增加,只会保留当前日志和设定数量的轮转文件。生产环境还应通过监控确认:容器日志目录增速应被上限约束,分区使用率不再与单个请求异常成正比。

预防措施

  • /var/lib/docker 所在文件系统设置 80%、90% 两级容量告警,并同时采集 inode 使用率;
  • 对请求体、响应体、SQL 全量参数等高基数字段禁止长期 debug 输出;
  • 接入 Loki、ELK 等集中日志后,仍保留本地 max-sizemax-file,它们是最后一道磁盘保护;
  • 将 Docker 数据目录单独挂载分区,避免容器日志写满根分区影响操作系统;
  • 在发布流程中检查新容器的 HostConfig.LogConfig,把“配置存在”升级为“配置已生效”。

总结

Docker 日志写满磁盘的关键是先识别 json-file 的真实占用,再以截断单个确认文件的方式应急,最后通过默认配置和容器重建实现轮转。清理空间只能恢复现场;限制单容器日志大小、降低无效日志量并配置容量告警,才能避免同类事故反复发生。