Docker容器跑在云服务器上磁盘IO打满了,先查哪里

云服务器上跑 Docker,CPU 看着不高,内存也没爆,但业务接口开始抖,SSH 登录卡,数据库偶尔 timeout。这个时候很多人第一反应是进容器看进程,实际使用中发现,磁盘 IO 打满时,先别急着钻容器,应该先在宿主机把 IO 状态看清楚。

Docker 容器不是虚拟机,它的文件系统、日志、镜像层、volume,最后都落到宿主机磁盘上。容器里看到的慢,通常只是结果;真正排查入口在宿主机的块设备、文件系统和 Docker 存储目录。

先看宿主机磁盘是不是已经顶住了

第一眼看 iostat,比看 top 更准。top 里的 wa 只能说明 CPU 在等 IO,但不知道是哪块盘、等到什么程度。

常用命令:iostat -x 1

重点看这几个字段:%utilawaitr/sw/srkB/swkB/saqu-sz。如果 %util 长时间 90% 以上,await 从几毫秒涨到几十毫秒甚至几百毫秒,基本可以判断磁盘已经在排队。

这里补充一点,云服务器上的云盘不一定是本地 SSD,很多时候后端是分布式存储。你看到的 %util 不只是本机磁盘忙,也可能是云盘规格的 IOPS 或吞吐打到了上限。比如一块普通 40G 云盘,可能随机写只有几百到一两千 IOPS,Docker 日志、MySQL binlog、Redis AOF 同时写,很容易顶满。

一个常见现场数据

iostat -x 1 看到类似这种情况,就不用怀疑业务为什么慢了:

Device: vda | r/s 12 | w/s 1850 | await 96.4 | aqu-sz 178 | %util 99.8

这类数据说明写请求非常多,平均等待已经接近 100ms。对于 Web API 来说,某个请求里只要碰一次同步落盘,就可能被拖慢;对于数据库来说,提交事务会明显抖动。

别只看 docker stats,它看不出磁盘真凶

docker stats 能看到 CPU、内存、网络,也能看到 Block I/O 的累计值,但它不适合判断当前谁在把磁盘打满。Block I/O 是累计读写量,不代表当前压力,也不代表等待延迟。

更直接的办法是在宿主机上看进程级 IO:

pidstat -d 1

重点看 kB_rd/skB_wr/siodelay。如果能看到某个 mysqldjavanginxdockerdcontainerd 写入很高,就继续往下追。

有时候 pidstat 看到的是 dockerdcontainerd-shim,不要误判成 Docker 自己有问题。Docker 只是帮容器处理日志、文件层、进程管理,后面真正写入的可能是某个容器的 stdout 日志,或者 overlay2 里的业务文件。

先查 /var/lib/docker,很多 IO 都藏在这里

默认情况下,Docker 的数据目录在 /var/lib/docker。容器可写层、镜像层、容器日志、volume 元数据都在这里。磁盘 IO 打满时,这个目录是重点。

先看目录占用:

du -sh /var/lib/docker/*

重点关注 containersoverlay2volumes

/var/lib/docker/containers 下面通常是容器日志,默认 json-file 日志驱动会把容器 stdout/stderr 写成 *-json.log。如果业务把访问日志、debug 日志、错误堆栈全部打到控制台,Docker 就会持续写 json 日志。这个场景非常常见,而且很隐蔽。

可以这样查大日志:

find /var/lib/docker/containers -name "*-json.log" -size +1G -ls

实际使用中发现,有些 Java 服务开启 debug 后,单容器一天写 30G JSON 日志,磁盘吞吐不一定一直很高,但随机追加、日志轮转、压缩叠在一起,IO 延迟会明显升高。

Docker json 日志要限制大小

生产环境不建议让 json-file 无限写。可以在 /etc/docker/daemon.json 里配置日志轮转:

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

修改后需要重启 Docker,新容器才会使用新配置。已经存在的容器通常要重建才会生效。这里要注意,重启 Docker 会影响容器,业务窗口要安排好。

overlay2 写放大也要看,尤其是容器里频繁改小文件

Docker 默认使用 overlay2。它适合跑无状态应用,但不适合把大量频繁变化的数据直接写在容器可写层里。

比如应用在容器内部路径 /app/data 不断写缓存、小文件、临时文件,如果这个路径没有挂 volume,那这些写入会进入 overlay2 的 writable layer。overlay2 有 copy-on-write 机制,某些场景会有额外开销,尤其是大量小文件创建、修改、删除时,IO 放大很明显。

这类问题可以从两个方向看:

docker inspect 容器ID,看 GraphDriverMounts

如果业务数据目录没有挂到 volume 或宿主机目录,建议改。数据库、对象缓存、上传文件、索引文件,不要直接放容器层。把它们挂到独立数据盘或性能更稳的云盘路径上,例如 /data/mysql/data/es/data/app_upload

数据库容器优先查 fsync、binlog、慢查询临时表

磁盘 IO 打满,数据库容器是高频来源。MySQL、PostgreSQL、MongoDB、Elasticsearch 都可能把云盘打满,但表现不一样。

MySQL 常见的是 redo log、binlog、doublewrite、临时表落盘。业务写入并发上来后,如果云盘 IOPS 不够,事务提交会被 fsync 卡住。现象通常是 CPU 不高,但 SQL 延迟飙升。

MySQL 可以看:

SHOW GLOBAL STATUS LIKE 'Innodb_data_fsyncs';

SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';

SHOW ENGINE INNODB STATUS\G

如果 Created_tmp_disk_tables 增长很快,说明很多查询的临时表落盘了,IO 不一定是写业务数据造成的,可能是排序、分组、join 撑爆内存临时表后落到了磁盘。

多说一句,数据库跑容器不是不行,但数据目录一定要挂 volume,最好挂到独立数据盘。不要把 MySQL datadir 丢在 overlay2 里面跑生产,这种问题迟早会遇到。

日志类容器也很容易把磁盘打满

ELK、Loki、Prometheus、VictoriaMetrics、Fluent Bit、Filebeat 这类组件,经常被当成辅助服务放在同一台云服务器上。问题是它们本身就是 IO 密集型。

Prometheus 写 TSDB,Elasticsearch 写 segment,Loki 写 chunk 和 index。如果业务容器、数据库容器、日志组件全在同一块系统盘上,晚上流量一高,磁盘队列会很难看。

一个比较典型的配置是 4C8G 云服务器,系统盘 80G 普通 SSD,同时跑 API、MySQL、Redis、Prometheus、Loki。白天看没事,晚上定时任务加报表查询,await 从 3ms 到 150ms,接口开始批量超时。最后不是 CPU 不够,也不是内存不够,是单块盘扛了太多角色。

确认是不是云盘规格上限,而不是 Linux 参数问题

很多排查会跑偏到内核参数、文件系统参数、Docker 参数。参数当然能调,但先确认云盘本身的规格。

云服务器磁盘一般有几个限制:容量、吞吐、IOPS、突发额度。小容量云盘通常基准 IOPS 较低,有些平台按容量线性给性能,比如 20G 盘和 200G 盘不是一个级别。业务写入量没变,只是数据变多、索引变大、日志变多,原来的云盘就可能不够。

可以用 fio 在低峰期压测,但要小心别把生产打挂。更稳妥的方式是看云厂商控制台里的磁盘监控:IOPS、吞吐、IO 使用率、读写延迟。如果控制台显示磁盘达到性能上限,服务器里面再怎么优化都只能缓解。

几个现场场景的数据感

小型 Web 服务,日志正常、数据库外置,系统盘写入长期低于 5MB/s,IOPS 几十到几百,普通 SSD 云盘通常够用。

单机 MySQL 写入型业务,binlog 开启,QPS 1000 左右,事务提交频繁,随机写 IOPS 可能上千到几千,普通小盘容易抖。

日志采集节点,持续接收 Nginx access log,每秒几千到上万行,磁盘顺序写吞吐可能不夸张,但索引、压缩、切分会带来额外 IO。

Elasticsearch 单节点跑搜索和写入,segment merge 时 IO 会突然升高,平时看着正常,merge 一来全盘等待。

容器限速不是万能药,但能防止单容器拖死整机

如果已经定位某个容器写盘太猛,可以考虑做 blkio 限制。Docker 支持对设备读写速率做限制,例如限制某容器写入某块盘的吞吐。

类似参数有:--device-write-bps--device-read-bps--device-write-iops--device-read-iops

比如某个日志处理容器不能让它写爆系统盘,可以限制写入速度。但这只是隔离手段,不是根治。限制太狠会导致容器内部队列堆积,日志延迟、数据消费延迟照样会出现。

更合理的做法是把 IO 密集型服务拆出去,或者至少把数据盘拆开:系统盘跑 OS 和 Docker,数据盘给数据库,日志盘给日志组件。磁盘角色分开后,问题会少很多。

df 没满也会慢,inode 和文件数量也要看

磁盘 IO 慢不一定是空间满。小文件太多、inode 接近耗尽、目录项过大,也会导致操作变慢。

查空间:df -h

查 inode:df -i

查某目录文件数量:find /var/lib/docker/overlay2 -xdev -type f | wc -l

如果某个业务容器疯狂生成临时文件,磁盘空间可能只用了 60%,但 inode 已经很紧张。还有一种情况是删除了大日志,但空间没释放,因为进程还持有文件句柄。

可以查:

lsof | grep deleted

看到某个进程还持有 deleted 文件,就需要重启对应进程或容器。直接 rm 文件只是目录项没了,磁盘块还没真正释放。

宿主机 dmesg 不能漏看

如果 IO 打满伴随文件系统报错、磁盘错误、只读挂载,必须看内核日志。

dmesg -T | egrep -i "error|fail|reset|ext4|xfs|blk|nvme|scsi|io"

云服务器虽然看不到真实硬盘,但系统层还是可能报出块设备 reset、文件系统 journal error、I/O error。出现这类日志,优先联系云服务商排查底层存储或迁移实例,不要继续在业务层硬调。

容器目录和数据盘最好从部署时就分开

很多 IO 问题不是突然出现,而是部署时把所有东西都丢到系统盘里。系统盘同时承担 OS、Docker overlay2、容器日志、数据库数据、应用上传文件,早期流量小没感觉,业务起来后就变成单点瓶颈。

建议把 Docker 数据目录迁到独立盘,或者至少把关键 volume 放到独立数据盘。例如把数据盘挂载到 /data,数据库 volume 放 /data/mysql,日志放 /data/logs,上传文件放 /data/uploads

如果是新购云服务器,尽量别只看 CPU 和内存。Docker 场景要看磁盘类型、IOPS、吞吐、是否 U.2、是否适合高频写。如果业务同时还有防护需求,比如游戏服、接口服务容易被 DDoS 打,选机型时可以顺手把防护和磁盘性能一起看。像宁波高防-特惠这种 8C16G、100G U.2、100Gbps 防御的配置,就比普通小盘机器更适合跑有一定写入压力的容器业务。如果你也在找这种高防云服务器或海外大带宽服务器,可以看看129云,他们有高防、G口大带宽、海外云计算产品线,客服热线 400-9177118 可以直接问具体磁盘和线路规格。

排查顺序可以按这个思路走

宿主机先确认整体 IO

先跑 iostat -x 1,确认是哪块设备高、读高还是写高、await 是否异常。如果 await 正常,只是吞吐高,和 await 很高导致排队,是两种处理方式。

再定位进程和容器

pidstat -d 1 找进程,再通过 docker psdocker inspectps aux 对上容器。看到 containerd-shim 不要停在这里,继续查容器内应用、日志路径、volume 挂载。

然后看 Docker 自身目录

/var/lib/docker/containers 的 json 日志,查 /var/lib/docker/overlay2 是否被业务写爆,查 /var/lib/docker/volumes 是否有数据库或日志组件占用异常。

接着看业务类型

数据库看 fsync、binlog、临时表。日志系统看写入、索引、压缩、保留周期。对象存储类服务看小文件数量。Java 服务看 GC 日志、应用日志、临时文件。Nginx 看 access log 是否同步写到容器 stdout 和文件两份。

最后确认云盘规格和底层异常

看云控制台磁盘监控,确认 IOPS、吞吐、延迟是否顶到规格。再看 dmesg 是否有 I/O error、文件系统错误。如果规格不够,扩盘、换高性能盘、拆分数据盘;如果有底层错误,迁移或联系服务商处理。

几个容易误判的地方

CPU wa 高不等于一定是 Docker 问题

wa 高只是 CPU 等 IO。Docker 只是运行形态,真正原因可能是云盘性能、数据库写入、日志策略、文件系统错误。不要看到容器就把锅丢给 Docker。

清理镜像不一定能解决 IO 慢

docker system prune 能释放空间,但如果当前瓶颈是持续写入和高 await,清理镜像只能改善容量,不会让云盘 IOPS 变高。生产环境执行 prune 还要确认别删掉未使用但后续要回滚的镜像和 volume。

把日志删掉以后空间没回来,不是 df 坏了

大概率是进程还持有文件句柄。查 lsof | grep deleted,重启对应容器后空间才会释放。

把数据库放容器里没问题,问题是放在哪里

数据库容器本身不是原罪,数据目录放 overlay2、系统盘性能太弱、日志无限写、备份任务和业务写入抢同一块盘,才是常见问题。volume、独立盘、合理备份窗口,比纠结容器还是裸进程更关键。

处理时别一上来就重启整机

IO 打满时重启整机可能短暂恢复,因为日志文件关闭了、队列清了、某些临时任务停了。但如果根因还在,过一会儿又会复现。更麻烦的是数据库在高 IO wait 状态下被强重启,可能触发恢复流程,启动更慢。

更稳的处理方式是先降写入:临时降低日志级别,暂停非核心批处理,停掉异常写盘容器,关闭不必要的调试日志。然后再做迁移、扩盘、拆盘、调整 Docker 日志策略。

如果已经卡到 SSH 都慢,可以从云控制台进 VNC 或救援模式处理。优先保住数据目录,不要随手删 /var/lib/docker/volumes。很多数据库数据就在这里,删错比 IO 打满更难处理。

一个比较贴近生产的处理例子

某台 8C16G 云服务器跑 12 个 Docker 容器:Nginx、业务 API、MySQL、Redis、Prometheus、Loki、Grafana,还有几个 worker。故障表现是接口 P99 从 200ms 飙到 5s,MySQL 偶发 lock wait timeout,机器 load 40 多,CPU 使用率只有 30%。

iostat -x 1 显示 vda %util 99%await 180ms,写 IOPS 2000 左右。pidstat -d 1 看到 dockerd 和一个 java 进程写入很高。

继续查 /var/lib/docker/containers,发现一个业务容器的 *-json.log 已经 78G,应用同时还在容器内写一份业务日志。也就是说,同一条日志写了两遍:stdout 一遍,文件一遍。Loki 又在采集这些日志,形成第三层 IO 压力。

临时处理是把业务日志级别从 debug 改回 info,停掉异常 worker,truncate 超大 json 日志,并重启对应容器释放句柄。后续处理是配置 Docker json-file 轮转,把业务日志改为单路径输出,Loki 只采集文件日志,不再采集容器 stdout;MySQL 数据目录迁到独立数据盘,Prometheus 保留周期从 30 天改成 7 天。

调整后同样流量下,await 从 100ms 以上降到 5ms 到 15ms,%util 峰值还有 70% 左右,但业务延迟恢复正常。这里的关键不是某条神奇命令,而是把宿主机 IO、Docker 日志、业务写入、云盘规格放在一起看。

线上可直接用的命令片段

看磁盘等待

iostat -x 1

看进程 IO

pidstat -d 1

看 Docker 日志大文件

find /var/lib/docker/containers -name "*-json.log" -size +500M -ls

看 Docker 目录占用

du -h --max-depth=1 /var/lib/docker | sort -h

看 inode

df -i

看已删除但仍占用空间的文件

lsof | grep deleted

看内核 IO 错误

dmesg -T | egrep -i "error|fail|reset|ext4|xfs|blk|nvme|scsi|io"

看容器挂载

docker inspect 容器ID | grep -A 20 Mounts

哪些情况该直接升级或换盘

如果业务已经确认是正常写入,不是异常日志、不是临时文件、不是慢查询造成的额外落盘,那就别在原盘上硬抠。比如订单系统高频写 MySQL、日志平台持续写入、游戏服保存状态和战斗日志、对象服务大量小文件,这些都是天然吃 IO 的场景。

普通云盘可以跑轻量服务,但写入型业务更适合高性能 SSD、U.2 或者带明确 IOPS 保障的磁盘。系统盘和数据盘分离也很重要,别让 Docker overlay2 和数据库抢同一个队列。

海外业务还要注意网络和磁盘是两件事。比如美国精品大宽带-A型有 1Gbps 峰值和三网精品线路,适合轻量出海业务、下载分发、小型代理类应用;但如果本地磁盘持续高写入,仍然要关注 SSD 容量和 IOPS。英国大宽带这种 1Gbps 峰值更偏大带宽场景,不保证大陆网络访问,适合面向海外用户的流量型服务。选机器时把带宽、线路、防御、磁盘写入压力分开看,别只盯着一个指标。

遇到 Docker 容器磁盘 IO 打满,入口放在宿主机:iostat 看盘,pidstat 找进程,/var/lib/docker 查日志和 overlay2,数据库看 fsync 和临时表,云控制台确认云盘规格。