Prhub

#24375 [SMG] Expand K8s integration tests: cross-namespace, lifecycle, multi-model

原始 PR 作者 Kangyan-Zhou 合并时间 2026-05-14 11:08 文件变更 7 提交数 2 评论 3 代码增减 +1364 / -0

执行摘要

跨命名空间、生命周期、多模型 K8s 集成测试

PR Body 指出:构建在 K8s 集成测试脚手架(#24278)之上,新增 5 个测试覆盖原始套件未触及的服务发现路径,确保网关在各种场景下的健壮性。

值得精读,尤其是测试设计中的以下决策:

  • 使用独占标签(如 cross-ns-test=truelifecycle=restart)隔离不同测试的 worker 池,避免跨文件干扰。
  • 在 IP 变化测试中优雅处理 CNI IP 重用场景,通过 skip 而非 false-pass。
  • 优雅排空测试验证 elapsed < grace_secs,确保 deregistration 在 deletionTimestamp 触发而非等到 Pod 完全终止。
    这些模式可以复用到底层 sglang 或其他微服务的集成测试中。
讨论亮点

Review 由 gemini-code-assist[bot] 提出三点改进建议,均已在第二提交中解决:

  • 复用 _poll_until helper_wait_for_pod_gone 自定义轮询逻辑重复了 conftest.py 中的 _poll_until,建议改为调用已有函数以提高维护性。
  • 安全访问 items[0]:直接访问 res["items"][0] 可能因空列表引发 IndexError,应在访问前用 assert 检查列表非空。
  • 避免脆弱的字符串匹配:在 _gone 函数中通过检查 stderr 是否包含 "NotFound" 来判定 Pod 是否存在,这受语言环境影响,建议改用 --ignore-not-found 标志并检查输出是否为空。
    以上建议均被作者采纳并在第二提交中修复。

实现拆解

  1. 添加 YAML 清单文件:在 manifests/ 目录下新增 4 个 YAML 文件(gateway-restart.yaml、gateway-cluster-scoped.yaml、rbac-cluster-scoped.yaml、gateway-multimodel.yaml),定义不同配置的网关 Deployment 和 Service,用于测试重启、集群范围发现和多模型隔离。
  2. 编写测试辅助函数:每个测试文件包含私有的辅助函数,如 _deploy_worker_pod_safe_force_delete_wait_for_pod_gone_ensure_namespace 等,用于部署/清理 worker Pod 并获取 Pod 信息。这些函数通过 subprocess 调用 kubectl,并注重错误信息的清晰呈现。
  3. 实现测试类与 fixture:使用 pytest fixture 管理网关部署(模块级),如 restart_gatewaycluster_scoped_gatewaymultimodel_gateways,确保网关就绪后再启动 port-forward。测试类(如 TestGatewayRestart、TestClusterWideDiscovery、TestMultiModelSelectorIsolation)包含具体的测试方法。
  4. 执行断言与轮询:测试通过网关的 HTTP API 查询注册的 worker,使用 _poll_until 轮询直到条件满足(如 worker 数量匹配、IP 变更生效),并通过 URL membership 断言而非单纯计数,避免跨测试干扰。
文件 模块 状态 重要度
sgl-model-gateway/e2e_test/k8s_integration/test_lifecycle.py 测试文件 added 8.45
sgl-model-gateway/e2e_test/k8s_integration/test_cross_namespace.py 测试文件 added 8.26
sgl-model-gateway/e2e_test/k8s_integration/test_multi_model.py 测试文件 added 7.91
sgl-model-gateway/e2e_test/k8s_integration/manifests/gateway-multimodel.yaml 清单文件 added 6.43
sgl-model-gateway/e2e_test/k8s_integration/manifests/gateway-restart.yaml 清单文件 added 5.83
sgl-model-gateway/e2e_test/k8s_integration/manifests/gateway-cluster-scoped.yaml 清单文件 added 5.81
sgl-model-gateway/e2e_test/k8s_integration/manifests/rbac-cluster-scoped.yaml 清单文件 added 5.08

关键符号

test_workers_re_discovered_without_duplicates_after_restart test_pod_ip_change test_graceful_drain test_workers_in_two_namespaces_are_both_discovered test_each_gateway_sees_only_its_model_pool _wait_for_pod_gone _gone restart_gateway cluster_scoped_gateway multimodel_gateways

关键源码片段

sgl-model-gateway/e2e_test/k8s_integration/test_lifecycle.py test-coverage

核心测试文件,包含 3 个生命周期场景:网关重启、Pod IP 变化、优雅排空,共 554 行。

"""Worker lifecycle integration tests.Covers three scenarios that the existing reconciliation/PD tests don't:
1. Gateway pod restart with persistent workers — verifies the K8s watcher
   re-discovers existing pods after the gateway restarts, with no duplicate
   registrations.
2. Pod IP change (same pod name, new IP) — verifies the gateway's worker
   registry tracks the new IP after a pod is force-deleted and recreated,
   not the stale one.
3. Graceful drain — verifies the gateway deregisters a worker as soon as
   K8s sets `metadata.deletionTimestamp` (handled by handle_pod_deletion in
   sgl-model-gateway/src/service_discovery.rs:533), instead of waiting for
   the pod to fully terminate. This is what keeps the registry fresh during
   long terminationGracePeriodSeconds windows / preStop hooks.
"""# ... imports and constants ...def _wait_for_pod_gone(name: str, timeout: int = 60):
    """Wait until a pod no longer exists.    Uses `kubectl get -o name --ignore-not-found`: empty stdout means the pod
    is gone (no need to substring-match "NotFound" against stderr, which is
    locale- and version-fragile). Any non-zero rc is a real cluster error
    (apise...
"""
    def check_gone():
        result = _kubectl(
            "get", "pod", name, "-n", NAMESPACE, "-o", "name",
            "--ignore-not-found", check=False,
        )
        if result.returncode != 0:
            raise RuntimeError(f"kubectl get failed: {result.stderr}")
        # stdout empty means pod is gone
        return not result.stdout.strip()
    # Use the existing _poll_until helper instead of a custom loop
    _poll_until(check_gone, timeout=timeout, pause=2)

上述代码片段展示了 _wait_for_pod_gone 函数,它利用 --ignore-not-found 标志和 _poll_until 轮询(在第二提交中根据 review 重构)。其他关键函数如 _deploy_worker_podrestart_gateway fixture 和三个测试方法(test_workers_re_discovered_without_duplicates_after_restarttest_pod_ip_changetest_graceful_drain)协同工作,覆盖声明中的场景。

评论区精华

复用 _poll_until 替代自定义轮询 设计

gemini-code-assist[bot] 指出 _wait_for_pod_gone 自定义轮询逻辑重复了 conftest.py 中的 _poll_until 辅助函数,建议改为调用已有函数以提高维护性和一致性。

结论:作者在第二提交中重构了 _wait_for_pod_gone,使用 _poll_until 进行轮询,并保留了自定义检查函数。 · 已解决

安全访问 items[0] 防空列表 正确性

gemini-code-assist[bot] 发现在获取重启网关的名称时,直接访问 res["items"][0] 假设列表非空,可能因部署延迟导致 IndexError,建议先检查 assert not empty。

结论:作者在第二提交中添加了 assert res.get("items"), "No pods found for smg-gateway-restart" 保护,使失败信息更可读。 · 已解决

避免脆弱的 stderr 字符串匹配 测试

gemini-code-assist[bot] 指出 _gone 函数通过检查 stderr 是否包含 "NotFound" 来判定 Pod 是否存在,这受语言环境 / 版本影响,与 _wait_for_pod_gone 文档中避免脆弱的字符串匹配的陈述相悖,建议改用 --ignore-not-found 标志。

结论:作者在第二提交中修改了 _gone 函数,使用 --ignore-not-found 标志并检查 stdout 是否为空,消除了依赖 stderr 文本的脆弱性。 · 已解决

风险与影响

本次变更仅涉及测试文件和部署清单,未修改任何运行时源码,因此源代码回归风险非常低。主要风险来自测试本身对 K8s 集群环境的依赖:kubectl 命令可能因上下文配置错误而失败;Pod 状态轮询超时可能导致测试不稳定;IP 重用场景被跳过但未标记强 skip,可能遗漏问题。但这些风险已被 PR 中的设计(独占标签、模块级 fixture、轮询重试)和 review 修复降低了。此外,新增的 YAML 清单引入了额外的网关 Deployment,需要确保 CI 集群资源充足。

影响范围:仅限 sgl-model-gateway 的 K8s 集成测试套件。
影响程度:低。对用户无感知,对系统无性能影响,但显著提高了服务发现功能(跨命名空间、生命周期、多模型)的测试信心。测试套件从 8 个增加到 13 个(加上原 PR 的 8 个),覆盖了更多关键路径。团队可通过这些测试更早地发现回归。

测试依赖 K8s 集群环境 kubectl 配置依赖 IP 重用场景被跳过未全覆盖 新增额外网关部署需资源

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论