3. CPU进阶:CPU份额(--cpu-shares)与CPU绑定(--cpuset-cpus)
好,咱们接着聊CPU。上一节我们讲了CPU限制的硬指标,比如--cpus和--cpu-quota。这一节,我们来聊聊两个更“高级”的玩法:CPU份额和CPU绑定。
说实话,这两个参数在实际生产环境中,用好了是神器,用不好就是给自己挖坑。我刚开始接触Docker时,就吃过--cpu-shares的亏,以为设了就能保证性能,结果线上服务还是被邻居容器拖垮了。嗯,咱们今天就把这两个参数彻底讲透。
3.1 CPU份额(--cpu-shares):不是“分蛋糕”,是“抢蛋糕”
很多人第一次看到--cpu-shares,会以为它是“给容器分配多少CPU”。其实不是的。它更像是一个权重,决定了当CPU资源紧张时,容器之间怎么“抢”CPU。
说白了,--cpu-shares只在CPU竞争时生效。如果宿主机CPU空闲,你设成1024还是2048,效果一样——都能跑满。但一旦CPU吃紧,这个值就决定了谁能抢到更多时间片。
默认值是多少?
默认是1024。你可以把它理解成“基准线”。
- 容器A:
--cpu-shares=1024 - 容器B:
--cpu-shares=2048
当CPU满负荷时,容器A和B会按1:2的比例分配CPU时间。注意,这不是说容器B能拿到2个核,而是说在同等竞争下,它拿到的CPU时间是A的两倍。
核心理解:CPU份额是相对值,不是绝对值。它只在资源竞争时生效。
实战示例
我们跑两个容器来验证一下:
# 容器A:份额1024
docker run -d --name stress-a --cpu-shares=1024 stress-ng --cpu 2 --timeout 60s
# 容器B:份额2048
docker run -d --name stress-b --cpu-shares=2048 stress-ng --cpu 2 --timeout 60s
用docker stats观察,你会发现容器B的CPU使用率大约是容器A的两倍。但注意,如果宿主机只有2个核,两个容器各占2个线程,那它们加起来可能超过200%?不会的,内核会按比例分配。
我的经验:我个人习惯把核心业务容器的--cpu-shares设成2048或4096,把一些批处理任务、日志采集等非关键容器设成512或256。这样即使CPU打满,核心服务也不会被饿死。
避坑指南
我曾经踩过的坑:有一次我把所有容器都设成了--cpu-shares=4096,以为这样每个容器都能拿到更多CPU。结果呢?所有容器权重一样,等于没设。而且因为数值太大,反而让调度器多了一些无意义的计算开销。记住,--cpu-shares是相对值,大家一样高就等于大家一样低。
3.2 CPU绑定(--cpuset-cpus):把容器“钉”在指定核上
如果说--cpu-shares是“软限制”,那--cpuset-cpus就是“硬绑定”。它直接把容器进程绑定到指定的CPU核心上,不让它跑到别的核上去。
为什么要这么做?
- 减少上下文切换:进程固定在一个核上,CPU缓存命中率更高。
- 隔离敏感业务:比如实时性要求高的服务,不希望被其他容器干扰。
- NUMA优化:在多路服务器上,绑定到同一NUMA节点的核,可以避免跨节点内存访问延迟。
基本用法
# 绑定到0号核
docker run -d --cpuset-cpus="0" nginx
# 绑定到0,1,2号核
docker run -d --cpuset-cpus="0-2" nginx
# 绑定到0,3号核(跳过1,2)
docker run -d --cpuset-cpus="0,3" nginx
注意,CPU编号从0开始。你可以用lscpu查看宿主机有多少个核。
什么时候该用?
我个人建议,不是所有场景都适合CPU绑定。比如:
- 适合:数据库、缓存服务、实时音视频处理、DPDK应用。
- 不适合:普通的Web服务、微服务、短生命周期任务。
为什么?因为CPU绑定会限制调度器的灵活性。如果绑定的核被占满,而其他核空闲,容器也只能排队等着。你想想看,这多浪费资源。
一个真实案例:我在项目中遇到过,一个Redis容器绑定了4个核,但业务高峰期流量暴增,这4个核全部打满,而宿主机其他12个核却在“看热闹”。最后我们不得不去掉绑定,让调度器自由分配,问题才解决。
CPU绑定 + CPU份额 组合使用
这两个参数可以一起用。比如:
docker run -d --cpuset-cpus="0-3" --cpu-shares=2048 myapp
这样容器只能使用0-3号核,但在这4个核上,它拥有更高的竞争权重。
3.3 实战对比:什么时候选哪个?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 多容器共享宿主机,CPU偶尔打满 | --cpu-shares |
保证关键服务在竞争时获得更多资源 |
| 延迟敏感型服务(如Redis、Nginx) | --cpuset-cpus |
减少缓存抖动和上下文切换 |
| CPU密集型批处理任务 | --cpuset-cpus + --cpu-shares |
固定核数,同时保证在核内竞争时不吃亏 |
| 普通微服务,资源充足 | 默认值即可 | 过度优化反而增加运维复杂度 |
3.4 小结与建议
好了,我们来捋一捋:
- CPU份额是“软性”的,只在竞争时生效。适合做服务质量保障。
- CPU绑定是“硬性”的,直接锁定核心。适合做性能极致优化。
我个人建议,先不加绑定,用--cpu-shares做分层。如果监控发现延迟抖动严重,再考虑用--cpuset-cpus做精细化绑定。别一上来就绑死,给自己留点弹性空间。
一个小技巧:如果你用--cpuset-cpus,记得配合taskset命令检查一下进程实际跑在哪个核上:taskset -p <PID>。我经常用这招来验证绑定是否生效。
下一节,我们会聊到内存限制和Swap的坑。嗯,那个话题更有意思——我见过太多因为Swap没配好导致服务雪崩的案例了。到时候咱们细说。