ECS容器服务把根分区写满的排查记录
那周周三是 21:47,我在告警群里看到网关错误率升到 8.6%。服务进程还在,健康检查也能返回 200,但新请求开始返回 502 Bad Gateway。我登录 ECS 排查,第一眼看到的不是进程异常,而是根分区已经 100% 使用。
$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 40G 40G 0 100% /
$ docker ps --format 'table {{.Names}}\t{{.Status}}'
NAMES STATUS
gateway-api Up 3 days
nginx-proxy Up 3 days
node-exporter Up 3 days
这台机器是 2 vCPU、4 GiB 内存的系统盘规格,控制台显示按量计费约 0.026 元/GB/天,40 GiB 的日成本约 1.04 元。机器费用不高,但磁盘写满以后,Nginx 无法写访问日志,Node.js 写错误日志也失败了。网关虽然还在监听 3000 端口,实际已经不能完整处理请求。
2024-06-12T21:47:13.842Z error request_id=req_7fd2 write log failed: ENOSPC: no space left on device, write
2024-06-12T21:47:13.843Z error request_id=req_7fd2 send response failed: errno=28
我一开始以为是 Node.js 内存泄漏,甚至准备把 4 GiB 内存扩到 8 GiB。我先看了 docker stats --no-stream,三个容器的内存占用都没有接近上限,宿主机 free -h 也还有 1.6 GiB available。扩容内存并不能解决 ENOSPC,这是我当时判断得太快。
日志文件没有按预期轮转
服务通过 Docker Compose 启动,配置文件里用了 json-file 驱动,但驱动参数写在了错误的层级。我把它改成了 logging.options.max-size: "50m" 和 logging.options.max-file: "3",运行中的容器没有应用新配置。我直接重启后看了一眼启动日志,果然出现 unknown log opt 'max-size' for driver json-file。
services:
gateway-api:
image: registry.hz.internal/gateway-api:2.8.4
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
真实原因不是这一个配置项。Compose 文件后来补正过,但线上容器最初是用 docker run 部署的,配置文件与实际运行方式对不上。我核对 docker inspect gateway-api --format '{{json .HostConfig.LogConfig}}',返回的 Config 是空的,说明 Docker 仍使用默认日志配置。结果是三个容器都没有限制日志大小,其中 gateway-api 单个日志文件已经涨到 31.6 GiB。
我试图用 docker update --log-opt max-size=50m --log-opt max-file=3 gateway-api 原地修改,命令返回成功,但 docker inspect 里的参数没有变化。这个失败尝试让我确认,修改日志参数必须重建容器,旧容器不会因为 update 获得轮转策略。
这台机器上的镜像也用了 overlay2 存储驱动,旧镜像、解包缓存和容器日志混在同一个根分区里。执行 docker system df -v 后,我看到 Images 占 6.2 GiB,Containers 占 2.7 GiB,Local Volumes 占 94 MiB,Build Cache 占 3.1 GiB。
清理时没有直接删容器目录
我最开始想在 /var/lib/docker/containers 下面直接删除 -json.log,文件确实少了几个,df 却只释放了 700 MiB。原因是正在运行的容器仍持有文件描述符,空间不会马上归还。我用 lsof +L1 确认后才发现,删掉文件只是让日志继续写入已经删除的 inode,这一步不但没解决问题,还让排查变得更混乱。
我停掉网关,维护窗口只有 6 分钟,原计划 3 分钟完成清理。把容器逐个重建后,磁盘使用率从 100% 降到 68%,耗时 4 分 18 秒。
$ docker compose down
$ docker image prune -f
$ docker builder prune -f --filter until=168h
$ docker compose up -d --force-recreate
$ df -h /
这里不能把 docker compose down 当作固定动作,它会带走依赖容器。我的脚本只处理 gateway、nginx 和 exporter 三个明确的应用,数据库容器不在这个 Compose 项目里。重建时我指定了原镜像的提交号 2.8.4,没有使用 latest,避免拉取到不同摘要的镜像。
重建后仍然保留 22 GiB
重建完成后,根分区用了 27.4 GiB,我盯着这个数字看了几秒,没有继续删系统目录。40 GiB 系统盘里居然还有 22 GiB 被占用,这不符合我对一台只有三个容器的机器的预期。
$ sudo du -xh --max-depth=1 / 2>/dev/null | sort -h
1.1G /boot
2.4G /opt
4.8G /var
18G /home
$ sudo du -xh --max-depth=2 /home 2>/dev/null | sort -h | tail
18G /home/app
18G /home/app/archive
/home/app/archive 是发布系统留下的历史包,每小时同步一次,已经积累了 417 个目录。目录名称带日期,但脚本的清理条件写成了“文件超过 30 天才删除”,压缩包本身由 rsync 保留 mtime,每次同步又更新 mtime,所以它们永远不满 30 天。我先检查脚本,确认错误在 find ... -mtime +30,而不是 OBS 上传程序。
我先保留最近 7 天的发布包,把其余 389 个包移到挂载的 OBS 归档桶,再用临时目录做软链接。释放磁盘 8.7 GiB 后,根分区使用率降到 46%。这一步耗时 11 分 06 秒,OBS 存储 9.2 GiB,桶内数据在华北-北京,按控制台当时的标准存储单价估算约 0.099 元/月。直接删掉归档包也能马上腾出空间,但发布系统下一次同步会重新下载,所以我没有这么做。
补上磁盘和 inode 告警
这次故障只有磁盘容量告警,没有 inode 告警。我后来把 CES 告警拆成三组,每组单独设置静默窗口,避免同一条发布通知触发多个主题。阈值没有直接照搬默认值,而是按线上日志增长量设置。
disk_used_percent > 75 for 5 minutes
disk_used_percent > 88 for 2 minutes
disk_free_bytes < 4GiB for 5 minutes
inode_used_percent > 80 for 10 minutes
我把日志驱动限制为单文件 50 MiB、最多保留 3 份,Node.js 版本固定为 20.15.1,镜像标签从 latest 改成提交号。ECS 里还配置了 log-opt max-size=50m 和 max-file=3,Compose 与实际启动方式统一后才算真正生效。重建后的容器日志上限是 150 MiB,连续跑了两天,最大文件停在 47.8 MiB,没有再出现无限增长。
目前系统盘仍是 40 GiB,我没有贸然扩容。下一轮压测会生成每天约 1.8 GiB 的调试日志,我会继续观察 72 小时,再决定是否改成 60 GiB,或者把归档任务改成发布流水线直接上传 OBS。现在至少不会再因为一个 -json.log 文件把整个根分区撑满。
- 点赞
- 收藏
- 关注作者
评论(0)