Docker容器部署后服务器磁盘IO被打满该怎么查

Docker容器上线后,服务器突然变卡,top里CPU不一定高,load average却一路往上飙,接口响应从几十毫秒变成几秒,SSH敲命令都有延迟。这种场景里,很多人第一反应是看CPU和内存,但实际使用中发现,问题经常卡在磁盘IO上。

尤其是把应用从裸机迁到Docker之后,磁盘写入路径变了,日志方式变了,overlay2也参与进来了。以前看起来没问题的业务,容器化以后可能会把磁盘打满。

先确认是不是IO问题,不要一上来就重启容器

机器卡的时候,先看load。命令用 uptime 或 top。比如4核机器load跑到20、30,但CPU user不高,wa比较高,这时就要怀疑IO等待。

top里重点看这一行:%Cpu(s) 里面的 wa。如果 wa 长时间超过20%,业务明显变慢,基本可以认为磁盘IO已经影响服务了。线上见过比较典型的情况是,CPU idle还有50%,但wa有35%,应用日志不断刷,接口超时一片。

再用 iostat 看细节。命令:iostat -x 1。重点看 await、util、r/s、w/s、rkB/s、wkB/s。

如果 util 长时间接近100%,await从几毫秒涨到几十甚至几百毫秒,磁盘就是堵住了。SSD正常情况下await通常比较低,1ms到5ms比较常见;机械盘或者低配云盘在高并发写入时,await到50ms以上就会明显拖慢业务。

这里补充一点,util 100%不等于磁盘坏了,它表示设备层面已经很忙。就像一个收费站一直有车排队,不一定是路断了,但通行能力已经顶住了。

确认是哪个进程在写磁盘

系统级确认后,要把锅定位到进程。常用命令是 iotop -oPa。-o只显示有IO的进程,-P按进程显示,-a看累计值。

如果看到 dockerd、containerd、java、node、nginx、mysqld 这类进程持续写入,就要继续往下查。线上最常见的是应用日志、容器JSON日志、数据库临时文件、队列落盘、缓存持久化。

也可以用 pidstat:pidstat -d 1。它能看到每个进程每秒读写多少KB。比iostat更适合找具体进程。

有些机器没装工具,临时装一下 sysstat 和 iotop。CentOS用 yum install -y sysstat iotop,Debian或Ubuntu用 apt install -y sysstat iotop。

Docker场景要重点看容器日志

Docker默认json-file日志驱动,如果不限制大小,容器标准输出会一直写到宿主机磁盘。这个问题非常常见,尤其是Java应用、Node应用、Python脚本,调试日志没关,容器一跑就是几百MB、几十GB。

查看容器日志文件大小可以用:du -sh /var/lib/docker/containers/*/*-json.log。也可以进一步按大小排序:du -h /var/lib/docker/containers/*/*-json.log | sort -hr | head。

如果看到某个json.log几十GB,基本就对上了。再通过路径里的容器ID去查容器:docker ps --no-trunc | grep 容器ID前几位。

这里要注意,直接 rm 日志文件不推荐。文件被dockerd占用时,rm只是删除目录项,空间不一定马上释放。临时止血可以用 truncate:truncate -s 0 /var/lib/docker/containers/容器ID/容器ID-json.log。

长期处理要配置日志轮转。daemon.json里加类似配置:log-driver为json-file,log-opts里设置max-size为100m,max-file为3。改完重启Docker,对新容器生效;已经在跑的容器通常需要重建。

实际使用中发现,很多业务不是磁盘容量先爆,而是日志持续小块写入,把IOPS打满。日志文件只有十几GB,但每秒几千次写入,低配云盘一样会被拖死。

看overlay2是不是被频繁写入

Docker默认存储驱动一般是overlay2。容器内写文件,实际会落到宿主机的 /var/lib/docker/overlay2 下面。应用如果把临时文件、上传文件、缓存、编译产物都写进容器层,overlay2会越来越重。

可以先看目录体积:du -h --max-depth=1 /var/lib/docker/overlay2 | sort -hr | head。这个命令能找出占用大的层,但它不直接告诉你对应哪个容器,需要再结合 docker inspect。

更直观的方式是看容器自身大小:docker ps -s。输出里有 Size 和 Virtual Size。Size表示容器可写层大小。如果某个容器Size从几百MB涨到几十GB,就要查应用在容器里写了什么。

常见路径包括 /tmp、/var/tmp、应用的 logs 目录、upload 目录、cache 目录。进入容器查:docker exec -it 容器名 sh,然后 du -h --max-depth=1 / | sort -hr。

多说一句,容器不要当虚拟机用。业务数据、上传文件、数据库数据、需要保留的日志,尽量挂载到宿主机目录或独立数据盘。容器可写层适合短生命周期,不适合高频写入和长期堆数据。

不要忽略Docker build和镜像清理

有些IO打满不是业务容器造成的,而是CI/CD在服务器上频繁docker build。构建镜像会大量读写层、解压包、复制依赖,Node、Java、Go项目都可能把磁盘打得很忙。

排查时看 docker system df,能看到 Images、Containers、Local Volumes、Build Cache 占用。Build Cache如果长期不清理,几十GB很常见。

清理命令要谨慎。docker system prune 可以清理未使用资源,docker builder prune 可以清理构建缓存。生产环境执行前要确认不要误删还要用的镜像和volume。

如果服务器同时跑业务和构建,建议把构建挪到单独机器。线上业务机器最怕这种突发IO:白天流量高,CI一触发,构建过程把磁盘打满,接口超时但容器本身又没挂,看起来很迷惑。

定位容器和宿主机目录的关系

查到某个进程或某个容器后,要弄清楚它到底写到哪里。docker inspect 容器名 看 Mounts,可以看到绑定挂载、volume挂载和路径。

如果Mounts里没有业务数据目录,应用却在容器内写日志或上传文件,那基本就是写进overlay2了。这样的结构不利于排障,也不利于迁移。

建议把日志、数据、上传目录明确挂出来,比如宿主机 /data/app/logs 挂到容器 /app/logs。这样 iostat、du、lsof、iotop 查起来都更直接,也方便把数据盘单独扩容。

如果使用的是云服务器,磁盘类型差异很明显。普通云盘适合轻量业务,SSD或U.2更适合频繁写入场景。业务本身有日志高写入、游戏服落盘、数据库本地部署、高防业务清洗后仍有大量请求日志时,机器配置不能只看CPU和内存。

如果你也在找这种能承载高写入、高并发访问的云服务器,可以看看129云。例如宁波高防-A型配60G U.2,适合中小型高防业务和稳定线路场景;西安电信-E型有16C、16G、80G SSD和50Gbps单机防御,适合西北电信用户访问明显的业务;美国高防-E型带300G防御,适合海外业务和攻击压力比较高的站点。需要按业务写入量、带宽、防御来选型,可以直接打客服热线400-9177118沟通。

用lsof看谁还占着已删除文件

磁盘空间和IO问题经常混在一起。日志删了,df还是不降,这时要看是否有进程占用已删除文件。命令:lsof | grep deleted。

如果看到某个进程占着很大的deleted文件,说明文件名删了,但句柄还在。进程不释放,空间不回收。处理方式是重启对应进程,或者找到fd后清空。线上更稳妥的做法是先确认进程角色,再安排重启窗口。

Docker场景里,容器日志、应用日志、Nginx日志都可能出现这个问题。尤其是手工rm日志以后,空间不释放,很多人会误判为磁盘异常。

数据库容器要单独看,不要只看Docker

MySQL、PostgreSQL、MongoDB、Redis这类服务跑在容器里时,IO被打满不能只盯着Docker。数据库自己的写入机制会制造大量IO,比如binlog、redo log、WAL、checkpoint、AOF rewrite。

MySQL可以看 show processlist、show engine innodb status,结合 iostat 判断是不是刷盘压力过高。Redis开启AOF后,如果appendfsync always,写入会很猛;一般业务更多用 everysec,但也要看数据安全要求。

数据库容器一定要挂载独立数据目录,不建议把数据放在容器可写层。还要确认宿主机文件系统、磁盘调度、云盘性能是否匹配。数据库一旦和应用日志抢同一块低IOPS磁盘,业务抖动会非常明显。

用docker stats只能看部分信息

docker stats能看到CPU、内存、网络、Block I/O。它适合快速看哪个容器读写量大,但它不是完整的磁盘排障工具。

比如某个容器Block I/O显示写了几百GB,可以说明它确实写得多,但不能说明当前磁盘设备是否饱和,也不能看到await和util。所以docker stats要和 iostat、iotop 一起看。

排查时比较顺手的节奏是:top看wa,iostat看盘是否忙,iotop或pidstat找进程,docker ps/docker inspect定位容器和挂载路径,du找大目录,lsof查deleted文件。

线上止血时怎么处理

如果IO已经把业务拖挂,先降低写入。最常见的动作是调低日志级别,把DEBUG改成INFO或WARN,暂停非必要任务,停掉异常容器,限制批处理任务。

容器日志过大时,用 truncate 清空json.log,随后尽快加日志轮转。不要等磁盘彻底写满再处理,磁盘满了以后,数据库、消息队列、容器运行时都可能出现连锁问题。

如果是批量任务打满IO,可以临时用 ionice 降低优先级。例如对某个进程使用 ionice -c2 -n7。这个办法适合备份、压缩、同步这类后台任务,不适合所有业务进程。

如果是磁盘能力确实不够,临时办法只能缓解。该拆盘就拆盘,该换SSD就换SSD,该把日志转到ELK、Loki、对象存储就转出去。把所有写入都堆在系统盘上,是Docker部署里很容易踩的坑。

常见现场表现和对应方向

场景:top里wa很高,iostat util接近100%。方向:磁盘设备已饱和,继续用iotop或pidstat找写入进程。

场景:/var/lib/docker/containers 下json.log巨大。方向:容器标准输出日志未限制,清空后配置max-size和max-file。

场景:docker ps -s 某个容器Size很大。方向:应用把文件写进容器可写层,检查/tmp、logs、upload、cache。

场景:df显示空间不释放,du又看不到大文件。方向:lsof查deleted文件,可能有进程占用已删除日志。

场景:发布时IO飙升,平时正常。方向:docker build、解压依赖、镜像拉取、CI任务在业务机执行。

场景:数据库容器运行一段时间后整机抖动。方向:检查binlog、WAL、AOF、checkpoint和数据盘性能。

配置层面提前规避

Docker daemon建议默认加日志限制。不要等某个业务容器把json.log写到100GB后再补配置。线上比较常用的是单文件100m到500m,保留3到10个文件,具体看日志采集方式和排障需求。

业务日志尽量不要只打到stdout然后完全依赖Docker json-file。小规模服务可以这样用,服务多了以后,日志采集、轮转、保留周期都要统一处理。

容器里的高频写入目录要挂载出来。数据库、队列、对象上传、缓存文件、临时导出文件,最好和系统盘隔离。系统盘主要放操作系统、Docker运行时和少量组件,不要承担全部业务写入。

监控也要加上磁盘IO指标。只监控磁盘使用率不够,IOPS、吞吐、await、util、io wait都要看。很多事故发生前,磁盘空间还有40%,但await已经飙了半小时。

报警阈值可以按业务压测数据来定。没有压测数据时,先粗略设:wa持续5分钟超过20%、await持续超过50ms、util持续超过80%、系统盘使用率超过80%。后面根据实际误报情况再调。

一个真实排查节奏示例

某台8C16G服务器,Docker部署了Nginx、Java API、Redis和一个定时任务容器。上线后半小时,接口开始超时,load从3涨到38,CPU user只有20%左右。

top看到wa在40%上下,iostat -x 1显示系统盘util 99%,await 120ms。iotop -oPa里dockerd和一个java进程写入很明显。

继续查 /var/lib/docker/containers,发现Java容器json.log 46GB,而且还在快速增长。docker logs里大量DEBUG SQL和请求体日志,每秒刷几千行。

处理时先把应用日志级别从DEBUG改成WARN,truncate清空json.log,业务延迟很快恢复。后面补daemon.json日志轮转,把应用日志改成文件输出并挂载到/data/logs,再接入日志采集。定时任务容器单独迁到另一台机器,避免和API抢系统盘IO。

这类问题看起来是Docker导致的,其实Docker只是把日志和写入路径集中暴露出来了。真正要查的还是谁在写、写到哪里、磁盘能不能扛住。