执行摘要
- 一句话:修复多节点 Docker 测试的网络清理与资源隔离问题,提升 CI 可靠性。
- 推荐动作:这是一个重要的 CI 基础设施修复,直接解决了阻塞测试流水线的 bug。尽管变更集中在两个 shell 脚本,但其设计决策(如使用
mktemp 生成唯一网络名、捕获容器 ID、分离清理责任)体现了良好的防御性编程和所有权模型。建议 CI/CD 维护者和基础设施团队精读此 PR,了解其解决资源隔离问题的方法,这些方法可能适用于其他需要可靠清理的脚本。对于普通贡献者,了解 CI 环境的这一改进即可。
功能与动机
L4 分布式 2-节点测试车道在测试开始前就失败,因为 docker-net 网络已存在。其 EXIT 陷阱在容器不存在时停止于 docker stop node0,导致网络从未被移除。主构建 #87376 演示了这两种失败;开放警报有八次失败观察。需要恢复现有的空遗留 docker-net,拒绝被占用的遗留网络,并为新网络和容器使用调用特定的名称。使用本次调用实际创建的容器 ID 执行命令和清理,防止另一个调用在启动期间删除新创建的空网络。即使早期停止失败,也要尝试清理每个拥有的容器和拥有的网络。保留原始命令错误;当命令本身成功时报告清理失败。移除 AMD 包装器中冗余的全局清理,该清理之前可能在助手拒绝占用网络后停止 node0/node1。助手在成功和失败时都拥有其清理。
实现拆解
- 网络创建与生命周期管理:在
.buildkite/scripts/run-multi-node-test.sh 中,引入 RUN_DIR 和 NETWORK_NAME 变量,使用 mktemp -d 生成唯一临时目录并从中提取唯一名称(如 docker-net-XXXX),确保每次调用使用唯一网络。start_network() 函数首先检查名为 docker-net 的遗留网络是否存在:如果存在且无容器连接,则移除它;如果仍有容器连接,则拒绝复用并返回错误。然后创建以唯一名称命名的新网络。这解决了遗留网络残留和并发调用冲突问题。
- 容器命名与跟踪:修改
start_nodes() 函数,将容器命名从固定的 node$node 改为使用 NETWORK_NAME 前缀的 $NETWORK_NAME-node$node,确保容器名唯一。同时,使用 docker run 命令的输出捕获容器 ID,并存储在 STARTED_CONTAINERS 数组中。所有后续的 docker exec 命令(如启动 Ray、运行测试)都改用容器 ID 而非名称,避免名称冲突和歧义。
- 清理逻辑增强:重写
cleanup() 函数。该函数现在遍历 STARTED_CONTAINERS 数组,尝试停止每个容器,并记录任何失败。然后检查 NETWORK_CREATED 标志,如果为 1 则尝试移除唯一网络。清理成功后移除临时目录 RUN_DIR。函数保留原始退出状态:如果测试命令失败,则返回该失败状态;否则返回清理失败状态。这确保了即使部分清理失败,测试结果仍能正确报告,且不会遗漏清理步骤。
- AMD 包装器简化:在
.buildkite/scripts/hardware_ci/run-amd-test.sh 中,删除了 cleanup_network() 函数及其在测试执行后和解析失败时的调用。这是因为助手脚本 (run-multi-node-test.sh) 现在完全负责其自身创建的资源的清理,移除了冗余且可能引发问题的全局清理逻辑。
关键文件:
.buildkite/scripts/run-multi-node-test.sh(模块 测试脚本;类别 other;类型 core-logic): 多节点测试辅助脚本,包含本次修复的核心逻辑:网络生命周期管理、容器命名与跟踪、增强的清理逻辑。
.buildkite/scripts/hardware_ci/run-amd-test.sh(模块 CI包装器;类别 infra;类型 infrastructure): AMD CI 硬件测试包装器,移除了冗余的全局清理逻辑,将资源清理责任委托给助手脚本。
关键符号:start_network, start_nodes, cleanup
关键源码片段
.buildkite/scripts/run-multi-node-test.sh
多节点测试辅助脚本,包含本次修复的核心逻辑:网络生命周期管理、容器命名与跟踪、增强的清理逻辑。
# 生成唯一网络名称,基于临时目录
RUN_DIR=$(mktemp -d)
NETWORK_NAME="docker-net-${RUN_DIR##*/}"
# 启动网络函数:检查并处理遗留网络,然后创建唯一网络
start_network() {
# 检查名为 docker-net 的遗留网络是否存在
if docker network inspect docker-net > /dev/null 2>&1; then
# 检查遗留网络是否有容器连接
if [ "$(docker network inspect --format '{{len .Containers}}' docker-net)" != 0 ]; then
echo "docker-net still has attached containers; refusing to reuse it." >&2
return 1
fi
# 无容器连接,安全移除遗留网络
docker network rm docker-net
fi
# 使用唯一名称创建新网络,固定子网保持兼容性
docker network create --subnet=192.168.10.0/24 "$NETWORK_NAME"
NETWORK_CREATED=1
}
# 启动节点函数:使用唯一网络名和容器名,并捕获容器 ID
start_nodes() {
for node in $(seq 0 $(($NUM_NODES-1))); do
# ... (GPU 设备映射等配置省略 )
# 使用唯一容器名,并捕获容器 ID
CONTAINER_ID=$(docker run -d ... --name "$NETWORK_NAME-node$node" --network "$NETWORK_NAME" ...)
STARTED_CONTAINERS+=("$CONTAINER_ID")
# 使用容器 ID 执行后续命令,确保引用唯一
docker exec -d "$CONTAINER_ID" /bin/bash -c "ray start ..."
done
}
# 清理函数:遍历拥有的容器和网络,记录失败但继续清理
cleanup() {
local status=$?
local cleanup_status=0
# 停止本次调用创建的所有容器
for container_id in "${STARTED_CONTAINERS[@]}"; do
docker stop "$container_id" || cleanup_status=$?
done
# 移除本次调用创建的网络(如果创建了)
if [ "$NETWORK_CREATED" -eq 1 ]; then
docker network rm "$NETWORK_NAME" || cleanup_status=$?
fi
# 移除临时目录
rmdir "$RUN_DIR" || cleanup_status=$?
# 保留原始测试命令的退出状态
if [ "$status" -ne 0 ]; then
return "$status"
fi
return "$cleanup_status"
}
评论区精华
CodeRabbit AI 在审查中提出了三个关键问题:
- 资源所有权隔离:指出当
start_network() 返回错误时,下游的 cleanup_network() 可能仍会尝试停止不属于本次调用的容器和网络,造成破坏。结论是助手脚本应完全拥有其资源的生命周期管理,AMD 包装器的冗余清理已被移除。
- 并发调用安全:指出当前实现仍使用固定子网
192.168.10.0/24,在并发调用时可能因子网重叠而导致网络创建失败。作者在 PR body 中已说明此限制,并指出并发调用可能因子网重叠而失败,但不会相互删除资源。这被视为一个已知且可接受的折衷,因为 SIGKILL 等强制终止仍可能遗留需要清理的资源。
- 静态分析建议:AI 工具建议使用调用特定的锁或唯一网络来序列化所有权。作者通过为每次调用使用唯一网络名称(基于
mktemp 生成的临时目录)实现了这一目标,从而避免了复杂的锁机制。
- 资源所有权与清理责任 (design): 助手脚本
run-multi-node-test.sh 完全拥有其创建的资源的生命周期管理。通过移除 AMD 包装器中冗余的 cleanup_network() 函数,将清理责任明确化。助手脚本内部的 cleanup() 函数处理所有清理,并保留原始退出状态。
- 并发调用的子网冲突 (correctness): 作者在 PR body 中承认这是一个已知限制:‘Concurrent invocations on one daemon may fail network creation due to subnet overlap; they do not delete each other's resources.’ 固定子网被保留,因为测试命令使用其头部 IP。这是一个可接受的折衷,优先解决更紧迫的资源泄漏问题。
风险与影响
- 风险:
- 子网冲突风险:所有调用仍使用固定的子网
192.168.10.0/24。在单个 Docker 守护进程上并发运行多个多节点测试时,如果两个调用尝试创建网络,第二个调用将因子网冲突而失败。这是一个已知的限制,在 PR body 中已说明。
- 强制终止风险:如果调用被
SIGKILL 终止,EXIT 陷阱不会执行,可能导致临时目录和创建的 Docker 资源残留。这些资源需要手动清理。
- 遗留网络处理风险:
start_network() 函数在检查到名为 docker-net 的遗留网络且无容器连接时会将其移除。这假设此操作是安全的,但如果其他脚本也依赖此名称,则可能产生意外影响。
- 影响:
- CI 可靠性:直接修复了 L4 分布式 2-节点测试车道的启动失败和资源泄漏问题,预计能恢复该测试车道的稳定性,减少构建失败次数。
- 开发者体验:减少了因 Docker 网络冲突或清理不完整导致的 CI 失败,降低了开发人员的调试负担。
- 维护成本:清理逻辑更加健壮和自包含,移除了 AMD 包装器中的冗余代码,降低了长期维护的复杂性。
- 测试隔离:通过为每次调用使用唯一资源,提升了测试隔离性,减少了测试间的意外干扰。
- 风险标记:子网冲突风险, 强制终止后资源残留, 遗留网络清理假设
关联脉络
- PR #47484 [ROCm][CI] Adding rocm multinode proto: 关联 Issue #47484 为 ROCm 多节点测试添加了协议支持。本 PR 修复了共享的多节点测试辅助脚本中的资源管理问题,这两个 PR 共同改进了多节点测试基础设施。
参与讨论