容器日志撑满磁盘:json-file 轮转、存量清理与两层日志的分工

服务器磁盘莫名其妙满,最后查出来是容器日志——这件事几乎每个用 Docker 的站长都遇到过。宿主机上 du -sh /var/lib/docker 一看两个多 G,进去一查,某个容器的 *-json.log 单文件就有 1.8G。更气人的是,你配了 logrotate 却完全没管它,因为 Docker 的日志根本不走 syslog,也不在 /var/log 下面,logrotate 再怎么写都轮不到它。

这篇文章讲清楚三件事:Docker 默认的日志驱动到底把日志写在哪、轮转该怎么配(包括已经涨到 1.8G 的存量文件怎么安全处理)、以及容器日志和宿主机系统日志该怎么分层管理。

一、先搞清楚日志写在哪:json-file 驱动与它的目录约定

Docker 默认的日志驱动是 json-file,每个容器一行 JSON 记录,写在宿主机的固定路径下:

/var/lib/docker/containers/<容器ID>/<容器ID>-json.log

这个路径和容器名无关,和 compose 服务名也无关,只跟容器 ID 有关。想知道某个容器的日志到底落在哪个文件、有多大,一条命令就够:

docker inspect --format '{{.LogPath}}' my-app
# /var/lib/docker/containers/3f2a.../3f2a...-json.log

docker ps -q | while read id; do
  p=$(docker inspect --format '{{.LogPath}}' "$id")
  [ -f "$p" ] && printf '%s\t%s\t%s\n' \
    "$(du -h "$p" | cut -f1)" "$(docker inspect --format '{{.Name}}' "$id")" "$p"
done | sort -rh | head

第二段脚本是我最常用的巡检命令:列出所有运行中容器的日志文件大小,按大小倒序。第一次跑的时候要有个心理准备,很多人会在这里发现一个占了几 G 的文件。

顺便说清一个高频误解:docker logs 读的就是这个 json.log 文件,它不是在内存里缓存的。所以文件被清空或轮转之后,docker logs 看到的内容也会跟着变化——这一点和「日志已持久化到别处」的直觉不一样。

二、轮转的三种配法,以及它们的适用边界

Docker 的日志轮转是在日志驱动层面做的,配置项只有三个,写在 daemon.json 里:

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

这三个值的意思分别是:单文件上限 50MB,最多保留 5 个文件,也就是一个容器最多占 250MB。注意 max-file 是「文件个数」不是「轮转次数」,写 1 的效果是永不轮转(只有一个文件可用,轮转后没有落点),而不是「只保留一份」。这个语义坑在文档里写得含糊,我在一次排查中就因为它踩了半天。

这套全局配置有两个明确的边界。

第一个边界:只对之后新建的容器生效。已经存在的容器不会因为改了 daemon.json 就变小,也不会开始轮转——因为日志参数是容器的创建时属性,固化在容器配置里。要让存量容器生效,只能重建容器(docker rm 再 docker run,或者 compose 的 up -d --force-recreate)。

第二个边界:改 daemon.json 需要重启 Docker 守护进程,而重启守护进程默认不会重启设了 restart: always 之外的容器——实际上各个发行版行为不一致,有的会重启全部容器,有的不会。稳妥做法是选在维护窗口做,并且事先确认每个容器都能独立启动。

如果不想动全局配置,可以按容器单独配。compose 里的写法是:

services:
  app:
    image: myapp:latest
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "3"

按容器配的好处是可以给不同角色定不同额度:应用容器日志多,给 50MB×5;Nginx 容器有自己的 access log 走 stdout,给 20MB×3;数据库容器通常日志很少,给 10MB×2 就够。混在一起用全局配置,等于按最高需求给所有容器都留了空间。

三、存量日志怎么清:三种方法,只有一种安全

配置改好了,磁盘上那 1.8G 还在。这时候有几种做法,风险完全不同。

第一种:直接 rm。

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

不推荐。这个文件是容器的打开文件句柄,删掉之后进程还在往里写,内核会把数据写到已被删除的 inode 上——df 显示磁盘没释放,du 找不到对应文件,这就是经典的「删了日志磁盘还是满的」。用 lsof | grep deleted 能看到那个残留句柄,只有重启容器才会释放。

第二种:truncate 清空。

truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log

这是安全的。截断保留 inode,文件句柄继续有效,正在写的进程不受影响,磁盘空间立刻释放。唯一的副作用是日志内容全没了——而这通常正是你要的效果(前提是你不打算事后追溯)。

第三种:让 Docker 自己轮转。

严格说 Docker 没有「立即轮转」的命令。docker system prune 不会动日志(它清的是未使用的镜像、容器、网络、构建缓存,日志不在其列)。真正让文件轮转的唯一方式是达到 max-size 阈值,或者重建容器。所以在配置生效之前,实务顺序只能是:先 truncate 清空存量,再重建容器让新配置生效。

顺便提醒一个更常见的误操作:看到 docker system df 显示很大的数字,就执行 docker system prune -a。这会删掉所有未被运行中容器引用的镜像,包括你打算回滚用的旧版本。日志文件和它没有任何关系,但很多人把它当成「清空间」的万能命令,结果回滚时发现镜像没了。

四、容器日志与宿主机日志的分层关系

最容易被忽视的一点是:Docker 日志和系统日志是两套体系,需要分两层管理。

第一层是映射到宿主机的日志。如果容器里跑的是 Nginx,通常会把 access.log 和 error.log 通过 volume 挂到宿主机目录:

volumes:
  - ./logs/nginx:/var/log/nginx

这些文件在宿主机上、容器外,logrotate 完全能管,用标准的 /etc/logrotate.d/nginx 配置就行。但这里有个必须注意的顺序问题:logrotate 轮转之后,Nginx 仍然握着旧文件句柄,必须让它重新打开日志文件,否则日志继续写进 access.log.1:

/srv/site/logs/nginx/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    sharedscripts
    postrotate
        docker kill -s USR1 nginx 2>/dev/null || true
    endscript
}

docker kill -s USR1 就是给容器内的 Nginx 进程发 HUP/USR1 信号,让它重新打开日志文件。这比 docker exec nginx nginx -s reopen 轻量,不需要 exec 新进程,也不会因为容器里没有 nginx 命令而失败。

第二层是没挂载出来的应用日志。这些日志走 stdout,被 json-file 驱动接走,写在 /var/lib/docker/containers/ 下,只能通过 max-size/max-file 控制。这一层没有任何外部工具能介入(除非换成别的日志驱动),所以配置必须在一开始就做对。

两层之间的判断依据很简单:日志在容器里是否写到了挂载目录。写了就归 logrotate 管,没写(输出到终端)就归 Docker 的 max-size 管。很多站点出问题,就是因为一半日志在挂载目录、一半在 json-file,只配了其中一层。

五、换日志驱动:什么情况下值得,什么情况下不必

json-file 之外还有几种驱动,值得简单说一下取舍。

local 驱动是 json-file 的改良版,同样是本地文件,但内部用了更紧凑的二进制格式,并且默认就带轮转(每个文件 20MB、保留 5 个)。如果你不需要用 docker logs 之外的任何工具读日志,也没有集中收集的需求,local 是更省心的默认值。代价是日志文件不能再用 json-file 的格式解析——任何依赖 JSON 逐行解析的第三方脚本都会失效。

syslog、journald 驱动把容器日志转给宿主机日志体系。journald 在 systemd 发行版上比较自然,优点是通过 journalctl CONTAINER_NAME=xxx 统一查,轮转由 journald 自己的 SystemMaxUse 管,不用再加一层配置。缺点是 journald 一旦写满,所有容器的日志会一起丢,而且 journald 的默认压缩策略在高并发下会有可见的 CPU 开销。

fluentd、loki 这类驱动适合有集中日志平台的多机场景。单机站用它们属于过度工程——引入一个日志收集服务本身就要占几百 MB 内存,为了省几百 MB 日志空间不划算。

我的选择是:单机小站用 json-file + max-size,因为有 docker logs 的便利;容器数量超过十个、或者开始有多台机器,换 local 或 journald。判断标准不是「哪个更先进」,而是「你有没有超过一台机器」。

六、运维巡检:三个数字每周看一次

配好之后不代表不用管。每周花两分钟看三个数字就能提前发现问题。

第一个是日志文件总占用:

du -sch /var/lib/docker/containers/*/*-json.log | tail -1

如果这个数字在持续增长且没有稳定下来,说明某个容器的 max-size 没生效——最常见的原因是那个容器是在配置修改之前创建的。

第二个是日志增长速率。比较两天同一时刻的文件大小,差值除以时间就是速率。一个正常的应用容器,每天的日志量应该在同一量级波动;如果突然涨了十倍,通常是应用在刷错误日志,日志问题只是表象。

第三个是轮转是否真的在发生。看目录里是否存在 -json.log.1、.2 这些文件:

ls -la /var/lib/docker/containers/*/ | grep -c 'json.log.[0-9]'

一个跑了几天、日志量中等的容器,应该有至少一个轮转出来的备份文件。如果全是 -json.log 没有编号文件,要么日志量太小还没触发,要么 max-size 设得太大。

最后补一个常被忽略的细节:/var/lib/docker 所在的文件系统如果和根分区是同一个,日志涨起来会把根分区一起撑满,系统日志、包管理、临时文件全部受影响。有条件的站点应该把 /var/lib/docker 单独挂一块盘,把这类「不受控增长」的容量风险隔离出去。这比任何日志配置都更根本。

Last modification:November 14th, 2026 at 07:30 pm

Leave a Comment