朕喜技术(ZhenxiJS)
2026.09.23
1 分钟阅读

Docker 容器里读 cgroup CPU 配额读到了外层:服务上报 8 核、实际只有 0.9 核,自检全程绿灯

日期:2026-09-23 | 环境:Docker 29.6.2 / Docker Compose v5.3.1 / cgroup v1 / Linux 5.4 / Python 3.11 关键词:cpu.cfs_quota_us、cpu.max、/proc/self/cgroup、sched_getaffinity、cpuset.cpus、cpuacct.usage、getrusage(RUSAGE_SELF)、cgroup namespace、docker --cpus 限制不生效、容器 CPU 核数读取错误、容量规划上报错误、自检恒真、NameError plan_self_check、deviation 695%

一、问题背景

calcpool 是一个跑在 Docker 容器里的计算节点服务,它做两件事:

  1. 探测「本容器能用几个 CPU 核」,用它做容量规划(决定分片数 cpu_shards);
  2. 把这个值当 cpu_limit 上报给上游容量调度器。调度器完全依据它决策 —— 上报 8 核就往这个节点派 8 核的活。

docker-compose.yml 里部署方给这个服务限了 cpus: "1.0"(一个核)。结果:

  • 启动日志和 /stats 都写着 cpu_limit = 8.0、cpu_shards = 16;
  • 服务自检 plan_is_self_consistent 返回 True,认为一切正常;
  • 但容器真实并行度只有 ≈ 1.0 核,偏差 695%。

调度器按 8 核派活 → 节点被压得动不了;而服务的自检、日志、健康检查全是绿的。这是一个典型的**「观测口径错了 + 自检恒真」双重失效**:读数错了没人发现,因为自检根本不校验「读数是否代表自己」。

二、复现步骤

git checkout 2026-09-23-1600
cd scenario/2026-09-23-1600
docker compose up -d --build
sleep 4
docker compose logs app | tail -8

问题现象(上报 8 核,自检绿):

3day-calcpool  | [start] calcpool v1 环境=prod pid=1
3day-calcpool  | [plan] cpu_limit=8.0 (来源 cgroup-v1:cpu.cfs_quota_us)   ← 上报 8 核
3day-calcpool  | [plan] cpu_shards=16 每分片 0.5 核
3day-calcpool  | [plan] self_consistent=True                              ← 自检绿
3day-calcpool  | [stats] affinity=8 cpuset=8 cpuinfo=8
3day-calcpool  | calcpool ready

核心判定(容器内跑):

docker compose exec app python3 /client/check_capacity.py; echo "RC=$?"
服务上报 cpu_limit  : 8.0
服务上报 cpu_shards : 16
读数来源            : cgroup-v1:cpu.cfs_quota_us
自检自洽            : True

实测窗口            : 3.00s
窗口内 CPU 秒       : 3.020
实测真实可核数      : 1.007
偏差                : 6.993 核 (695%)

判定: BAD —— 上报的节点算力与真实能拿到的算力不符
  · 上报 cpu_limit=8.0,真实只能并行 1.01 核,偏差 695%(要求 ≤ 40%)
RC=1

完整判定脚本基线:

bash ops/verify.sh      # PASS=17 FAIL=4
[FAIL] A1 上报核数 8.0000 超过真实可用 1.0057
[FAIL] A2 读数 8.0 明显大于实测真实可核数 1.0057(读的是父/宿主那层)
[FAIL] C1 check_capacity.py 判定不是 OK
[FAIL] C2 plan_check.py 判定不是 OK

三、根本原因

3.1 直接原因:只读固定路径,没有定位「自己那一层」

app/cpu_capacity.py 的 read_cpu_limit() 只读固定路径:

CGROUP_V1_CPU = '/sys/fs/cgroup/cpu'
...
quota_s = _read(os.path.join(CGROUP_V1_CPU, 'cpu.cfs_quota_us'))   # ← 固定路径

它没有按 /proc/self/cgroup 里的相对路径去找「本容器自己那一层」。证据链:

$ docker compose exec app cat /proc/self/cgroup | grep ':cpu'
4:cpu,cpuacct:/cnb-arg-1k36ken0h-001/bc348e5d5af894c2512586d90aff3ae9e4c389550765832befc39c4810ab7bf2

$ docker compose exec app cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us
800000                    # 800000/100000 = 8.0 核 ← 读到的「配额」
$ docker compose exec app cat /sys/fs/cgroup/cpu/cpu.cfs_period_us
100000

# 拿相对路径去找自己那一层 —— 不可达!
$ docker compose exec app sh -c 'ls -d /sys/fs/cgroup/cpu/cnb-arg-1k36ken0h-001/bc348e…b7bf2'
ls: cannot access '...': No such file or directory

/proc/self/mountinfo 说明这份挂载的视角:

$ docker compose exec app grep cgroup /proc/self/mountinfo | grep 'cpu,cpuacct'
2815 2811 0:33 /cnb-arg-1k36ken0h-001/bc348e…b7bf2 /sys/fs/cgroup/cpu,cpuacct \
     ro,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu,cpuacct
#            ^^^^^^^^ 挂载源,被 remap 成了 /sys/fs/cgroup/cpu,cpuacct

**/sys/fs/cgroup/<ctrl>/ 里的 quota 值不代表本容器的有效配额。**实测一量就露馅:

$ docker compose exec app python3 - <<'PY'
import resource, threading, time
def burn(stop):
    s=0.0
    while time.time()<stop:
        for _ in range(2000): s+=1.5
best=0
for _ in range(3):
    r0=resource.getrusage(resource.RUSAGE_SELF); t0=time.time(); W=1.0
    ts=[threading.Thread(target=burn,args=(t0+W,)) for _ in range(8)]
    [x.start() for x in ts]; [x.join() for x in ts]
    w=time.time()-t0; r1=resource.getrusage(resource.RUSAGE_SELF)
    best=max(best, ((r1.ru_utime-r0.ru_utime)+(r1.ru_stime-r0.ru_stime))/w)
print("quota file -> 8.000 cores | measured -> %.3f cores" % best)
PY
quota file -> 8.000 cores | measured -> 1.011 cores

cpu.stat 里有 throttle 物证,证明配额确实在生效、只是不在那个文件里:

nr_periods 36555
nr_throttled 9954
throttled_time 7610261046

3.2 三条「看起来能读核数」的口径全是错的

$ docker compose exec app python3 -c "import os;print(len(os.sched_getaffinity(0)), os.cpu_count())"
8 8
$ docker compose exec app cat /sys/fs/cgroup/cpuset/cpuset.cpus
110-117                   # 「允许跑在哪些 CPU 上」,不是配额
$ docker compose exec app python3 -c "print(sum(1 for l in open('/proc/cpuinfo') if l.startswith('processor')))"
8                         # 整机核数
读法量的是什么为什么不能当 CPU 配额
cpu.cfs_quota_us(固定路径)挂载者那一层的配额挂载点视角 ≠ 本容器
sched_getaffinity()调度器亲和性(被允许跑在哪些 CPU)--cpus 限制不会收缩亲和性集合
/proc/cpuinfo整机核数与容器完全无关
cpuset.cpuscgroup cpuset 列表是「允许集合」,不是时间配额
cpuacct.usage(固定路径)外层累计 CPU同一个挂载点问题

**另一个坑:cpuacct.usage 也不能用来量自己。**容器里只跑 1 个忙线程 2 秒:

$ docker compose exec app python3 - <<'PY'
import time, threading, resource
def burn(stop):
    while time.time()<stop: pass
t0=time.time()
th=[threading.Thread(target=burn,args=(t0+2,)) for _ in range(1)]   # 只 1 个忙线程
[x.start() for x in th]; [x.join() for x in th]
w=time.time()-t0; r=resource.getrusage(resource.RUSAGE_SELF)
print('wall=%.2f self_cpu=%.3f -> self_cores=%.3f' % (w, r.ru_utime+r.ru_stime, (r.ru_utime+r.ru_stime)/w))
print('cpuacct.usage =', open('/sys/fs/cgroup/cpuacct/cpuacct.usage').read().strip())
PY
wall=2.00 self_cpu=2.063 -> self_cores=1.031
cpuacct.usage = 25599184459103      # ← 2.5e13 纳秒,整机累计,和自己无关

自己只用了 ~2 CPU 秒,cpuacct.usage 却给出 2.5e13 纳秒(≈ 25599 秒)。

3.3 本环境 Cgroup Driver: none —— 容器没有自己独立的 cgroup 目录

$ docker info | grep -iE 'cgroup'
 Cgroup Driver: none
 Cgroup Version: 1

Docker 配置成不管理 cgroup,容器直接被放进外层(CNB pipeline / 宿主)的 cgroup 层级里,cpus: "1.0" 的限制由外层施加。这解释了为什么「容器自己那一层的目录根本不存在」。

推论:「只按固定路径读 cgroup 文件」这种写法,在任何 namespace / driver 组合下都不可移植。正确姿势是先按 /proc/self/cgroup 定位自己那一层,读不到就用实测自证。

3.4 次要但致命的原因:自检恒真

app/server.py 里:

return {
    'cpu_limit': cores,
    'cpu_shards': shards,
    'shard_cpu_cost': 0.5,
    'plan_is_self_consistent': True,   # ← 写死 True,等于没有自检
}

即使 cpu_shards=1 而 cpu_limit=8(差 16 倍)也会报「一切正常」。正是这个恒真的自检,让「上报 8 核实际 1 核、偏差 695%」一路绿灯。

还有一处放大器:探测失败时的兜底值。

if cores is None:
    cores = float(os.environ.get('CALC_CPU_FALLBACK_CORES', '8'))   # 在 1 核容器里报 8 核

四、解决办法

只改 app/ 下两个文件,没有动 docker-compose.yml 的 cpus: "1.0" 闸门。

改动 1:app/cpu_capacity.py —— 读数改为三层,按可信度

read_cpu_limit()
  ├─ 1. 元数据:按 /proc/self/cgroup 相对路径找【自己那一层】的
  │     cgroup v2 cpu.max / cgroup v1 cpu.cfs_quota_us
  │
  ├─ 2. 自证:黑盒实测——N 个纯 CPU 线程跑 0.3s × 3 轮,
  │     取 getrusage(RUSAGE_SELF) 的 CPU秒/墙钟秒 的【最大值】
  │
  └─ 3. 交叉校验:元数据 ≤ 实测×1.35 → 采信元数据
                  元数据 >  实测×1.35 → 【以实测为准】

关键点:

  1. 自己那一层读不到时绝不静默回退到挂载点根目录 —— 那等于拿别人的配额冒充自己。
  2. 实测是物理兜底,不受任何静态元数据影响。
  3. 多轮取最大值:配额只会让真实并行度低于硬上限(被 throttle),不会让它高于物理上限,所以取最大是最接近上限的估计,不会测高。

核心代码:

def _cgroup_relpaths():
    """解析 /proc/self/cgroup,返回 {控制器名: 相对路径}"""
    out = {}
    with open('/proc/self/cgroup') as f:
        for line in f.readlines():
            parts = line.strip().split(':', 2)
            if len(parts) != 3:
                continue
            _hid, controllers, rel = parts
            for c in controllers.split(','):
                if c.strip():
                    out[c.strip()] = rel
    return out

def measure_affordable_cores(duration=0.30, rounds=3):
    """黑盒自证:getrusage(RUSAGE_SELF) 的 CPU 秒 / 墙钟秒,多轮取最大"""
    import resource, threading, time
    n_threads = max(2, min(len(os.sched_getaffinity(0)), 16))
    def burn(stop):
        acc = 0.0
        while time.time() < stop:
            for _ in range(2000): acc += 1.5
    best = 0.0
    for _ in range(rounds):
        r0 = resource.getrusage(resource.RUSAGE_SELF); t0 = time.time()
        ts = [threading.Thread(target=burn, args=(t0 + duration,)) for _ in range(n_threads)]
        for t in ts: t.start()
        for t in ts: t.join()
        wall = time.time() - t0
        r1 = resource.getrusage(resource.RUSAGE_SELF)
        cpu = (r1.ru_utime - r0.ru_utime) + (r1.ru_stime - r0.ru_stime)
        if wall > 0 and cpu > 0:
            best = max(best, cpu / wall)
    return (round(best, 3), 'probe') if best > 0 else (None, 'probe-failed')

改动 2:app/server.py —— 规划、自检、兜底

① 分片数:int() 截断 → 向上取整

# 旧(截断,会让分片总容量装不下上报核数)
shards = int(cores / 0.5)

# 新(向上取整:cores=0.915 → 2 片 = 1.0 核容量,装得下)
shards = max(1, min(MAX_SHARDS, _ceil_div(cores / SHARD_CPU_COST)))

差别(shard_cpu_cost=0.5):

cores=1.000  int-> 2 shards(1.0核)  ceil-> 2 shards(1.0核)   一致
cores=0.915  int-> 1 shards(0.5核)  ceil-> 2 shards(1.0核)   ← 截断装不下 0.915
cores=1.007  int-> 2 shards(1.0核)  ceil-> 3 shards(1.5核)   ← 截断装不下 1.007

② 自检:写死 True → 真判据

两条判据,任一不成立即 False:

  • shards == ceil(cpu_limit / shard_cpu_cost),且夹在 [1, MAX_SHARDS];
  • 分片总容量 shards * cost 必须装得下 cpu_limit。

③ 兜底值:向上兜底 8 → 只向下兜底

# 旧:探测失败 → 回退 CALC_CPU_FALLBACK_CORES=8(在 1 核容器里报 8 核!)
# 新:兜底值不许超过实测,宁可少报
cores = min(fallback, measured)

理由:多报会让调度器往节点超量派活(本问题的危害);少报只是浪费一点算力,是安全的一侧。

重建并验证

docker compose up -d --build     # 改的是 COPY 进镜像的文件,必须 --build,restart 无效
sleep 4
docker compose logs app | grep '\[plan\]'
3day-calcpool  | [plan] cpu_limit=1.011 (来源 probe(threads=8,rounds=3)(self-cgroup-unreadable))
3day-calcpool  | [plan] cpu_shards=3 每分片 0.5 核
3day-calcpool  | [plan] self_consistent=True

五、验证

5.1 核心判定:偏差 695% → 2%

docker compose exec -T app python3 /client/check_capacity.py
服务上报 cpu_limit  : 0.985
服务上报 cpu_shards : 2
读数来源            : probe(threads=8,rounds=3)(self-cgroup-unreadable)

实测窗口            : 3.00s
窗口内 CPU 秒       : 3.021
实测真实可核数      : 1.007
偏差                : 0.022 核 (2%)

判定: OK —— 上报值与真实可用一致      ← RC=0

5.2 完整判定:PASS=17 FAIL=4 → PASS=21 FAIL=0(连续 6 次稳定)

bash ops/verify.sh
# PASS=21 FAIL=0

连跑 5 次 + down -v 干净重建,全部稳定(实测口径依赖计时,所以特意验证稳定性):

RUN 1: PASS=21 FAIL=0   RUN 4: PASS=21 FAIL=0
RUN 2: PASS=21 FAIL=0   RUN 5: PASS=21 FAIL=0
RUN 3: PASS=21 FAIL=0   干净重建后: PASS=21 FAIL=0

关键断言:

[PASS] 部署闸门未被改动(cpus == 1.0)                        ← 没靠拧大闸门通关
[PASS] A1 上报核数未超过真实可用(0.9850 <= 1.0073 + 容差)
[PASS] A2 读数 1.011 与实测真实可核数 1.0073 一致(未读到别人的配额)
[PASS] B2 /resize 能改到 limit=2.0(规划确实以上报值为输入)    ← 没写死常量
[PASS] B2 /replan 后回到真实读数 1.0100(没有被写死成常量)
[PASS] C1 check_capacity.py 判定 OK
[PASS] C2 plan_check.py 判定 OK
[PASS] D1 自检对不自洽的 plan 能判 False(不是恒真)
[PASS] F1 并发全部 200
[PASS] F2 /resize 非法入参不崩

5.3 自检判别力(9 组用例,含畸形入参)

docker compose exec -T app python3 -c "
import sys; sys.path.insert(0,'/app'); import server
cases=[({'cpu_limit':8.0,'cpu_shards':1,'shard_cpu_cost':0.5},False,'装不下(8核只给1片)'),
({'cpu_limit':2.0,'cpu_shards':4,'shard_cpu_cost':0.5},True,'正好装下'),
({'cpu_limit':0.985,'cpu_shards':2,'shard_cpu_cost':0.5},True,'向上取整'),
({'cpu_limit':0.985,'cpu_shards':1,'shard_cpu_cost':0.5},False,'截断了装不下'),
({'cpu_limit':-1,'cpu_shards':2,'shard_cpu_cost':0.5},False,'负核数'),
({},False,'缺字段'),(None,False,'None'),
({'cpu_limit':'x','cpu_shards':'y','shard_cpu_cost':'z'},False,'非数字'),
({'cpu_limit':2.0,'cpu_shards':40,'shard_cpu_cost':0.5},False,'超上限')]
for p,e,n in cases:
    g=server.plan_self_check(p) if not isinstance(p,type(None)) else server.plan_self_check(p)
    print('%-4s exp=%-5s got=%-5s  %s'%('OK' if g==e else 'BAD',e,g,n))
"
OK   exp=False got=False  装不下(8核只给1片)
OK   exp=True  got=True   正好装下
OK   exp=True  got=True   向上取整
OK   exp=False got=False  截断了装不下
OK   exp=False got=False  负核数
OK   exp=False got=False  缺字段
OK   exp=False got=False  None
OK   exp=False got=False  非数字
OK   exp=False got=False  超上限
mismatch = 0

5.4 反向验证:还原后必须重新变红

python3 myfix/patch_server.py --restore
cp app/cpu_capacity.py.problem app/cpu_capacity.py
docker compose up -d --build && sleep 4
bash ops/verify.sh
# PASS=17 FAIL=4     ← 回到同样的 4 条失败,证明修的是真因

六、经验与避坑

6.1 关键认知

  1. 容器里读 cgroup,先问「我读的是谁的」。/sys/fs/cgroup/<ctrl>/ 这份挂载的「视角」不等于「本容器」。必须按 /proc/self/cgroup 的相对路径定位自己那一层;定位不到就不要读,绝不能退回挂载点根目录。
  2. 读不到配额时,唯一的自证口径是「实测」:getrusage(RUSAGE_SELF) 的 ru_utime+ru_stime / 墙钟秒。cpuacct.usage 在同一环境下量的是别人(实测 2.5e13 纳秒,自己只用 2 CPU 秒)。
  3. sched_getaffinity / cpuset.cpus / /proc/cpuinfo 都不是 CPU 配额 —— 分别量的是「允许跑在哪些 CPU 上」「cgroup cpuset 列表」「整机核数」。--cpus 限制不会收缩亲和性集合,所以 affinity 恒等于整机核数。
  4. 恒真的自检等于没有自检。plan_is_self_consistent 写死 True 是「观测口径错了却全程绿灯」的直接原因。
  5. 「让自检有判别力」这件事本身也要被测。只验证「好 plan 返回 True」不够 —— 旧代码写死 True 时那条用例也是绿的。必须专门拿坏 plan / 畸形 plan去撞它。
  6. 兜底值只能向下。探测失败时回退到「看起来合理」的 8 核,比回退到 1 核危险得多。
  7. int() 截断会把容量算小,导致分片总容量装不下上报核数 —— 向上取整才对。

6.2 踩坑记录

坑 1:Cgroup Driver: none 让固定路径彻底不可信docker info 显示 Cgroup Driver: none、Cgroup Version: 1。容器没有自己独立的 cgroup 目录,直接被放进外层 cgroup 层级,cpus 限制由外层施加。所以容器内不存在「自己那一层」的路径,cpu.cfs_quota_us 里的 800000 是外层给的。这类环境下静态读 quota 天然不可信,必须实测兜底。

坑 2:NameError: name 'plan_self_check' is not defined 第一版改完直接崩:

File "/app/server.py", line 100, in <module>
    _PLAN = plan_capacity(_CORES)
NameError: name 'plan_self_check' is not defined

原因:_PLAN = plan_capacity(_CORES) 在模块级执行,而 plan_self_check() 定义在它之后。 修法:抽出自包含的 _self_check_plan_values(limit, shards, cost) 放在 plan_capacity() 之前(内部 import math),plan_self_check(plan) 变成薄封装。 教训:模块级初始化代码不能调用后面才定义的函数。

坑 3:自检函数对畸形入参抛 KeyError 第一版写成 plan['cpu_limit'] 直接下标取值,喂 {} 就崩:

File "/app/server.py", line 129, in plan_self_check
    return _self_check_plan_values(plan['cpu_limit'], plan['cpu_shards'],
KeyError: 'cpu_limit'

自检函数面对畸形输入必须返回 False,而不是抛异常把 /stats、/plan 一起打挂。 改成 isinstance(plan, dict) 前置校验 + .get()。

坑 4:cpu_source 串太长,上报报文不可读 把「自己 cgroup 不可读的完整路径」拼进 source 后:

probe(threads=8,rounds=3)(自己cgroup不可读:v1-self-path-unreachable:/cnb-arg-1k36ken0h-001/bc348e…b7bf2)

这个串会随报文送给调度器。改成短标签 probe(threads=8,rounds=3)(self-cgroup-unreadable),细节留注释里。

坑 5:改的是 COPY 进镜像的文件,restart 不生效Dockerfile 是 COPY app/ /app/,改完 app/*.py 必须 docker compose up -d --build。(挂载进来的 ops/、client/ 则不用重建。)

坑 6:实测口径依赖计时,必须验证稳定性 黑盒实测会被机器负载影响。补偿手段:每轮 0.3s × 3 轮取最大;线程数按 sched_getaffinity(这里 8);交叉校验容忍因子给 1.35。并且连跑多次 + down -v 重建都要绿才算过。

坑 7:/resize 之后忘记 /replan,测试会串/resize 是人工覆盖,/replan 才重新探测。判定脚本 B 节会把规划改成 2 核,中途失败留在 2.0 会让后续 A/C 节读到被覆盖的值。手工打接口调试后记得 curl -X POST .../replan。

坑 8:ops/apply_fix.sh 不带参数是「应用参考解」而非「还原」 场景自带的 ops/apply_fix.sh 裸跑会覆盖你自己写的实现。还原要用 --restore。本解法的可重放脚本在 myfix/(patch_server.py 支持 --restore)。

6.3 一句话自查法(线上 5 分钟)

在怀疑「容器 CPU 核数读错」的服务里跑:

# 1) 我读的是谁?(固定路径 vs 自己那一层)
cat /proc/self/cgroup | grep -E 'cpu|cpuacct'
cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us /sys/fs/cgroup/cpu/cpu.cfs_period_us
ls -d /sys/fs/cgroup/cpu/<上一步的相对路径> || echo "自己那一层不可达"

# 2) 到底有几个核是真的?(唯一可信口径)
python3 -c "
import resource,threading,time
def b(s):
    x=0.0
    while time.time()<s:
        for _ in range(2000): x+=1.5
best=0
for _ in range(3):
    r0=resource.getrusage(resource.RUSAGE_SELF);t0=time.time()
    ts=[threading.Thread(target=b,args=(t0+0.3,)) for _ in range(8)]
    [x.start() for x in ts];[x.join() for x in ts]
    w=time.time()-t0;r1=resource.getrusage(resource.RUSAGE_SELF)
    best=max(best,((r1.ru_utime-r0.ru_utime)+(r1.ru_stime-r0.ru_stime))/w)
print('real cores = %.3f'%best)"

# 3) 服务上报的值和它一致吗?
curl -s localhost:8800/stats | python3 -m json.tool | grep -E 'cpu_limit|cpu_source'

只有第 2 步的实测值是可信基准;第 1 步读到的东西只有「等于实测」时才能用。

6.4 修复 checklist

  • 读 cgroup 前先按 /proc/self/cgroup 定位自己那一层,读不到不退回根目录
  • 拿不到配额就用 getrusage(RUSAGE_SELF) 实测自证(不要用 cpuacct.usage)
  • 静态读数与实测冲突时以实测为准(报高的危害远大于报低)
  • 兜底值只向下,绝不向上
  • 自检必须有判别力,且判别力本身要被坏输入测过
  • 容量规划用向上取整,保证分片总容量装得下上报核数
  • 上报报文里的 source 用短标签,长排障信息留日志/注释

七、复现命令速查

git checkout 2026-09-23-1600
cd scenario/2026-09-23-1600

# 问题态
cp app/cpu_capacity.py.problem app/cpu_capacity.py
cp app/server.py.problem       app/server.py
docker compose up -d --build && sleep 4
bash ops/verify.sh                                        # PASS=17 FAIL=4
docker compose exec -T app python3 /client/check_capacity.py   # 判定: BAD

# 应用本解
cp myfix/cpu_capacity.py app/cpu_capacity.py
python3 myfix/patch_server.py
docker compose up -d --build && sleep 4
bash ops/verify.sh                                        # PASS=21 FAIL=0
docker compose exec -T app python3 /client/check_capacity.py   # 判定: OK

# 反向验证
python3 myfix/patch_server.py --restore
cp app/cpu_capacity.py.problem app/cpu_capacity.py
docker compose up -d --build && sleep 4 && bash ops/verify.sh   # 又变红

docker compose down -v

相关文件:

  • 场景:scenario/2026-09-23-1600/
  • 完整操作记录:scenario/2026-09-23-1600/SOLUTION.md
  • 可重放补丁:scenario/2026-09-23-1600/myfix/

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-09-23-1600-dockercgroup-cpu-81

分享这篇文章