一轮沙箱压测 12 批负载,10 批全部成功。这 10 批没告诉我系统扛住了什么,只告诉我压力没打到设计边界。我的判断是:一轮全绿首先是测试设计的结论;要拿它说系统没问题,得先证明负载越过了边界、样本走进了被测的那段代码。
全绿的那批,不知道是扛住了还是没压到
那轮压测 12 批里,另外 2 批是按设计失败的。
一批 58 个任务,其中 30 个排队超过 20 秒的等待上限,被直接拒绝。另一批把镜像仓出口限速到 20 Mbit/s,20 个任务里 10 个因为镜像下载太慢,没能在时限内就绪。
拒绝 30/58 是整轮里唯一能证明「20 秒排队上限在起作用」的数据。假如这批也全成功,20 秒这个配置从头到尾一次都没被执行过——它可能写成了 20 分钟,可能根本没读进去,测试结果长得一模一样。
限速那批测的是另一件事:把镜像带宽单独压到不够用,看资源不足时它怎么失败——是超时未就绪,还是把别人的数据返回给了这个沙箱。失败的样子本身要测。
12 批一共 375 个请求(305 个任务 + 70 个会话),没有一个沙箱看到别人的数据。这条是全绿的,但它站得住:同一轮里有 2 批真被推到了拒绝,说明这套负载推得过边界。
报告里那 2 批写的是「按设计失败」,不是「有 2 批没过」。前一种是开跑前就排进去的,后一种是跑完才知道的。
同样的做法还有一处:建 pod 前的准入校验规则先跑一段只告警不拒绝,当场抓出 1 条会拒掉全集群所有 pod 的错规则。这个校验器能一票否决所有人,一条规则写错,全集群一个 pod 都建不出来。所以要灰度的是校验器本身。
「什么都没发生」和「什么都没跑起来」长得一样
另一轮测作弊防护。判分放在一个干净的新沙箱里跑,三种作弊手法 6 次全是 0 分,看着像防住了。
同一轮里另有一个本来不相干的功能测试:同时施加 5 种改动——改一个文本文件、新建一个文件、一个被 git 忽略的文件、一个二进制文件、一个仓库外的文件。结果是 5 种改动一个都没到判分沙箱。
根因在复制方式上:改动是按文本差异(git diff)搬过去的,diff 里混进 1 个二进制文件,整份 patch 就 apply 不上,5 种改动一起落空。正解是按字节原样复制。
那 6 次 0 分于是没有信息量。0 分可以来自「作弊被挡住」,也可以来自「作弊代码根本没传过去」,观测上完全一样。排掉后一种可能只需要一件事:交一份正确答案,看它能不能得分。
交正确答案就是阳性对照——放一个已知会出结果的样本,用它证明这条链路是通的。这轮里它做了:5 道安全类题由脚本交正确答案,runc 和 gVisor 下各 5 次全得分。只是做在了不需要它的那一组,最需要它的那组没有。
安全测试最容易撞上这条,它期望的结果本来就是「什么都没发生」。
选样本时该问的不是「这个样本跑不跑得通」,是「它会不会走到我要测的那几行」。
判据写在测之前
一个「要不要把开源沙箱引擎从链路里拿掉」的实验,门槛是事先写死的:直接建 pod 要快 30% 以上、每个沙箱对 K8s 的写请求不超过 5 次、成功率不降、不串数据、不留残留 pod。
实测 60 道题:直接建 22.6 秒,经引擎 45.0 秒,快约 50%,达标;写请求 4.2 次对 13.9 次,达标。
结论仍然是不换。两项都达标之后又追了一步根因,差距全落在三处配置上:init 容器没写资源申请,被命名空间默认值按 2 核记账(主容器实际只要 0.5 核);控制器自己 CPU 上限 0.5 核;沙箱对象配额只有 40 个,而这轮要 60 个。三处改完,差距降到 5% 以内。
写请求 13.9 次对 4.2 次这项改配置消不掉,留成了一条风险记录。判据达标只是开始查的理由。
另一次验证的门槛是 90% 出分率、gVisor 相对 runc 不到 2 倍。最后拿到 93.3%(15 个带判分器的题里 14 个正常出分,唯一没出分的是题目数据问题)和 1.66/1.74 倍。踩线通过。
踩线通过和事后把线划到 93% 以下,在报告里看起来一样,区别只在于判据写在哪一天。
还有一类判据是用来判定「不能下结论」的。同一份报告把判据定在 5% 和 10%,而关键对照组每组只跑了 1 次,报告自己写明单次波动就和 5% 同量级。按这条线划:44.3 秒对 56.0 秒差 21%,结论站得住;21.7 秒对 21.1 秒差 3%,只能写「没发现差异」。跑得多的那几组才分得出来:限内存 17 次对 9 次、让位 4 次对 3 次、gVisor 内存超限检测 6 次。
另一份报告把环境噪声量化了:实验集群的 etcd 磁盘常停顿 5–12 秒,最长卡过 18.9 秒,被测时间在几十秒量级,噪声占信号 10–20%。这个量级撑得起「差 3.79 倍」,撑不起「慢 15%」。噪声和判据阈值同量级时,单次测量判不出谁快谁慢,这笔账得在测之前算;测完再算,只会去凑那个更好看的解释。
没测到的要写成没测到
同一批实验里有几项是「没测成」,原样记着:
- 从一次快照复制多份,8 次全失败。实验机能分给沙箱的内存被别的实验占得只剩约 350 MiB,放不下一个几 GB 的任务环境。这是环境限制。
- 「只放行 kubelet 探测」的防火墙规则需要 nftables 0.9.5 以上,实验集群是 0.9.3。
- 只有 1 台工作机,「在 A 机器拉镜像、在 B 机器建沙箱」这条路一次都没跑。
- 一次沙箱误删(建好 120.7 秒后被删),复跑一轮加 4 个冷建都没复现,写成「未复现」,同时留下根因定位和适用边界。
「未复现」「没测成」「测出不行」是三种强度,后续动作各不相同:未复现要留根因和边界,没测成要留复测条件,只有测出不行才能关掉一个方向。全写成「不行」,省下的是当下几行字,赔进去的是以后重测的成本和一个被误杀的方案。
「没测出差别」要能说清为什么没差别,才算一条结论。控制器 CPU 从 0.5 核加到 2 核、限流值不动,耗时完全不变;同一组实验把限流减半到每秒 50 次,耗时从约 65 秒涨到 141.55 秒,差不多正好翻倍。两个结果放在一起才说得清:它大部分时间在限流器里等令牌,不在算。卡在限流上和卡在算力上,到这里才分得开。
照着一篇论文把批量任务的每个线程都调到最低 CPU 优先级,实测交互类沙箱每步耗时增幅 +187%,不调是 +180%,各 3 次——调了反而略差。原因没查清之前这条只能当待办:nice 只在同一个 cgroup 内部排序,而每个沙箱各占一组 cgroup,组间份额由 weight 决定,组内一起调低,相对顺序一点没变。知道这一点才找得到该换的位置——按类别给 cgroup 预留,交互类等 CPU 的时间占比从 16.5%/16.8% 降到 1.1%/0.8%。
我现在开跑前先写三行:这轮里哪一批注定被拒绝、哪条「什么都没发生」的结论配了阳性对照、每条判据的数值和写下它的日期。三行写不全就先别跑——写不全说明这轮的设计还没想清楚,跑完手里也只有全绿这一个事实。