服务器磁盘告警,du -sh一查,发现/var/lib/docker/containers占了上百G。这种情况我每年至少帮人处理七八回,而且每次都是同一个原因:容器日志没限制。
Docker默认的日志驱动是json-file,这个驱动会把容器的标准输出和标准错误全部写到宿主机的JSON文件里,而且默认不轮转、不限制大小。一个容器跑个一星期,日志文件干到几十G是家常便饭。如果你的容器里跑的是Java应用,GC日志加上业务日志全打stdout,那磁盘撑爆的速度比你想象的要快得多。
这篇文章直接给方案:怎么查、怎么清、怎么从根本上限制,一次解决。
先看看磁盘到底被谁吃了?
别急着删东西,先搞清楚是什么占的空间。
# 查看磁盘整体使用情况
df -h
# 定位Docker目录的大小
du -sh /var/lib/docker/
# 进一步看是哪个子目录大
du -sh /var/lib/docker/containers/* | sort -rh | head -10
第二条命令会列出所有容器ID目录的大小,按从大到小排,最大的那几个就是罪魁祸首。记下容器ID,后面有用。
如果你用的是devicemapper存储驱动(老版本Docker常见),日志路径可能不太一样,但绝大多数场景下json-file的日志都在 /var/lib/docker/containers/<容器ID>/<容器ID>-json.log 这个文件里。
确认一下某个容器的日志文件到底有多大:
ls -lah /var/lib/docker/containers/<容器ID>/<容器ID>-json.log
看到文件大小的时候别吃惊,我见过最大的是单文件280G,直接把整个数据盘写爆了。
临时清理:先让磁盘喘口气
磁盘告警的时候,最紧急的任务是马上释放空间,让服务先恢复。这时候直接用truncate命令清空日志文件,不要用rm删除。
为什么要用truncate而不是rm?因为容器运行时,Docker会一直持有这个日志文件的文件句柄。你用rm删了文件,磁盘空间并不会立即释放,要等到容器停止或者进程退出才行。而truncate是直接把文件内容截断,空间实时释放,效果立竿见影。
# 先把文件内容清空,保留文件本身
truncate -s 0 /var/lib/docker/containers/<容器ID>/<容器ID>-json.log
清空之后再看磁盘使用率,应该马上就降下来了。
如果你不确定具体是哪个容器,或者想一次性清空所有容器的日志,可以用这条命令:
find /var/lib/docker/containers/ -name "*-json.log" -exec truncate -s 0 {} \;
这条命令会找到所有容器的json.log文件,逐一清空。执行之前最好先确认一下有哪些日志文件特别大:
find /var/lib/docker/containers/ -name "*-json.log" -exec ls -lh {} \;
看到过大的文件再决定要不要全清,别把想保留的日志也清掉了。
根本解决:配置日志轮转和大小限制
手动清空只是救急,治本必须给Docker配置日志轮转策略。Docker提供了两种方式:一种是修改daemon.json做全局默认配置,一种是在docker-compose里针对单个容器配置。两种都用得上。
方案一:修改Docker Daemon全局配置
编辑 /etc/docker/daemon.json,如果文件不存在就新建一个:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3",
"compress": "true"
}
}
配置解释:
max-size: "100m":单个日志文件最大100MB,超过这个大小就轮转。
max-file: "3":保留最近3个轮转文件。算下来日志总大小上限就是100MB × 3 = 300MB。
compress: "true":轮转后的旧文件自动压缩成gzip格式,进一步节省空间。
改完之后需要重启Docker服务让配置生效:
systemctl daemon-reload
systemctl restart docker
注意:这个配置只对重启之后新建的容器生效,已经存在的容器不会自动应用新配置。已经在跑的容器需要重建才会用新的日志策略。
另外,daemon.json 里可能已经有其他配置项(比如registry-mirrors、insecure-registries),千万别直接覆盖,用编辑器追加内容进去,保证JSON格式正确。改完后用 docker info | grep "Logging Driver" 确认当前的日志驱动已经变成json-file。
方案二:docker-compose里单独配置
如果你用docker-compose管理容器,可以在每个服务的配置里单独指定日志限制,这个方式更灵活,不用重启整个Docker服务:
version: '3.8'
services:
web:
image: nginx:latest
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "2"
ports:
- "80:80"
这样只对web这个容器生效,其他容器不受影响。适合那种日志量特别大的服务单独给大一点的配额,普通的服务给个小配额。
方案三:docker run时直接指定
如果你习惯用docker run启动容器,可以在命令行里加上日志参数:
docker run -d \
--log-driver json-file \
--log-opt max-size=50m \
--log-opt max-file=2 \
nginx:latest
这种方式适合临时测试或者少量容器管理的场景,生产环境大规模部署不太方便。
对已存在的容器如何应用新配置?
这是一个比较常见的问题:daemon.json改了,但老容器还在用旧配置,日志文件还在疯涨。
解决办法是重建容器。如果你的容器是有状态的服务(比如数据库),操作要谨慎。对于无状态的服务,重建很简单:
docker-compose down
docker-compose up -d
对于单独运行的容器,先停止再重新创建:
docker stop <容器名或ID>
docker rm <容器名或ID>
docker run ... # 带新的日志参数重新创建
如果容器用的是volume持久化数据,重建不会有数据丢失。但如果是数据库容器,建议先做好备份再操作。
还有一种更进阶的做法:不用重建容器,而是用logrotate配合cron来轮转Docker的日志文件。这个方案的逻辑是在宿主机层面,用系统自带的logrotate工具定期对Docker的json.log文件做轮转和压缩。考虑到Docker本身已经提供了完善的日志管理机制,一般没必要再用logrotate绕一圈。除非你有特殊需求,比如想保留更长时间的日志归档,或者想对日志做额外的处理(比如上传到对象存储),这种场景下可以组合使用。
日志驱动的选择:不只是json-file
Docker支持的日志驱动远不止json-file。如果你的环境对日志管理有更高的要求,可以考虑切换驱动。
syslog驱动:把所有容器的日志统一发给syslog,再由syslog负责轮转和管理。适合老派运维风格,日志集中到系统日志里统一处理。
fluentd驱动:直接把日志发到fluentd日志收集器,然后再转给Elasticsearch或者其他存储。这是云原生环境下比较流行的方式,尤其配合EFK/ELK栈做日志分析的时候,省去了在宿主机上存日志的环节。
gelf驱动:发给Graylog,也是日志分析方向的。
none驱动:不要日志,完全不记录。测试环境用可以,生产环境慎用。
切换驱动只需要在daemon.json里改 log-driver 的值,同时配上对应的 log-opts:
{
"log-driver": "fluentd",
"log-opts": {
"fluentd-address": "localhost:24224"
}
}
一些容易被忽略的细节
journald日志也别忘:如果你的系统用了journald,并且Docker容器把日志打到了journald里,那systemd-journald本身也可能有日志占空间的隐患。检查journald的占用:
journalctl --disk-usage
如果占用太大,调整 /etc/systemd/journald.conf 里的 SystemMaxUse 参数。
docker system prune的局限:很多人以为 docker system prune -a 会清理日志文件,但实际并不会。这个命令清理的是:停止的容器、未使用的网络、悬空的镜像、构建缓存,日志文件不在这之列。想单独清理日志,得用上面说的truncate或者配置自动轮转。
查看当前容器的日志配置:想知道某个容器当前使用的是哪个日志驱动、限额是多少,用inspect命令:
docker inspect <容器ID> | jq '.[0].HostConfig.LogConfig'
返回结果里会显示driver和options,方便确认配置是否生效。
容器日志管理这件事,说大不大说小不小。配置好了,它安安静静地在角落里轮转压缩,你根本感觉不到它的存在。配置不好,它能在你睡觉的时候把磁盘撑爆,然后你被监控告警吵醒,半夜起来连服务器做应急处理。哪种体验更好,你自己选。
推荐文章
