一轮测试 4 个样本全部通过,对照组 0 通过,结果里没有一处异常。但它什么都没证明——这 4 个样本根本没走进我要测的那段代码。我的判断是:绿灯要能成立,先得证明样本执行到了被测代码;这件事得单独确认,通过率证明不了。
4 比 0 的那轮结果
一个沙箱服务要解决这样一个问题:平台替用户启动镜像时,怎么同时跑起平台自己的执行程序和镜像自带的启动命令。参考的开源做法 AgentENV 用一段启动脚本,它依赖只有虚拟机里才有的终端输入,换到容器运行时下 4 种镜像一个都起不来。我的改法是在只读工具盘里放一个 supervisor,由它把两边一起拉起来。
同样 4 种镜像,改进版全部能执行命令。4 比 0。
复核时我又加了两个镜像:nginx,和一个真实的强化学习题目镜像。两个各暴露一个 bug。
问题出在前面那 4 个镜像的入口上:它们在测试里跑的是工具盘上的 busybox httpd,不是镜像自己声明的启动命令。supervisor 确实把它拉起来了,命令也确实能执行,但「读出镜像的启动配置再原样施加」这段代码从头到尾没有被调用过。绿灯证明的是工具盘上那个 supervisor 能跑。
nginx 和真实题目镜像带自己的入口,那段代码第一次被执行,两个 bug 同时露出来:
- 启动命令被交给 shell 按空格切分。
nginx -g "daemon off;"这种带引号的参数被切成多段,正解是拿到参数列表直接 exec,不经过 shell。 - 只读了启动命令和参数,用户、工作目录、环境变量没读。镜像里声明了 WORKDIR 的程序换到根目录下启动,找不到自己的文件。
这件事最后写进架构文档,成了一条设计约束:凡是平台替用户启动镜像的地方,都要读出镜像自带的启动命令、参数、用户、工作目录、环境变量并原样施加;读不到就报错,不许退回默认值。
那 4 个样本的操作系统和包管理器各不相同,在我要测的那件事上完全一样——入口都来自工具盘。对这段代码来说它们是 1 个样本,不是 4 个。
阳性对照:先有一个已知会出结果的样本
这类问题在实验设计里有现成的名字,叫阳性对照:一组实验里要放一个已知一定会出结果的样本,它出了结果,才说明测量链路是通的。没有它,那些「什么都没测出来」全都读不出意思。4 个镜像那轮缺的就是它——没有一个样本是已知会走进被测代码的。
同一批实验里还有一个更难发现的例子。为了判断「另起一个干净沙箱判分能不能挡住作弊」,我在干净沙箱里跑了三种作弊手法共 6 次,全是 0 分。看起来是防住了。
另一个功能测试查的是「5 种文件改动能不能传到判分沙箱」:改一个文本文件、新建一个文件、一个被 git 忽略的文件、一个二进制文件、一个仓库外的文件。结果 5 种一个都没传过去。根因在复制方式上——按文本差异复制改动,diff 里只要混进 1 个二进制文件,整份 patch 就 apply 不上,5 个改动一起落空。
那 6 次 0 分于是有两个解释:作弊被挡住了,或者答题改动根本没到判分沙箱。
这一轮里阳性对照其实做了——5 道安全题由脚本提交正确答案,runc 和 gVisor 下各 5 次全部得分——但它做在了不需要它的那一组上。真正需要它的干净沙箱判分那组,恰好没有。
安全测试最容易撞上这条:它期望的结果本来就是「什么都没发生」,而「什么都没发生」和「什么都没跑起来」在结果里分不出来。
压测全绿,说明的是压力不够
压力测试也一样:成功率 100% 的那批负载,没给出任何「限流代码被执行过」的证据。
一组 12 批负载里,10 批全部成功,2 批按设计失败。一批是 58 个任务里有 30 个排队超过 20 秒的等待上限,被直接拒绝;另一批把镜像仓出口限速到 20 Mbit/s,20 个任务里 10 个没能在时限内就绪。
有信息量的是这 2 批。拒绝 30/58 证明排队上限那段代码跑到了;这批如果也全成功,那个 20 秒的配置从头到尾没被执行过,它写成 200 秒、或者单位写错,都不会被发现。限速那批把镜像带宽单独压到不够用,确认失败的形态是「超时未就绪」而不是「返回了错误的数据」——后一种才是要紧的失败。
同一份报告里还有一条:建 pod 前的准入校验规则先跑了一段只告警不拒绝,当场抓出 1 条写错的规则,它一旦生效会让全集群一个 pod 都建不出来。校验器自己就是那个能一票否决所有人的地方,所以要灰度的是它本身。
怎么确认样本走到了
最省事的办法是让被测代码故意坏一次:在那段代码里插一行抛异常,重跑全部样本,没变红的那个就是没走到。这一步只花几分钟。
另一件要看的是样本之间在被测的那件事上有没有区别。扩样本要沿着被测代码的分支扩,不是沿着「还有哪些镜像没试过」扩——白盒那一轨从真实调用路径反推用例,做的就是这件事。
代价是复核要多花时间,而且样本永远加不完。我这次只加了 2 个就命中 2 个 bug,靠的是挑样本时先问了一句:它会走到哪一段代码。