JobSet 问题排查:解决 “follower pod node selector not set” 报错

JobSet 问题排查:解决 “follower pod node selector not set” 报错

在使用 JobSet 运行 multi-slice TPU 或 GPU 分布式训练时,JobSet 的 leader-follower 机制会先调度 leader Pod,再通过 mutating webhook 读取 leader 所在节点上的 topology label,并把对应的 nodeSelector patch 到 follower Pod 上,以确保同一组 Pod 被调度到同一个拓扑域。这个设计在正常情况下运行良好,但如果底层加速器资源不足,leader Pod 一直卡在 Pending 无法调度,你就可能看到这样一条让人困惑的错误:

admission webhook "vpod.kb.io" denied the request: follower pod node selector not set

直觉上,你可能会先去检查 follower Pod 的 node selector 配置,但真正的问题并不在那里。经过数小时排查后,我发现这条报错其实是一种诊断假象。它掩盖了真正的根因,让你以为是 JobSet 配置写错了,而实际情况很可能只是基础设施容量不足,导致 leader Pod 根本调度不起来。之所以会出现这条误导性报错,是因为 pkg/webhooks/pod_webhook.go 中的 validating webhook 在验证顺序上有一个很微妙的问题:它先检查 follower 的 NodeSelector,却没有先确认 leader 是否已经完成调度。

在定位到这个问题后,我开了 Issue #1187,并提交了两个 PR,从不同层面进行修复。本文会结合这次真实的排障经历,拆解 JobSet 的 leader-follower 调度机制、mutating 与 validating webhook 的交互关系,以及为什么只要调整一下验证顺序,就能把误导性的报错变成真正有帮助的诊断信息。

本文中的代码引用基于 JobSet main 分支在 PR #1159 重构之后的实现。该 PR 将原先分散在 pod_mutating_webhook.gopod_admission_webhook.go 中的逻辑合并为单一的 pod_webhook.go,并采用 controller-runtime 的 admission.Defaulteradmission.Validator 接口。在 release-0.11 及更早版本中,相同逻辑仍分布在两个文件里,但验证顺序的问题是一样的。

JobSet Leader-Follower 机制概览

当 JobSet 处理 TPU multi-slice 或任何需要 exclusive placement 的工作负载时,会采用 leader-follower 调度模型。核心逻辑很简单:先调度 leader Pod(completion index 0),然后在 follower Pod 被创建并进入 admission 流程时,由 mutating webhook 读取 leader 所在节点的 topology label,再把对应的 nodeSelector patch 到 follower Pod 上,从而保证 follower 会被调度到同一个 topology domain。

整个流程依赖以下两个 webhook 的协同工作。

Mutating Webhook:负责 Patch Node Selector

JobSet 的 mutating webhook(mpod.kb.io)会拦截每一个 Pod 创建请求。对于使用 exclusive placement 的 follower Pod,它会调用 setNodeSelector

  1. 首先通过 leaderPodForFollower 找到对应的 leader Pod,然后检查 leader 的 Spec.NodeName 是否已经被设置。
  2. 如果 leader 已经调度完成,webhook 就会调用 topologyFromPod 获取 leader 所在节点上的 topology label,例如 node pool 名称,并把这个值 patch 到 follower Pod 的 nodeSelector 中。
  3. 但如果 leader 还处于 Pending,也就是 Spec.NodeName 为空,mutating webhook 就会直接返回 nil,不做任何 patch。代码注释也明确说明了:拒绝 follower 的责任交给 validating webhook。
func (p *podWebhook) setNodeSelector(ctx context.Context, pod *corev1.Pod) error {
	log := ctrl.LoggerFrom(ctx)
	// Find leader pod (completion index 0) for this job.
	leaderPod, err := p.leaderPodForFollower(ctx, pod)
	if err != nil {
		log.Error(err, "finding leader pod for follower")
		// Return no error, validation webhook will reject creation of this follower pod.
		return nil
	}

	// If leader pod is not scheduled yet, return error to retry pod creation until leader is scheduled.
	if leaderPod.Spec.NodeName == "" {
		// Return no error, validation webhook will reject creation of this follower pod.
		return nil
	}

	// Get the exclusive topology value for the leader pod (i.e. name of nodepool, rack, etc.)
	topologyKey, ok := pod.Annotations[jobset.ExclusiveKey]
	if !ok {
		return fmt.Errorf("pod missing annotation: %s", jobset.ExclusiveKey)
	}
	topologyValue, err := p.topologyFromPod(ctx, leaderPod, topologyKey)
	if err != nil {
		log.Error(err, "getting topology from leader pod")
		return err
	}

	// Set node selector of follower pod so it's scheduled on the same topology as the leader.
	if pod.Spec.NodeSelector == nil {
		pod.Spec.NodeSelector = make(map[string]string)
	}
	log.V(2).Info(fmt.Sprintf("setting nodeSelector %s: %s to follow leader pod %s", topologyKey, topologyValue, leaderPod.Name))
	pod.Spec.NodeSelector[topologyKey] = topologyValue
	return nil
}

这种行为本身是符合预期的。因为 leader 还没调度到具体节点时,JobSet 就无法知道对应的 node topology label,自然也没有任何值可供 patch。

Validating Webhook:负责校验 Follower Pod 是否合法

在 mutating webhook 执行完之后,vpod.kb.io validating webhook 会继续执行 ValidateCreate。在 pkg/webhooks/pod_webhook.go 里,这段验证流程包含两个关键检查:

  1. NodeSelector 检查:确认 follower Pod 是否已经带有 node selector
  2. Leader 调度状态检查:确认 leader Pod 是否已经完成调度
// ValidateCreate validates that follower pods (job completion index != 0) part of a JobSet using exclusive
// placement are only admitted after the leader pod (job completion index == 0) has been scheduled.
func (p *podWebhook) ValidateCreate(ctx context.Context, pod *corev1.Pod) (admission.Warnings, error) {
	...
	// Do not validate anything else for leader pods, proceed with creation immediately.
	if placement.IsLeaderPod(pod) {
		return nil, nil
	}
	// If a follower pod node selector has not been set, reject the creation.
	if pod.Spec.NodeSelector == nil {
		return nil, fmt.Errorf("follower pod node selector not set")
	}
	if _, exists := pod.Spec.NodeSelector[topologyKey]; !exists {
		return nil, fmt.Errorf("follower pod node selector for topology domain not found. missing selector: %s", topologyKey)
	}
	// For follower pods, validate leader pod exists and is scheduled.
	leaderScheduled, err := p.leaderPodScheduled(ctx, pod)
	if err != nil {
		return nil, err
	}
	if !leaderScheduled {
		return nil, fmt.Errorf("leader pod not yet scheduled, not creating follower pod. this is an expected, transient error")
	}
	return nil, nil
}

问题的关键就在于这两个检查的顺序。当 validating webhook 先检查 NodeSelector,再去检查 leader 是否已调度时,最终返回的错误信息就不再指向真正的原因。更有诊断价值的 transient error 会被完全遮蔽,用户只能看到一条很误导的失败信息。

验证顺序遮蔽了真正的错误

问题现象

我在 GKE 上运行一个 multi-slice TPU 训练任务时,所有 follower Pod 都被拒绝,报错如下:

admission webhook "vpod.kb.io" denied the request: follower pod node selector not set

与此同时,在 webhook server 的日志里,mutating webhook 有时还会输出另一条信息量更高的报错。当 leader Pod 甚至还没创建出来时,leaderPodForFollower 会返回一个 error,并记录成:

ERROR  admission  finding leader pod for follower
{"error": "expected 1 leader pod (example-job-0-0), but got 0. this is an expected, transient error"}

这两条信息的差别很关键。大多数用户在事件或报错输出里只会看到 validating webhook 返回的第一条,于是自然会怀疑是 follower Pod 配置有问题。而第二条来自 mutating webhook 的信息,才是真正对排障有帮助的线索。在权限更严格的环境里,详细的 mutating webhook 日志往往只有 cluster admin 才能看到,因此普通用户更容易被 admission 错误所误导。

根因:验证顺序有问题

当 leader Pod 因为集群里没有足够的加速器容量而卡在 Pending 时,整个流程实际上是这样的:

  1. 创建 follower Pod,进入 admission 流程。
  2. mutating webhook 的 setNodeSelector 发现 leaderPod.Spec.NodeName 为空,于是直接返回 nil,不会执行 patch。拒绝这个 follower 的责任被刻意留给 validating webhook。
  3. follower Pod 进入 validating webhook,也就是 ValidateCreate
  4. validating webhook 先检查 NodeSelector,发现它不存在,于是立刻拒绝,并返回 follower pod node selector not set
  5. 用于检查 leader 是否已完成调度的逻辑根本没有机会执行,因为在第 4 步就已经提前返回了。

这就导致最终看到的错误,只指向最末端的症状,而不是根因:

基础设施容量不足(TPU/GPU 资源不够)
└── Leader pod 一直 Pending(未完成调度)
    └── Mutating webhook 正确跳过 patch(符合预期)
        └── Follower pod 创建时没有写入 node selector
            └── Validating webhook 先检查 NodeSelector 并直接拒绝
                ├── 用户看到:"follower pod node selector not set"
                └── 真正有价值的 transient error 被完全遮蔽 ✘

实际上,JobSet 内部已经准备好了一条设计合理的 transient error,只是因为验证顺序不对,永远不会显示出来。如果把顺序反过来,先检查 leader 是否已调度,用户看到的就会是:

leader pod not yet scheduled, not creating follower pod. this is an expected, transient error

这条信息会直接引导你去看 leader Pod 的调度状态,而不是把时间浪费在 follower 配置上。

下图展示了三种情况:正常流程中 leader 先完成调度;当前实现中由于验证顺序问题,真正有用的错误被遮蔽;以及 PR 修正后的流程中,调整顺序后 transient error 能被正确暴露。

如何排查这个问题

如果你也遇到这个错误,关键点很简单:不要被错误信息带偏,先检查上游的 leader Pod 状态。

首先确认 leader Pod 是否存在,以及是否已经被调度:

kubectl get pods -n <namespace> | grep <jobset-name>.*-0-0

如果 leader Pod 仍然是 Pending,继续用 describe 查看具体原因:

kubectl describe pod <leader-pod-name> -n <namespace>

通常你会看到类似下面这样的事件:

Warning  FailedScheduling  0/10 nodes are available: insufficient google.com/tpu resources

到这里,根因就很清楚了。问题不在 follower Pod 的配置,而是集群没有足够的基础设施容量,导致 leader 根本调度不起来。解决方法也很直接:确保集群里有足够的加速器资源供 leader Pod 使用。只要 leader 调度成功,mutating webhook 就会自动把正确的 node selector patch 到 follower Pod 上,后续 validating webhook 也会顺利通过。

提交 PR:从文档到代码的双重修复

找到真正根因后,我开了 Issue #11871,并提交了两个 PR,从不同层面解决这个问题:

  • PR #11892(文档改进):在官方 Troubleshooting Guide 中补充排查建议,已经合并。
  • PR #11883(代码修复):调整 validating webhook 的验证顺序,让更有帮助的 transient error 可以正确显示。

PR #1189:补充 Troubleshooting 文档

第一个 PR 是文档层面的改进。它在官方 JobSet Troubleshooting Guide 中新增了一节4,明确说明当你看到 follower pod node selector not set 时,首先应该检查的是 leader Pod 的调度状态,而不是直接去改 follower 的配置。

PR #1188:调整验证顺序

第二个 PR 才是真正从代码层面修复问题。在 pkg/webhooks/pod_webhook.go 中,它调整了 ValidateCreate 内部的检查顺序。等这个修复合并之后,当 leader Pod 尚未调度时,用户将看到:

leader pod not yet scheduled, not creating follower pod. this is an expected, transient error

而不是原本那条误导性的 follower pod node selector not set

结语

在分布式系统里,这种“因果链很长”的 bug 很常见。而在 Kubernetes 生态中,mutating webhook 与 validating webhook 的执行顺序和交互方式,更容易让问题表象与真实根因脱节。这个案例说明了为什么理解这套交互机制很重要,也说明了为什么一个很小的验证顺序调整,就能显著提升排障效率和诊断准确性。

参考资料

Eason Cao
Eason Cao Eason is an engineer working at FANNG and living in Europe. He was accredited as AWS Professional Solution Architect, AWS Professional DevOps Engineer and CNCF Certified Kubernetes Administrator. He started his Kubernetes journey in 2017 and enjoys solving real-world business problems.
comments powered by Disqus