尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

gVisor Runtime 测试实战:在 runsc 沙箱内运行语言运行时完整测试套件并治理兼容性

gVisor Runtime 测试实战:在 runsc 沙箱内运行语言运行时完整测试套件并治理兼容性 gVisor Runtime 测试实战在 runsc 沙箱内运行语言运行时完整测试套件并治理兼容性【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisorgVisor 的test/runtimes测试体系把 Go、Java、NodeJS、PHP、Python 五大语言运行时的官方测试套件直接放进 gVisor 容器中执行作为验证 runscgVisor 运行时对真实应用负载兼容性的最高层集成测试。读完本文你将理解该体系由 Docker 镜像、容器内代理 proctor、Bazel 入口 runner 与排除清单四个组件构成的完整工作机制掌握本地运行各语言测试套件的make目标与调参方法并学会升级运行时版本、清理排除清单、接入新语言运行时的完整操作流程。一、定位运行时测试在 gVisor 测试金字塔中的角色gVisor 的测试分为多个层次test/syscalls中的 C/C 系统调用测试针对单个 syscall 的行为做精确验证而运行时测试Runtime Tests则是高级集成测试high-level integration tests——它不关心单个系统调用而是直接跑某个语言运行时的全部官方测试如 CPython 的test模块、Node.js 的test-build、JDK 的 TCK以语言生态自身的标准来检验 gVisor 的 Linux 行为一致性。这种测试的价值在于语言运行时对操作系统环境的敏感度极高fork/exec、信号、文件描述符限制、调度、网络语义等任何 gVisor 与真实 Linux 的细微偏差几乎都会在大型运行时的测试套件中暴露出来。因此运行时测试既是回归防线也是兼容性信号的来源。二、体系架构四大组件2.1 组件总览运行时测试由以下四个部分组成组件路径职责镜像images/runtimes每种语言运行时一个 Docker 镜像内含该语言的完整测试套件及运行所需的全部库与工具proctortest/runtimes/proctor运行在容器内的代理二进制向 runner 提供统一的命令行 API列出并执行测试runnertest/runtimes/runner由bazel run调用的测试入口负责启动 Docker使用runsc作为 runtime、挂载 proctor、组织分批执行排除清单test/runtimes/exclude每种语言运行时一个 CSV 文件记录应排除的测试全路径及排除原因当前仓库内置了五个运行时的测试目标见 test/runtimes/BUILD对应的镜像位于images/runtimes下images/runtimes/go1.22/Dockerfile.x86_64images/runtimes/java21/Dockerfile.x86_64images/runtimes/nodejs22.2.0/Dockerfile.x86_64images/runtimes/php8.3.7/Dockerfile.x86_64images/runtimes/python3.12.3/Dockerfile.x86_642.2 proctor容器内统一测试代理proctor 的设计目标是屏蔽各语言测试框架的差异对外暴露统一命令。它由 test/runtimes/proctor/main.go 实现核心命令行参数及默认值源码中flag定义如下参数默认值说明--runtime必填运行时名称go/java/nodejs/php/python--listfalse列出镜像中全部可用测试每行一个--tests空全部逗号分隔的测试名子集--pausefalse让容器无限期挂起并回收僵尸子进程--timeout90m整个批次的超时--per_test_timeout20m单测试超时0 表示禁用--runs_per_test1每个测试重复运行次数用于发现 flaky 测试--flaky_is_errortrue检测到 flaky多次运行结果不一致时是否判为失败--flaky_short_circuittrue发现 flaky 后立即退出不再跑满runs_per_test次proctor 内部的抽象核心是TestRunner接口定义在 test/runtimes/proctor/lib/lib.go// TestRunner is an interface that must be implemented for each runtime // integrated with proctor. type TestRunner interface { // ListTests returns a string slice of tests available to run. ListTests() ([]string, error) // TestCmds returns a slice of *exec.Cmd that will run the given tests. // There is no correlation between the number of exec.Cmds returned and the // number of tests. It could return one command to run all tests or a few // commands that collectively run all. TestCmds(tests []string) []*exec.Cmd }TestRunnerForRuntime()通过 switch 将运行时名映射到具体实现goRunner、javaRunner、nodejsRunner、phpRunner、pythonRunner各实现的ListTests()位于 test/runtimes/proctor/lib 目录下这是理解如何给每种语言枚举测试的关键。几个值得注意的实现细节1.--pause模式充当容器内 init。当 runner 启动测试容器时首先进入的就是这个模式。lib.PauseAndReap()持续监听SIGCHLD并用Wait4(-1, nil, WNOHANG, nil)循环回收所有终止的子进程避免僵尸进程堆积——这相当于一个极简 init 进程。2. 主动压低 RLIMIT_NOFILE。main.go中的setNumFilesLimit()会把RLIMIT_NOFILE软限制降到 32768Docker 容器默认为 1048576。源码注释解释了原因某些运行时测试例如 Python 的test_subprocess会枚举所有可能的文件描述符限制值过高时这类测试会因耗时过长而超时而 gVisor 中系统调用比内核路径更慢所需时间更长因此必须收缩这个范围。这是gVisor 下 syscall 开销放大效应的一个真实工程例证。3. 批次超时通过 SIGTERM 优雅终止。超过--timeout时proctor 对每个仍在运行的测试进程发送SIGTERM等待 5 秒让测试自行处理信号后才 panic 失败给测试留出了清理机会。4. 心跳日志。运行期间 proctor 每 15 秒打印一条Proctor checking in日志便于排查测试卡死时容器是否还活着。以 Python 为例test/runtimes/proctor/lib/python.go 展示了TestRunner的实现风格ListTests()执行./python -m test --list-tests按**测试库模块**粒度枚举而非逐条测试用例——因为 Python 用--fromfile按用例运行的开销远大于直接跑整个模块源码注释中给出的对比数据跑test_grammar直接执行约 0.065 秒走--fromfile则要 9.5 秒TestCmds()中无排除项的测试模块被合并为一条命令批量执行有特殊排除项的模块则生成 shell 管道--list-cases | grep -v 排除项$ | sed | xargs ./python -m test.module即列出全部用例、过滤掉排除项、按模块一次跑完该文件还内置了一个exclude映射表记录了 Python 下已知在 gVisor 失败的用例及对应 bug 号如 UDP-LITE、sched_setparam相关用例部分用例标注为 Broken test. Fails with runc too.即原生 runc 下同样失败的用例。2.3 runnerBazel 侧测试入口runner 是宿主机上的入口二进制由 test/runtimes/runner/main.go 实现参数与环境变量映射如下这正是make变量的落点runner 参数环境变量默认值说明--lang由构建规则注入必填语言运行时名--image由构建规则注入必填运行时镜像名--exclude_file由构建规则注入空CSV 格式排除文件字段测试名, bug id, 注释--testsRUNTIME_TESTS_FILTER空逗号分隔测试名白名单即使被排除也会运行--batch由构建规则注入默认 5050一条命令内运行的测试数--timeout构建规则注入--test_timeout180020m批次超时--per_test_timeoutRUNTIME_TESTS_PER_TEST_TIMEOUT20m单测试超时0 禁用--runs_per_testRUNTIME_TESTS_RUNS_PER_TEST1每测试重复次数0 等同于 1--flaky_is_errorRUNTIME_TESTS_FLAKY_IS_ERRORtrueflaky 是否算套件失败--flaky_short_circuitRUNTIME_TESTS_FLAKY_SHORT_CIRCUITtrue检出 flaky 后是否提前退出其执行流程实现在 test/runtimes/runner/lib/lib.go完整调用链为通过dockerutil.MakeContainer构造 Docker 客户端并以runsc为 runtime 启动镜像runtimes/imageCopyFiles把 proctor 二进制拷入容器/proctor路径随后以/proctor/proctor --pause作为容器入口启动对应上文 2.2 中的 init 模式通过docker execprivileged、user 0执行/proctor/proctor --runtime lang --list拉取全部测试清单调用testutil.TestIndicesForShard做分片选择只处理本分片负责的测试并记录跳过项将测试按batchSize分批每批生成一个testing.InternalTestdocker exec执行/proctor/proctor --runtime lang --tests batch --timeout 剩余时间并追加ProctorSettings.ToArgs()透传的四个 flaky 相关参数批次成功打印PASS: (耗时) n tests passed失败或超时打印完整批次清单与输出testing.MainStart汇总退出码。其中ExcludeFilter解析 CSV 时跳过表头行把第一列测试名装入 map过滤函数对命中项返回 false——这就是排除清单的生效机制。2.4 构建规则runtime_test 宏definitions 见 test/runtimes/defs.bzl。runtime_test(name, ...)宏生成的实际可执行对象是一个 Bazel 生成的 wrapper 脚本内容为#!/bin/bash runner-binary --lang lang --image name --batch batch --exclude_file file $三个属性值得说明image默认为name即目标名就是images/runtimes/name目录名这也是升级流程中BUILD 目标名必须与 Dockerfile 目录名一致的由来batch默认 50Java 目标覆盖为 100见 test/runtimes/BUILD规则依赖//:releaserunsc 二进制源码注释说明其用途是gVisor 代码有任何变更时使 Bazel 缓存失效从而保证每次测试都使用最新的 runsc目标自带no-sandbox、manual标签local_test_tags并支持 shardingBUILD 中使用more_shards/most_shards如java21用most_shards以并行化最耗时的 Java 套件。exclude_test 还会为每个排除文件生成一个go_testexclude_test.go确保 CSV 能被正确解析。三、本地运行完整运行时测试套件以下make目标可在本地运行某语言的全部运行时测试各目标的版本以当前仓库为准语言版本运行测试套件Go1.22make go1.22-runtime-testsJava21make java21-runtime-testsNodeJS22.2.0make nodejs22.2.0-runtime-testsPhp8.3.7make php8.3.7-runtime-testsPython3.12.3make python3.12.3-runtime-tests注意Java 运行时测试在 16 核机器上需要 1 小时以上规划本地验证时间时请预留。这些目标定义在 Makefile 中其底层行为为%-runtime-tests: load-runtimes_% $(RUNTIME_BIN) IMAGE_TAG$(call tag,runtimes_$*) \ $(call test_runtime_cached,$(RUNTIME),--test_timeout1800 --test_envRUNTIME_TESTS_FILTER... //test/runtimes:$*)即先执行load-runtimes_name构建并导入对应镜像、确保runsc二进制RUNTIME_BIN就绪再对//test/runtimes:name目标执行bazel test30 分钟测试超时并把五个RUNTIME_TESTS_*环境变量透传给 runner。3.1 可调行为make 变量通过以下make变量可以修改运行时测试行为默认值见 Makefile其中RUNTIME_TESTS_PER_TEST_TIMEOUT默认 20m、RUNTIME_TESTS_RUNS_PER_TEST默认 1、两个 flaky 开关默认 trueRUNTIME_TESTS_FILTER逗号分隔的测试名单白名单即使测试在排除清单中也会运行。适合调试单个失败用例。RUNTIME_TESTS_PER_TEST_TIMEOUT修改单测试超时。调试倾向于卡死的测试时调小它让其更快失败。RUNTIME_TESTS_RUNS_PER_TEST每个测试的重复运行次数用于发现 flaky 测试。RUNTIME_TESTS_FLAKY_IS_ERROR布尔值。当某测试被判定为 flaky多次运行中有时成功有时失败时是把整个套件判为失败true还是判为成功false。RUNTIME_TESTS_FLAKY_SHORT_CIRCUIT布尔值。若为 true重复运行中一旦某测试被判定 flaky至少成功一次且至少失败一次立即退出而不再跑满RUNTIME_TESTS_RUNS_PER_TEST次。这些变量经--test_env传入 runner再由ProctorSettings.ToArgs()转成 proctor 的--per_test_timeout/--runs_per_test/--flaky_is_error/--flaky_short_circuit参数。flaky 判定逻辑在 proctor/main.go 中每个测试命令重复运行runs_per_test次统计成功次数与首次失败若既成功又失败即为 flaky随后按flaky_is_error决定log.Fatalf失败或log.Printf放行若全部失败则直接FAIL。3.2 调用示例以 PHP 为例仓库 README 原例为旧版本php8.1.1此处按当前仓库的 8.3.7 更新$ make php8.3.7-runtime-tests \ RUNTIME_TESTS_FILTERext/standard/tests/file/bug60120.phpt \ RUNTIME_TESTS_PER_TEST_TIMEOUT10s \ RUNTIME_TESTS_RUNS_PER_TEST100含义只运行该单个 PHP 测试即使它在排除清单里也照样跑单测试超时收紧到 10 秒以便快速暴露卡死重复 100 次以检验其是否 flaky。3.3 镜像构建细节测试镜像均基于ubuntu:jammy并从源码构建被测运行时以拿到完整的测试源树例如Pythonimages/runtimes/python3.12.3/Dockerfile.x86_64下载 cpython 源码./configure --with-pydebug后make -j $(nproc)debug 构建保留了更多断言检查有助于暴露 gVisor 侧问题NodeJSimages/runtimes/nodejs22.2.0/Dockerfile.x86_64下载 Node 源码后先应用 timeouts.patch增大测试内部超时以降低 gVisor 下的 flakiness再执行make test-build并以dumb-init作为 ENTRYPOINT 前缀模拟 Linux init 进程避免 worker 进程相关测试失败。本地构建单个镜像可用docker build images/runtimes/runtime验证详见下文升级流程。四、清理测试失败后的容器残留处理运行时测试失败或测试容器自身意外崩溃时容器可能未被移除甚至无法正常退出这会导致docker system prune等命令永远挂起。按顺序执行以下命令进行清理docker ps -a # 列出所有 docker 容器排查挂起容器时有用 docker kill $(docker ps -a -q) # 杀掉所有正在运行的容器 docker rm $(docker ps -a -q) # 移除所有已退出的容器 docker system prune # 移除无用数据五、升级已有运行时测试版本按 README 给出的流程升级某语言运行时版本需要五步更新 Docker 镜像位于 images/runtimes重命名版本目录更新其中的包与下载 URL 指向新版本用docker build images/runtimes/new_runtime验证镜像可构建。更新runtime_test目标test/runtimes/BUILDname字段必须等于第 1 步创建的 Dockerfile 目录名——因为runtime_test宏正是用name解析为镜像路径images/runtimes/name。更新 CI 流水线仓库根目录下.buildkite/pipeline.yaml。运行测试并分诊失败部分语言测试天生 flaky 或从未通过其他失败可能暴露 gVisor 的 bug 或与 Linux 行为的偏离需要逐一定性。更新排除清单test/runtimes/exclude按新版本重命名 CSV 文件并把所有失败测试连同原因加入。排除 CSV 的格式为三列test name, bug id, comment实际样例见 test/runtimes/exclude/go1.22.csvtest name,bug id,comment cmd/cgo/internal/testerrors,b/339191886,Too slow cmd/internal/testdir:0_1,,tries to run ALL tests in test/ directory and times out os/signal,b/339311315, syscall,b/118781998,六、清理排除清单把曾失败变回回归保护运行时升级后测试可能已被删除、修改修好或弄坏或新增。当你得到一份全绿的排除清单后按以下步骤收敛它核对测试是否仍存在查看每种运行时的ListTests()实现test/runtimes/proctor/lib 目录拿到镜像中的真实测试清单剔除排除清单里已不存在的条目。在 runc原生下运行所有被排除的测试若测试在原生 Linux 下也失败说明它本身是坏测试Broken test应在原因列标注Broken test。这类测试对 gVisor 不提供任何兼容性差距信号可以安心忽略。此外此前标记为 broken 的测试若仍未修复保持原样若已修复则清空其原因字段。在 runscgVisor下运行所有非坏且非 flaky 的被排除测试若现在能通过了就从排除清单中移除。这实际上扩大了测试覆盖面——这个测试曾经失败、现在通过说明中间某处被修好了重新启用它等价于为那个修复增加了一条回归测试。对标记为 flaky 的被排除测试在 runsc 下重复运行 100 次对应RUNTIME_TESTS_RUNS_PER_TEST100若不再 flake则移出排除清单。关闭对应 bug所有现在通过的测试其 bug 已经失效应逐一关闭。七、为新语言创建运行时测试为全新语言接入运行时测试的流程与上文升级流程基本相同只是第 1 步更费工你需要先弄清楚如何在一个 Docker 容器内下载并运行该语言的官方测试套件。完成镜像后还必须为它实现 proctor 的 TestRunner 接口即两个方法ListTests() ([]string, error)枚举镜像中可运行的测试。参照现有实现——Python 用python -m test --list-tests按模块枚举Go 类运行时则遍历go test可发现的包路径也可用 lib.go 中的 Search 辅助函数按正则匹配文件遍历目录TestCmds(tests []string) []*exec.Cmd为给定测试构造执行命令。注意返回值与测试数量之间没有一一对应关系可以返回一条跑所有测试的命令也可以返回若干条合起来覆盖全部测试的命令Python 实现正是无排除项模块合并为一条、有特殊排除的模块各生成一条 shell 管道的策略。实现完成后在TestRunnerForRuntime()的 switch 中注册该语言并按 2.4 节的方式新增runtime_test构建目标与排除 CSV即完成接入。八、小结test/runtimes体系的设计可以概括为三层解耦镜像封装语言环境、proctor抹平语言测试框架差异、runner负责编排分片、分批、超时、flaky 治理再由排除清单以测试名 bug 号 原因的结构化方式沉淀兼容性差距。对 gVisor 而言这套测试每重新启用一个被排除的测试就意味着兼容性覆盖面的一次实质增长而对使用者而言它同样是一套可以直接复用在自己环境里、用语言生态自己的标尺检验 gVisor 的沙箱验证工具。【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表