Docker容器跑在云服务器上,磁盘IO变慢先别急着怪Docker

线上遇到容器内写入慢、数据库抖动、日志落盘卡住,很多人第一反应是Docker是不是限制了磁盘IO。实际使用中发现,真正的原因经常不在Docker本身,而是在云服务器磁盘规格、云盘突发额度、宿主机cgroup限制、文件系统、应用写入方式之间来回叠加。

比较典型的现象是:容器里执行写文件很慢,应用日志里出现大量timeout,MySQL、PostgreSQL、Elasticsearch这类服务响应时间突然变长;但CPU不高,内存也够,网络也正常。这个时候就要把问题拆开看:是容器被限速,还是整台云服务器的磁盘被限速。

先确认慢的是容器,还是整台机器

排查时不要一上来就进容器里跑测试。容器看到的文件系统,多数是宿主机上的overlay2、bind mount或者volume。容器慢,不代表Docker限速;宿主机磁盘本身慢,容器一定跟着慢。

在宿主机上先跑一轮基础观察:

iostat -x 1

重点看几个字段:%util、await、r_await、w_await、aqu-sz、rkB/s、wkB/s。如果%util长期接近100%,await从几毫秒涨到几十毫秒甚至几百毫秒,基本可以判断磁盘层已经堵住了。

这里补充一点,云服务器上看到的磁盘可能是/dev/vda、/dev/sda、/dev/nvme0n1,不同虚拟化平台不一样。不要只盯着名字,重点看挂载点对应的是哪块盘。可以用lsblk -f、df -hT、findmnt看清楚容器数据目录到底落在哪个设备上。

Docker默认数据目录通常在/var/lib/docker,如果这个目录在系统盘上,而系统盘本身是低配云盘,容器里再跑数据库或者高频日志服务,就很容易把系统盘打满IO。

用docker stats只能看个大概,不能直接定责

docker stats里有Block I/O字段,但它显示的是容器累计读写量,不是实时IOPS,也不是延迟。看到某个容器Block I/O很大,只能说明它读写多,不能说明它被限速。

更准确的方式是结合宿主机工具看进程级IO:

pidstat -d 1

iotop -oPa

ps -ef | grep 业务进程名

如果某个容器内的MySQL进程对应宿主机上的PID持续写入很高,同时iostat里磁盘await也很高,那说明应用确实在制造IO压力。反过来,如果进程写入并不高,但await很高,就要怀疑云盘性能上限、突发额度耗尽或者底层存储抖动。

检查Docker有没有配置过blkio限制

Docker本身支持对容器做磁盘IO限制,比如--device-read-bps、--device-write-bps、--device-read-iops、--device-write-iops。如果容器是别人部署的,或者通过脚本、面板、CI/CD发布的,真有可能被带上了限制参数。

可以先看容器配置:

docker inspect 容器ID

重点看HostConfig里的BlkioWeight、BlkioDeviceReadBps、BlkioDeviceWriteBps、BlkioDeviceReadIOps、BlkioDeviceWriteIOps。

如果看到类似下面这种配置,就说明容器确实被人为限速了:

"BlkioDeviceWriteBps": [{"Path":"/dev/vda","Rate":10485760}]

这个意思是写入限制在10MB/s左右。数据库、对象存储、日志系统跑在这种限制下,很容易出现业务层面的慢请求。

Docker Compose也要看,尤其是老项目里可能有人写过blkio_config。Kubernetes环境则要看RuntimeClass、QoS、节点侧cgroup配置以及存储插件策略,不过普通Docker单机环境主要还是docker inspect最直接。

cgroup v1和v2路径不一样,别查错位置

很多排查文档写的是cgroup v1路径,但现在不少新系统已经默认cgroup v2。路径查错了,会误以为没有限制。

查看当前系统使用哪种cgroup:

stat -fc %T /sys/fs/cgroup

如果输出cgroup2fs,说明是cgroup v2;如果是tmpfs,大概率是cgroup v1。

cgroup v1常见位置

可以查看类似路径:

/sys/fs/cgroup/blkio/docker/容器ID/blkio.throttle.io_service_bytes

/sys/fs/cgroup/blkio/docker/容器ID/blkio.throttle.read_bps_device

/sys/fs/cgroup/blkio/docker/容器ID/blkio.throttle.write_bps_device

如果write_bps_device里有设备号和速率,说明写入被限制。

cgroup v2常见位置

systemd管理的Docker容器常见路径类似:

/sys/fs/cgroup/system.slice/docker-容器ID.scope/io.max

/sys/fs/cgroup/system.slice/docker-容器ID.scope/io.stat

io.max里如果不是default,或者出现rbps、wbps、riops、wiops,就要注意。比如wbps=10485760就是写入带宽被压到10MB/s。

云服务器磁盘规格经常才是关键

云服务器磁盘IO不是无限的。很多云厂商会把磁盘性能和容量、套餐、云盘类型绑定。常见限制包括IOPS上限、吞吐上限、突发积分、队列深度、快照期间性能波动。

举个实际场景:一台2C2G小规格云服务器,挂20GB SSD系统盘,容器里跑MySQL,再加一个日志量比较大的Java服务。平时看着没问题,一到业务高峰,MySQL刷脏页、binlog落盘、应用日志写入叠在一起,磁盘await从3ms涨到80ms,接口就开始抖。

这时候Docker只是承载环境,不是根因。就像小区门口只有一个出口,外卖车、私家车、快递车都从这里走,堵车不能怪车轮。

常见云盘性能档位可以这样理解

低配系统盘:适合系统、轻量服务、少量日志,不适合高频数据库写入。

普通SSD云盘:能跑中小型业务,但要关注IOPS和吞吐峰值,尤其是随机写。

高性能SSD或NVMe本地盘:适合数据库、缓存持久化、日志分析、构建服务这类IO敏感场景。

这里多说一句,购买云服务器时不要只看CPU和内存。Docker部署让服务迁移更方便,但不会让低IO磁盘变成高IO磁盘。如果业务有数据库、游戏服存档、日志采集、文件转码缓存,磁盘参数要提前问清楚。

如果你也在找这种适合容器业务、海外访问、游戏或企业场景的云服务器,可以看看129云。比如香港综合-C型是8C、8G DDR4 ECC、100G SSD,线路是CN2+CMI+CU,适合需要稳定回国链路的业务;德国双ISP-F型是8C、16G DDR4 ECC、130GB SSD、1Gbps带宽,适合欧洲节点和双ISP场景;日本活动款是2C、2G、20GB SSD、软银直连,适合轻量服务和测试环境。需要确认磁盘IO、线路和防护策略,可以直接打客服热线400-9177118。

fio测试要分清顺序写、随机写和fsync

很多人排查磁盘慢,会在容器里dd一个大文件:

dd if=/dev/zero of=test.img bs=1M count=1024 oflag=direct

这个命令只能粗略看顺序写吞吐,不能代表数据库性能。数据库更怕随机写、fsync延迟和小块IO。

更建议用fio做几组测试。宿主机跑一遍,容器里跑一遍,同一个目录跑一遍,不同目录再跑一遍。

顺序写吞吐:

fio --name=seqwrite --filename=/data/fio.test --rw=write --bs=1M --size=2G --iodepth=16 --direct=1 --numjobs=1 --runtime=60 --time_based --group_reporting

随机写IOPS:

fio --name=randwrite --filename=/data/fio.test --rw=randwrite --bs=4k --size=2G --iodepth=32 --direct=1 --numjobs=4 --runtime=60 --time_based --group_reporting

数据库类fsync压力:

fio --name=syncwrite --filename=/data/fio.test --rw=write --bs=16k --size=1G --iodepth=1 --direct=1 --fdatasync=1 --runtime=60 --time_based --group_reporting

看结果时不要只看带宽。clat、lat、99th、99.9th延迟很重要。平均延迟5ms,但99.9th到500ms,数据库一样会抖。

宿主机快,容器慢,要看overlay2和挂载方式

如果宿主机直接写/data很快,但容器内写入很慢,就要看容器的数据是不是写在overlay2层里。

Docker的overlay2适合镜像层、应用文件、少量临时写入,不建议把数据库主数据目录长期放在容器可写层。原因是overlay文件系统有额外开销,文件修改、copy-up、元数据操作会更复杂。

数据库、消息队列、搜索引擎这类服务建议使用volume或者bind mount,把数据目录明确挂到宿主机磁盘目录上。例如:

docker run -v /data/mysql:/var/lib/mysql mysql:8

这里/data/mysql应该位于性能更好的数据盘,而不是随手放在系统盘剩余空间里。

可以通过docker inspect看Mounts字段,确认数据目录到底怎么挂载。如果Mounts为空,或者业务数据就在容器层里,那后面迁移、备份、性能都会比较难受。

日志写入经常被忽略

容器磁盘IO异常时,日志是高频问题源。尤其是stdout日志量很大的服务,Docker默认json-file驱动会把日志写到宿主机/var/lib/docker/containers/容器ID/目录下。

可以检查日志文件大小:

du -sh /var/lib/docker/containers/*/*-json.log

如果单个容器日志几十GB,磁盘IO和磁盘空间都会受影响。更麻烦的是,日志持续写入时会和数据库争抢同一块系统盘。

建议至少配置日志轮转:

docker run --log-driver=json-file --log-opt max-size=100m --log-opt max-file=5 ...

daemon.json里也可以全局配置:

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

修改后需要重启Docker,新配置只对新创建容器生效。已经跑着的容器通常要重建才能吃到配置。

看到IO被限速,要判断是人为限制还是云盘上限

排查到这里,一般会出现两类结果。

一类是容器级限制。docker inspect或cgroup里能看到明确的rbps、wbps、riops、wiops。这种处理比较直接,调整Docker启动参数、Compose配置或上层编排配置。

另一类是没有容器限制,但整机磁盘指标撞上云盘上限。fio测试宿主机和容器都慢,iostat里await高,吞吐或IOPS稳定卡在某个数值附近,比如写入长期卡在30MB/s、随机写长期只有几百IOPS。这种更像云盘规格限制。

实际使用中还有一种情况:刚开始测试很快,跑几分钟后明显下降。这通常要怀疑突发积分或缓存耗尽。部分云盘会给短时间burst能力,持续写入后回落到基础性能。

几个常见判断信号

写入速度稳定卡在固定值,比如10MB/s、20MB/s、50MB/s,优先查cgroup和云盘规格。

await高但吞吐不高,随机IO或fsync压力可能太大,也可能底层存储拥塞。

容器慢、宿主机同目录也慢,Docker嫌疑下降。

容器写overlay2慢、bind mount快,调整数据目录挂载方式。

白天慢、夜间快,而且没有业务量变化,要看宿主机资源争用、云平台维护、快照、备份任务。

别忘了检查系统层面的异常

磁盘慢不一定都是限速,也可能是系统层面有错误。建议看一下内核日志:

dmesg -T | egrep -i "error|fail|timeout|reset|blk|nvme|virtio|scsi"

如果看到大量I/O error、reset、timeout,就不是普通性能不足那么简单了。云服务器上虽然看不到物理盘健康状态,但虚拟磁盘链路异常、宿主机迁移、存储后端抖动都有可能在dmesg里留下痕迹。

还要看是否有定时任务在抢IO,比如备份、压缩、日志切割、镜像构建:

systemctl list-timers

crontab -l

ls /etc/cron.*

有些线上机器每天凌晨看着很空,但备份脚本tar、gzip、rsync一起跑,容器里的数据库就会在同一时间出现延迟尖刺。

数据库容器尤其要看刷盘策略

MySQL、PostgreSQL这类服务对磁盘延迟很敏感。容器部署时,如果磁盘本来一般,再开强一致刷盘,性能会很快碰到上限。

MySQL里常见参数包括innodb_flush_log_at_trx_commit、sync_binlog、innodb_io_capacity、innodb_flush_method。不能为了性能随便改成不安全配置,但排查时要知道当前刷盘压力来自哪里。

例如innodb_flush_log_at_trx_commit=1加sync_binlog=1,每次事务提交都更依赖磁盘fsync延迟。云盘99线延迟一高,应用就会明显变慢。

PostgreSQL则要关注checkpoint、wal_sync_method、synchronous_commit、bgwriter相关指标。Elasticsearch要看merge、translog、segment写入压力。不同软件表现不一样,但共同点是:小块随机写和fsync延迟比顺序吞吐更关键。

处理方向要按证据来

如果确认是Docker参数限制,直接改启动参数或Compose配置,重建容器。注意容器重建前先处理好数据卷和备份,不要把业务数据误删。

如果是日志打满IO,先加日志轮转,再把高频日志接到日志系统,减少本机json-file持续写入。短期可以清理巨大日志,但不要直接rm正在被Docker占用的日志文件,推荐truncate -s 0 文件路径,或者停容器后处理。

如果是overlay2带来的性能问题,把数据库数据目录迁到volume或bind mount。迁移前停服务、校验数据、保留回滚目录。

如果是云盘规格不够,继续调Docker意义不大。该升配磁盘就升配,该拆数据盘就拆数据盘,数据库和日志不要跟系统盘挤在一起。对于跨境业务,还要同时看线路质量,比如CN2、CMI、CU、GTT、软银直连这些会影响访问体验,但磁盘IO问题仍然要从本机存储指标判断,别把网络延迟和磁盘延迟混在一起。

一次线上排查可以按这个顺序走

查看业务报错时间点,确认是写入慢、查询慢,还是整体系统卡顿。

宿主机执行iostat -x 1,看磁盘await、%util和吞吐是否异常。

执行pidstat -d 1和iotop -oPa,找出制造IO的进程,再映射到容器。

docker inspect容器,检查Blkio相关字段和Mounts。

确认cgroup版本,查看io.max或blkio.throttle配置。

宿主机和容器内分别用fio测试同一个挂载目录,对比顺序写、随机写、fsync延迟。

检查/var/lib/docker/containers下日志文件大小,确认是否json-file日志过大。

查看dmesg和定时任务,排除磁盘错误、备份、压缩、快照任务干扰。

根据证据处理:取消容器限制、调整挂载、拆分数据盘、升级云盘规格、迁移到IO更稳的实例。

排查时容易踩的坑

只在容器里跑dd,然后说Docker慢。dd顺序写不能代表数据库随机写,也不能说明cgroup没有限制。

看到CPU低就认为机器没压力。IO等待时CPU可能很空,业务照样慢。

把数据放在容器可写层,后面再排查性能和迁移都会麻烦。

日志不做轮转,json-file写到几十GB,系统盘空间和IO一起出问题。

云服务器买得很小,却在上面堆数据库、日志、队列、对象缓存,还期待SSD表现跟独立NVMe一样。

只看平均延迟,不看99线和99.9线。线上请求超时通常不是平均值造成的,而是尾部延迟造成的。

最后留一个现场命令组合

问题正在发生时,可以直接开三个窗口同时看:

窗口一:iostat -x 1

窗口二:pidstat -d 1

窗口三:iotop -oPa

再补一条容器映射:

docker inspect --format '{{.State.Pid}} {{.Name}}' 容器ID

拿到容器主PID后,结合pstree -p PID看里面实际进程。确认进程、磁盘设备、挂载目录、cgroup限制这几项对上,再决定是改Docker参数、迁数据目录,还是直接升级云服务器磁盘规格。