容器给人的直观印象是“轻量级虚拟化”,但它的本质是运行在同一个 Linux 内核之上的隔离进程组。为了让成千上万个容器和平共处,宿主机必须对每个容器所能使用的 CPU、内存、磁盘 I/O、进程数等资源进行精细限制。这些限制的核心实现,正来自 Linux 内核的两大机制:控制组(cgroup) 和 命名空间(namespace)。而内核参数(sysctl)则决定了容器内外的网络行为、内存管理策略等一系列关键行为。理解这些底层的钩子,是容器稳定性和性能调优不可或缺的一环。
1. cgroup:限制容器可以看到的资源上限
cgroup(control group)将进程分组,并对每个组施加资源配额。Docker、Podman、containerd 等容器引擎在启动容器时,实际上就是在为容器对应的 cgroup 树节点填写数值。cgroup 目前有两个版本,v1 使用一系列独立的子系统(如 cpu、memory、blkio),v2 则统一为一棵层次化的树。多数现代发行版(RHEL 8+、Ubuntu 22.04+)逐渐转向 cgroup v2,但容器引擎通常都兼容两种模式。
CPU 限制
- 权重式共享:
--cpu-shares(cgroup v1 的cpu.shares,v2 的cpu.weight)。这个值只在实际发生 CPU 争抢时生效,数字越大的容器获得的 CPU 时间比例越多。它不是绝对量,把容器 A 设为 1024,B 设为 512,在 CPU 满载时 A 占用大约是 B 的两倍。 - 绝对配额:
--cpus(如--cpus=1.5)按 CFS 调度器的周期和配额实现,会将cpu.cfs_quota_us和cpu.cfs_period_us设置为相应值。例如 1.5 核意味着每 100ms 周期内可使用 150ms 的 CPU 时间。 - 核绑定:
--cpuset-cpus=0-3直接限制容器只能运行在特定的 CPU 上,对于缓存敏感或 NUMA 优化的应用很有用。
内存限制
--memory(cgroup 的memory.limit_in_bytes)是容器能使用的物理内存上限。当容器内存超限时,可能会触发 OOM Killer 杀掉容器内进程,而不是整个宿主机崩溃。--memory-swap控制内存+Swap 的总量,通常设为--memory的两倍或与--memory相同以禁用 Swap。对于数据库这类希望避免 Swap 抖动的应用,可以设--memory-swap等于--memory。--oom-kill-disable关闭容器自身的 OOM Killer,但若需要,结合--oom-score-adj调整本容器的 OOM 得分,避免优先被杀。
其他资源
- IO 权重:
--blkio-weight(cgroup v1)或--io-weight(cgroup v2)限制磁盘读写比例。 - 进程数:
--pids-limit对应 cgroup 的pids.max,防止某个容器 fork 出大量进程耗尽宿主机 PID 资源。 - 设备访问:
--device可为容器开放特定的宿主机设备,底层由 cgroup 的设备白名单实现。
所有这些限制,你可以直接在宿主机上通过 /sys/fs/cgroup/ 路径查看(v1)或统一挂载点查看(v2)。例如,用 systemd-cgls 可以直观看到每个容器的 cgroup 树,或者 cat /sys/fs/cgroup/memory/docker/<container-id>/memory.limit_in_bytes 来确认限制是否生效。
2. 内核参数与容器的交互
内核参数(/proc/sys/ 下的可调参数)控制着网络协议栈、内存回收、文件句柄等全局行为。在容器中直接运行 sysctl -w 往往会出现“只读文件系统”或“无权限”的错误,因为默认下容器的网络、IPC、UTS 等命名空间并没有赋予写入 /proc/sys 的权限。你需要通过下面的方式之一来调整:
方法一:容器启动时通过 --sysctl 指定
Docker 允许传入一个安全列表内的 sysctl,这些参数是在命名空间隔离的前提下允许设置的:
docker run --sysctl net.core.somaxconn=4096 -d nginx
适用参数包括 net.core.somaxconn、net.ipv4.tcp_tw_reuse、net.ipv4.ip_local_port_range 等网络相关项,以及部分 kernel. 参数(如 kernel.hostname)。但对于 vm.、fs.* 等与内存、文件系统全局状态密切相关的参数,--sysctl 无效,需要从宿主机层面配置。
方法二:使用特权容器(不推荐)
加上 --privileged 后,容器几乎拥有宿主机的全部能力,可以直接修改大部分 sysctl。但这会严重削弱容器的安全隔离,生产环境应尽量避免。
方法三:调整宿主机全局参数
有些内核参数直接影响所有容器,比如 vm.max_map_count(Elasticsearch 需要大量内存映射区域)、fs.inotify.max_user_watches(IDE 或文件同步工具需要大量文件监控)。这些参数通常是整个系统共享的,需要在宿主机上执行 sysctl -w 或写入 /etc/sysctl.conf。所有容器都会继承这些设置,无法按容器做精细隔离。
3. 生产实践中必须关注的几个参数
网络相关
net.core.somaxconn:TCP 监听队列的全连接队列最大值。Nginx、HAProxy 等反向代理在高并发场景下需要调大,默认 128 经常不够。容器用--sysctl指定,或宿主机调高后容器继承。net.ipv4.tcp_tw_reuse:允许将处于 TIME_WAIT 状态的连接用于新的连接,减少连接耗尽。在有大量短连接的后端服务中非常有用。net.ipv4.ip_local_port_range:可用临时端口范围,默认 32768-60999,若不够可扩大为 1024-65535。
内存与存储
vm.swappiness:倾向使用 Swap 的程度。对于不希望使用 Swap 的 Redis、Elasticsearch 等,可在宿主机设为 1 或 0(值越低越不倾向换出)。注意 cgroup v2 可设置容器级别的memory.swap.max和memory.swappiness,更精细。vm.max_map_count:进程可拥有的内存映射区域数,Elasticsearch 推荐至少 262144,在某些发行版默认 65530 可能导致启动失败。需要宿主机层面设置后重启容器。
文件监控与资源上限
fs.inotify.max_user_watches:单个用户可创建的 inotify 监控数量。如果容器内运行 Tail、Webpack、Syncthing 等需要监听大量文件的工具,默认值(通常 8192)可能瞬间耗尽,表现为 "No space left on device" 或无法监控新文件。应在宿主机调大。kernel.pid_max:系统允许的最大 PID 号,决定整个机器可容纳的进程总数。高密度容器场景下常有成百上千的容器和进程,需确保足够(例如设为 4194304)。
4. 查看和调试
docker stats:实时显示每个容器的 CPU、内存、网络 I/O、磁盘 I/O 的用量和限额百分比,最直观的“血液检验”。- 直接查看 cgroup 文件:进入
/sys/fs/cgroup/system.slice/docker-<id>.scope/(systemd 管理下)查看cpu.max、memory.current、memory.high等文件,可以精确获知容器当前的资源消耗和瓶颈。 systemd-cgtop:类似top,但按 cgroup 分组显示 CPU、内存、I/O 任务,适合排障时快速找出哪个容器在耗资源。
最后,记住一个核心原则:任何不加限制的容器都是潜在的“邻居杀手”。合理配置资源限制不是“可选的优化”,而是保障容器平台可用性的底线。同理,盲目地复制粘贴网络调优参数可能会引入新的问题,应该以可观测数据(如丢包率、重传率、OOM 日志)作为调整依据。掌握了 cgroup 和内核参数的交互逻辑,你就不再只是在 GUI 或命令上操作容器,而是在真正与 Linux 的内核能力协同工作。