Kubernetes 集群长期稳定性验证:serve_hostnames Soak Test 源码级解析与实战指南
2026/9/8 18:15:08 网站建设 项目流程

Kubernetes 集群长期稳定性验证:serve_hostnames Soak Test 源码级解析与实战指南

【免费下载链接】kubernetesProduction-Grade Container Scheduling and Management项目地址: https://gitcode.com/GitHub_Trending/kuber/kubernetes

导读:本文聚焦 Kubernetes 官方仓库中面向「持续健康巡检」场景的浸泡测试(Soak Test)工具serve_hostnames,完整解析其测试原理、参数体系与输出解读。读者可以基于本文在 GCE 集群上复现该测试,理解它如何通过"每节点放置 Pod + 经由 Service 代理反复查询 + 校验所有 Pod 均响应"的机制暴露集群中失联节点,并掌握区分"性能测试"与"存活浸泡测试"的关键界限。

一、serve_hostnames 是什么:为"集群还活着吗"而生的持续测试

serve_hostnames位于 test/soak/serve_hostnames,其源码文件为 serve_hostnames.go,配套 Makefile 与本文所述的 README.md。从源码注释可以看出它的设计初衷非常直白:

This soak tests places a specified number of pods on each node and then repeatedly sends queries to a service running on these pods.

也就是说,它不是一个功能回归测试,也不是基准测试,而是一种持续运行的健康证明:只要集群还活着、调度还正常、Service 与端点还能联通,这个测试就能反复通过;一旦某个节点或链路失效,就会立刻在日志中暴露出来。

在 Kubernetes 的测试体系中,它归属于test/soak/目录(与test/e2etest/integration等并列),代表官方认可的"长时间浸泡型"验证手段,与一次性执行的端到端测试形成互补。

二、测试工作原理:七个连续动作拆解

当配合 GCE provider 运行时,serve_hostnames会顺序执行以下动作(参见 README.md 与源码 serve_hostnames.go 的main()):

  1. 建立连接:基于当前 kubeconfig 上下文($HOME/.kube/config)与集群 master 建立连接;
  2. 枚举节点:列出集群中所有节点,假设数量为N
  3. 逐个放置 Pod:在每个节点上创建M个 Pod(默认M=1)。Pod 内运行serve_hostnames镜像——对一次GET请求会回传"自身 Pod 名"作为响应。注意:这些 Pod 是逐个单独创建的(不通过 ReplicationController/Deployment 管理),命名带有N-M后缀,标识"第N个节点上的第M个 Pod";
  4. 创建 Service:创建一个映射到这些 Pod 的 Service(selector 为name: serve-hostname,端口 9376/TCP);
  5. 并发查询:程序执行I轮(默认 1 轮),每轮经由 master 上的Service 代理接口services/proxy)发起Q×N×M次查询(Q默认 10);
  6. 结果校验:验证每个 Pod(进而每个节点)至少响应了一次查询(平均应约为Q次);
  7. 报告与重试:输出各项操作的耗时,失败操作会按超时窗口重试。

2.1 关键源码印证一:镜像与"返回 Pod 名"的机制

README 中"pod 对 GET 请求返回 pod 名"的说法,可在仓库镜像源码中得到精确印证。测试实际使用的镜像并非专门单独构建的旧镜像,而是 e2e 通用镜像agnhost(版本2.66.1,见 test/utils/image/manifest.go 中configs[Agnhost]),启动其子命令serve-hostname。真正响应 HTTP 请求的实现位于 test/images/agnhost/serve-hostname/serve_hostname.go:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { ... fmt.Fprintf(w, "%s", hostname) })

其中hostname来自os.Hostname(),在 Kubernetes 中默认即为 Pod 名。服务默认监听 9376 端口(flag "port", 9376),与 soak 测试代码中 Service 的Port/TargetPort: 9376、容器端口 9376 一一对应(见 serve_hostnames.go)。这就形成了一条验证链:

查询响应内容 = Pod 主机名 = Pod 名 → 若某 Pod 名从未出现,即说明该 Pod 或它所在节点/网络路径存在问题。

2.2 关键源码印证二:查询如何经由 Service 代理发出

源码中,构造代理请求的入口是 test/e2e/framework/service/resource.go 的GetServicesProxyRequest

func GetServicesProxyRequest(c clientset.Interface, request *restclient.Request) (*restclient.Request, error) { return request.Resource("services").SubResource("proxy"), nil }

随后 soak 测试对每个查询执行Namespace(ns).Name("serve-hostnames").DoRaw(...),即访问 REST 路径services/<ns>/serve-hostnames/proxy。这一设计意味着:请求流量真实地贯穿了 API Server → Service 代理 → 后端 Pod 端点 → 容器进程的整条链路,而不只是直连 Pod IP,因此对 kube-proxy/端点分发/网络联通性的覆盖远高于普通探测。

2.3 每轮查询量的计算

每个循环中产生的总查询数在源码中是这样计算的:

queries := *queriesAverage * len(nodes.Items) * *podsPerNode

即"每个 Pod 平均查询次数 × 节点数 × 每节点 Pod 数"。例如 4 节点 × 每节点 1 Pod ×--queries=10,一轮就是 40 次查询;若用--pods_per_node=2则一轮为 80 次(对应下面示例日志中的 40/80 queries)。

三、编译与运行

先确认当前 kubeconfig 指向目标集群且当前上下文正确(README 说明连接取自$HOME/.kube/.kubeconfig,源码实现为$HOME/.kube/config,见 serve_hostnames.go,通过 clientcmd 加载;若为 GKE 集群可用--gke_context指定gke_{project}_{zone}_{cluster-name}并使用 gcloud 的 kubeconfig)。

按 Makefile 构建并直接运行:

go build serve_hostnames.go ./serve_hostnames

四、命令行参数一览

以下参数定义于源码 serve_hostnames.go:

参数默认值含义
--queries100每一轮中每个 Pod 平均发起的 hostname 查询次数
--pods_per_node1每个节点上部署的 serve-hostname Pod 个数
--up_to1迭代轮数;设为-1表示无限循环(用于长时间浸泡)
--max_par500同一时刻在途请求的最大数量(并发上限)
--gke_context""目标 GKE 集群的 context 名称(gke_{project}_{zone}_{cluster-name}
--v0klog 日志详细级别;--v=4输出每步操作耗时与响应分布

注意:README 示例中的queries=10来自当时示例运行场景的参数(默认值历史上为 10,当前源码默认已是 100),以你实际--queries传入值为准。

另外,源码中还定义了一组内部超时常量(serve_hostnames.go),它们决定了各阶段重试与放弃的节奏,可作为运行超时预期的参考:

常量适用阶段
nodeListTimeout2 min枚举节点
serviceCreateTimeout2 min创建 Service
podCreateTimeout2 min创建单个 Pod
podStartTimeout30 min等待 Pod 进入 Running
endpointTimeout5 min等待端点传播后首次代理请求成功
deleteTimeout2 min删除 Pod / Service
namespaceDeleteTimeout5 min删除测试命名空间并等待消失

五、运行输出解读

5.1 单轮默认运行

./serve_hostnames的典型输出如下(README 原例)。它先打印启动参数与节点清单,随后创建命名空间serve-hostnames-XXXX(随机后缀,源码用GenerateName生成,结束时自动删除),创建 Service 与命名形如serve-hostname-<N>-<M>的 Pod(Pod 直接指定NodeName精确绑定节点),然后等待所有 Pod Running:

I0326 14:21:04.179893 11434 serve_hostnames.go:60] Starting serve_hostnames soak test with queries=10 and podsPerNode=1 upTo=1 I0326 14:21:04.507252 11434 serve_hostnames.go:85] Nodes found on this cluster: I0326 14:21:04.507282 11434 serve_hostnames.go:87] 0: kubernetes-node-5h4m.c.kubernetes-satnam.internal I0326 14:21:04.507297 11434 serve_hostnames.go:87] 1: kubernetes-node-9i4n.c.kubernetes-satnam.internal I0326 14:21:04.507309 11434 serve_hostnames.go:87] 2: kubernetes-node-d0yo.c.kubernetes-satnam.internal I0326 14:21:04.507320 11434 serve_hostnames.go:87] 3: kubernetes-node-jay1.c.kubernetes-satnam.internal I0326 14:21:04.507347 11434 serve_hostnames.go:95] Using namespace serve-hostnames-8145 for this test. I0326 14:21:04.507363 11434 serve_hostnames.go:98] Creating service serve-hostnames-8145/serve-hostnames I0326 14:21:04.559849 11434 serve_hostnames.go:148] Creating pod serve-hostnames-8145/serve-hostname-0-0 on node kubernetes-node-5h4m.c.kubernetes-satnam.internal I0326 14:21:04.605603 11434 serve_hostnames.go:148] Creating pod serve-hostnames-8145/serve-hostname-1-0 on node kubernetes-node-9i4n.c.kubernetes-satnam.internal I0326 14:21:04.662099 11434 serve_hostnames.go:148] Creating pod serve-hostnames-8145/serve-hostname-2-0 on node kubernetes-node-d0yo.c.kubernetes-satnam.internal I0326 14:21:04.707179 11434 serve_hostnames.go:148] Creating pod serve-hostnames-8145/serve-hostname-3-0 on node kubernetes-node-jay1.c.kubernetes-satnam.internal I0326 14:21:04.757646 11434 serve_hostnames.go:194] Waiting for the serve-hostname pods to be ready I0326 14:23:31.125188 11434 serve_hostnames.go:211] serve-hostnames-8145/serve-hostname-0-0 is running I0326 14:23:31.165984 11434 serve_hostnames.go:211] serve-hostnames-8145/serve-hostname-1-0 is running I0326 14:25:22.213751 11434 serve_hostnames.go:211] serve-hostnames-8145/serve-hostname-2-0 is running I0326 14:25:37.387257 11434 serve_hostnames.go:211] serve-hostnames-8145/serve-hostname-3-0 is running W0326 14:25:39.243813 11434 serve_hostnames.go:265] No response from pod serve-hostname-3-0 on node kubernetes-node-jay1.c.kubernetes-satnam.internal at iteration 0 I0326 14:25:39.243844 11434 serve_hostnames.go:269] Iteration 0 took 1.814483599s for 40 queries (22.04 QPS) I0326 14:25:39.243871 11434 serve_hostnames.go:182] Cleaning up pods I0326 14:25:39.434619 11434 serve_hostnames.go:130] Cleaning up service serve-hostnames-8145/server-hostnames

输出关键点

  • Pod 名后缀N-M用来标识"第N号节点上的第M个 Pod";
  • 该次运行中,位于 node 3 上的 Pod 没有响应任何查询,于是日志出现W0326 ... No response from pod serve-hostname-3-0 ...警告;
  • 每轮结束时打印Iteration N took ... for X queries (Y QPS),例如 40 次查询耗时约 1.81s、约 22 QPS。

5.2 多轮与多 Pod 场景

通过--up_to--pods_per_node提高强度:

./serve_hostnames --up_to=3 --pods_per_node=2

输出中可以看到每个节点上创建了 2 个 Pod(serve-hostname-<N>-0-<N>-1),且每轮 80 次查询。README 示例展示了一个很有意思的现象:node 2 的两个 Pod 在第 0、1 轮均未响应,但到第 2 轮恢复正常。这说明浸泡测试的价值——单次测试容易受瞬时抖动影响误判,多轮重复才能区分"瞬时故障"与"持续故障"。

5.3 无限循环:真正的 Soak 模式

要让测试无限持续运行,使用:

./serve_hostnames --up_to=-1

源码中循环条件为for iteration := 0; iteration != *upTo; iteration++,当upTo == -1时该条件永不为假,从而无限循环,直到进程被手动终止。这正是"浸泡测试"的典型用法:让它长时间跑着,集群任何时刻出问题都会以 WARNING/日志形式被记录。

5.4 详细报告模式:--v=4

./serve_hostnames --v=4

--v=4会输出两类额外信息(对应源码中klog.V(4)的埋点):

  1. 各操作耗时:如Service create ... took 58.473361msPod create ... request took 56.178906ms、每条Proxy call in namespace ... took 43.6ms等;
  2. 各 Pod 的响应分布(源码 serve_hostnames.go 中以responses[r]++统计响应计数,最后逐 Pod 打印):
I0326 14:35:05.607164 12099 serve_hostnames.go:258] serve-hostname-3-0: 12 I0326 14:35:05.607176 12099 serve_hostnames.go:258] serve-hostname-1-0: 10 I0326 14:35:05.607186 12099 serve_hostnames.go:258] serve-hostname-0-0: 18 W0326 14:35:05.607199 12099 serve_hostnames.go:265] No response from pod serve-hostname-2-0 on node kubernetes-node-d0yo.c.kubernetes-satnam.internal at iteration 0 I0326 14:35:05.607211 12099 serve_hostnames.go:269] Iteration 0 took 1.774856469s for 40 queries (22.54 QPS)

该次运行中:node 0 的 Pod 响应 18 次、node 1 的 10 次、node 3 的 12 次,而 node 2 完全没响应。总响应 40 次恰好等于一轮的 40 次查询,说明分发基本均匀(理论平均每 Pod 约 10 次,由于并发与分发随机性会有波动)。

六、错误处理与"缺失响应"的判定机制

为了在故障场景下仍能给出可诊断的日志,源码对失败查询做了特殊编码(serve_hostnames.go):查询失败时向结果通道写入一个以!开头(不可能出现在合法主机名中)的字符串,主循环统计时若响应首字符为!则计入missing。随后:

  • missing > 0时打印Missing %d responses out of %d
  • 对每个 Pod 名逐一比对响应 map,未出现则打印No response from pod ... at iteration N
  • 每轮汇总行也改为... for %d queries (%.2f QPS) with %d missing(源码中的统计口径,日志正文同样可见)。

同时,几乎所有 API 操作(节点枚举、Service 创建/删除、Pod 创建/删除、等待 Running、端点传播探测)都带有超时窗口内的自动重试,例如创建 Service 在serviceCreateTimeout内每 2 秒重试一次。整个程序也通过defer保证退出前清理 Pod、Service 与命名空间。

七、重要定位:这不是性能测试

README 明确强调:serve_hostnames不是性能测试。其目标是为"无限期运行、持续验证集群健康"提供一种简便手段——通过反复锻炼足够多的 Kubernetes 功能(节点调度、Pod 生命周期、Service 端点、API 代理链路等)来确认集群仍处于健康可用状态。

报告的 QPS 数字主要反映的是master 的延迟,原因在于所有代理请求是刻意串行(serial)发起的(即上一请求完成才发起下一个)。这一点从并发上限参数--max_par(默认 500)也能侧面印证:虽然源码用 channel 对在途请求做了节流,但它关心的不是吞吐上限,而是"每一次都能通"。

7.1 QPS 低不代表集群有问题

结合 5.1 与 5.2 节的示例:40 次查询约 22 QPS、80 次查询约 21~22 QPS,数值几乎不随规模上升。如果把它当成性能基准,会得到"集群只有 20 多 QPS"的错误结论。正确的读法是:QPS 在这里约等于 API Server 到后端 Pod 的单次往返延迟的倒数,用于观察链路延迟是否劣化,而非衡量集群吞吐能力。

八、何时使用 serve_hostnames:适用场景与局限

适用场景

  • 集群长期健康巡检:用--up_to=-1在测试/预发集群上常驻运行,配合日志采集即可形成持续的健康信号;
  • 节点失联排查:输出能精确定位"哪个节点的哪个 Pod 没有响应",适合验证节点网络、kube-proxy、Service 端点同步是否正常;
  • 变更后的回归浸泡:集群升级、网络插件调整后持续运行一段时间,比一次性 e2e 更能暴露间歇性问题。

已知局限

  • 依赖 master 上services/proxy代理路径,因此它验证的是"控制面代理链路"的健康,不是纯数据面负载均衡的基准;
  • 仅针对 GCE provider 场景描述(README 开头限定 "when used with the GCE provider"),在其它云上需要自行保证 kubeconfig 可达;
  • 创建 Pod 时显式指定NodeName(见 serve_hostnames.go),即绕过调度器直接绑定节点,因此它不覆盖调度器行为——如需覆盖调度可考虑结合 e2e 框架中的相关用例。

九、从源码理解运行全流程(一图梳理)

结合 serve_hostnames.go 的main(),可将完整生命周期归纳为如下阶段:

  1. 准备:解析 flag(queries/pods_per_node/up_to/max_par/gke_context)→ 加载 kubeconfig → 构建 clientset;
  2. 发现Nodes().List()枚举节点(带 2 min 重试),无节点则 Fatal;
  3. 基建GenerateName: "serve-hostnames-"创建随机命名空间(defer 删除)→ 重试创建 Service(9376/TCP,selectorname: serve-hostname);
  4. 放置工作负载:双层循环for i, node := range nodes.Items { for j := 0; j < *podsPerNode; j++ }逐个创建serve-hostname-<i>-<j>Pod,镜像agnhost,容器端口 9376,NodeName固定;
  5. 就绪等待:轮询 Pod 直至Phase == Running(上限 30 min);
  6. 端点等待:通过代理发起首个请求,直到成功或非StatusFailure(上限 5 min),确保 Service 端点已传播;
  7. 浸泡循环:每轮queries次并发代理查询(max_par节流)→ 统计响应 map 与 missing → 输出缺失 Pod 警告与 QPS 汇总 →--up_to=-1时无限重复;
  8. 清理:defer 链删除全部 Pod、Service,删除命名空间并等待其消失(上限 5 min)。

十、延伸阅读

  • 测试主体源码:serve_hostnames.go
  • 构建入口:Makefile
  • 响应后端镜像实现:test/images/agnhost/serve-hostname/serve_hostname.go(HTTP/TCP/UDP 三种模式、--port默认 9376)
  • 代理请求构造工具:test/e2e/framework/service/resource.go
  • 镜像版本配置:test/utils/image/manifest.go
  • 本目录 README:test/soak/serve_hostnames/README.md

若需要了解 Kubernetes e2e 测试体系中与之互补的一次性功能测试,可参考仓库 test/e2e 与 test/e2e_kubeadm 目录的框架文档;若关心测试镜像的构建与使用约束,可阅读 test/images/agnhost/README.md。

【免费下载链接】kubernetesProduction-Grade Container Scheduling and Management项目地址: https://gitcode.com/GitHub_Trending/kuber/kubernetes

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询