努力
加油

运行多个Docker容器时需要注意哪些资源限制?

跑多个 Docker 容器,最怕的就是“邻居噪音”效应。你以为你给每个容器分配了资源,结果因为底层机制没搞懂,或者配置不当,导致整个宿主机 CPU 飙高、内存 OOM(Out of Memory),甚至把宿主机的网络或磁盘 IO 拖垮。

作为在云上摸爬滚打多年的运维和开发,我总结了几条必须死守的资源限制红线,按优先级从高到低排列:

1. 内存限制(Memory & Swap):这是最致命的

很多新手只设 --memory,不设 --memory-swap,这是大坑。

  • 现象:某个 Java 应用或 Python 脚本出现内存泄漏,如果不限制 swap,它会无限占用宿主机物理内存,直到触发内核的 OOM Killer,直接杀掉进程。更惨的是,如果其他关键服务也被连带杀死,整个业务就挂了。
  • 最佳实践:
    • 必须同时设置 memory 和 memory-swap。通常建议将 memory-swap 设置为与 memory 相同,或者略大一点(例如 memory=512m, memory-swap=512m)。这样容器使用的总内存(RSS + Swap)就被锁死了,不会挤占宿主机的其他资源。
    • 预留宿主机内存:不要把所有物理内存都分给容器。Docker daemon 本身、containerd、以及宿主机 OS 都需要内存。一般建议保留 10%-15% 的物理内存给宿主机系统。
    • 监控 RSS vs Cache:Linux 的 Page Cache 是可以回收的。如果你看到容器内存使用量很高,但大部分是 Cache,其实不用太紧张,内核会自动回收。但如果 RSS(常驻集)很高,那才是真问题。

2. CPU 限制(CPU Shares, Quotas, and Periods)

Docker 的 CPU 限制不是硬隔离,而是软限制(除非你用 cgroups v2 并配合严格的 cpuset)。

  • 误区:认为 --cpus 1 就是独占一个核心。错!它只是保证在竞争时,该容器能获得大约 1 个核心的份额。如果宿主机空闲,它可以利用所有算力;如果宿主机满载,它会被限流。
  • 关键点:
    • CPU Shares (权重):默认值是 1024。如果你有两个容器,A 设为 2048,B 设为 1024,那么在 CPU 竞争时,A 能拿到 B 两倍的算力。这适合非实时性任务。
    • CPU Quotas (硬上限):--cpus 本质上就是设置了 quota/period。比如 --cpus 2 意味着每 100ms 周期内,最多允许运行 200ms 的 CPU 时间。这对于防止单个容器吃光 CPU 非常有效。
    • CPU Set (硬隔离):如果你需要绝对隔离(比如数据库和 Web 服务不能互相干扰),使用 --cpuset-cpus="0-1" 将容器绑定到特定的物理核心。这需要你对 CPU 拓扑结构有了解,否则可能导致缓存命中率下降,性能反而变差。

3. 磁盘 I/O:被忽视的性能杀手

大多数人对 CPU 和内存敏感,但对 Disk I/O 毫无概念。

  • 问题:日志写入、数据库读写、大量小文件操作会迅速耗尽磁盘 IOPS 或 Bandwidth。一旦磁盘队列深度爆满,所有容器的响应时间都会飙升。
  • 解决方案:
    • cgroup blkio 限制:可以使用 --device-read-bps, --device-write-bps 限制每秒字节数,或用 --device-read-iops, --device-write-iops 限制每秒 IO 次数。
    • 区分日志路径:千万不要让容器日志直接写回宿主机的 /var/lib/docker/containers 目录并长期不清理。这会撑爆你的根分区。务必配置 log-driver 为 json-file 并设置 max-size 和 max-file,或者使用 fluentd/loki 等外部日志收集方案。
    • SSD vs HDD:如果可能,将数据盘和日志盘分开。日志盘可以用普通 SSD,数据盘用高性能 NVMe。

4. 网络资源:带宽与连接数

  • 带宽限制:Docker 默认不限制容器出口带宽。如果一个容器在进行大文件下载或上传,可能会占满网卡带宽,导致其他容器无法访问网络或内部服务延迟增加。
    • 解决:使用 tc (traffic control) 工具在宿主机层面进行限速,或者在 Kubernetes 中使用 NetworkPolicy 和 Ingress 控制器进行精细控制。单机 Docker 环境下,手动加 tc 规则比较麻烦,但值得做。
  • 端口冲突:确保不同容器绑定的宿主机端口不冲突。虽然这不是“资源消耗”,但是常见的部署失败原因。建议使用动态端口映射,或在编排工具中管理端口。

5. 文件系统层叠与存储驱动

  • OverlayFS 开销:Docker 默认使用 OverlayFS。每一层镜像都是一个只读层,容器层是可写的。当容器频繁创建/删除临时文件时,会导致元数据操作增多,影响性能。
  • volumes vs bind mounts:
    • 生产环境尽量用 named volumes。它们由 Docker 管理,备份、迁移方便,且在某些存储驱动下性能更好。
    • 避免过度使用 bind mount:尤其是挂载宿主机的大目录到容器内,可能会暴露敏感信息,且权限管理复杂。

6. 实际部署建议(结合国内云厂商)

如果你是在阿里云 ECS、腾讯云 CVM 或 AWS EC2 上跑 Docker:

  1. 使用 ECS 实例规格:选择支持超线程和 NUMA 架构优化的实例类型,并在启动容器时通过 --cpuset 绑定到同一 NUMA 节点,减少跨节点访问延迟。
  2. 利用云监控:开启云监控 Agent,不仅监控容器内的指标,更要监控宿主机的整体负载。设置告警阈值,比如宿主机 CPU > 80% 持续 5 分钟就告警。
  3. 考虑使用 K8s 或 Serverless 容器服务:如果容器数量超过几十个,手动管理 docker run 的参数几乎是不可能的任务。Kubernetes 的资源配额(Resource Quotas)、LimitRanges 和 Pod 优先级调度能更好地处理多租户场景下的资源争抢。
  4. 定期清理:执行 docker system prune -a 清理无用镜像、容器和网络卷。特别是那些构建失败的中间镜像,会占用大量磁盘空间。

总结 checklist:

资源类型 推荐限制方式 注意事项
内存 --memory, --memory-swap 两者设为相等,预留宿主机内存
CPU --cpus 或 --cpu-quota 根据业务类型选择软限制或硬上限
磁盘 I/O --device-read-bps, --device-write-bps 分离日志和数据盘,限制日志大小
网络 tc 限速, 端口规划 防止单容器占满带宽
文件系统 Named Volumes 避免 bind mount 敏感目录

最后记住一条原则:永远不要信任用户的代码不会出错。 资源限制的目的不是为了让程序跑得更快,而是为了防止一个坏掉的容器把整个系统拖垮。这就是“故障隔离”的核心思想。