综合 K8s 1.35.10 修了 namespaceSelector 负算子:被排除命名空间里的 Pod 不再参与打分

2026-11-03 21:03:24

Kubernetes 1.35.10 修了 kube-scheduler 的 namespaceSelector 负算子:被排除命名空间里的 Pod 不再参与过滤打分

变更内容

Kubernetes CHANGELOG-1.35.md 的 v1.35.10 段里有一条调度器修复:

Fixed kube-scheduler evaluation of pod affinity and anti-affinity namespace selectors using negative operators, so pods in excluded namespaces no longer affect node filtering or scoring. (#142050, @rahulbabu95) [SIG Scheduling and Testing]

也就是说:pod affinity / anti-affinity 的 namespaceSelector 在使用 NotIn、DoesNotExist 这类负算子时,本该被排除的命名空间里的 Pod 仍然参与了节点过滤与打分。1.35.10 修复后,这些 Pod 不再影响调度结果。

这是一次行为变更,不只是崩溃修复——升级后同一份配置可能落到不同节点上。

怎么判断自己踩了这个 bug

三个可观察的信号:

  1. 写了 namespaceSelector 想排除某些命名空间,反亲和看起来没生效。 典型情形是用 requiredDuringSchedulingIgnoredDuringExecution + topologyKey: kubernetes.io/hostname,期望某个命名空间里的 Pod 不和自己抢同一台机器,结果仍然被判定冲突,Pod 一直 FailedScheduling。
  2. 节点打分与预期不符。 用软反亲和(preferredDuringSchedulingIgnoredDuringExecution)时不会报调度失败,只是落点偏离预期:某些节点被系统性压低分,查下来那些节点上跑的都是"应该被排除的命名空间"里的 Pod。
  3. 同一份配置在 1.35.10 前后调度结果不同。 升级后 Pod 落点发生变化,而 manifest 一个字没改,基本可以指向这条修复。

注意失效范围只涉及负算子。In、Exists 这类正向匹配的 namespaceSelector 不受影响,看到反亲和"正常"不代表配置写法没问题,只代表没用到负算子路径。

一份会失效的配置

apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: prod
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostname
namespaceSelector:
matchExpressions:
- key: env
operator: NotIn
values:
- sandbox

意图是:app=web 的 Pod 不与自己同节点,但 env=sandbox 的命名空间里那些 app=web 的 Pod 不算数。

失效原因在于调度器求值 namespaceSelector 的负算子时没有正确把命名空间排除掉:sandbox 命名空间里的 app=web Pod 依然进入亲和/反亲和的计算集合,被当成需要避让的对象。于是 prod 的三个副本被错误地判定为互相冲突,节点过滤阶段就失败,或者软反亲和场景下把分数带偏。

同一个字段在 1.35.10 之后开始真正生效,配置本身不需要改。

核对实际生效的命名空间集合

namespaceSelector 匹配的是 Namespace 对象的 label,不是 Pod 的 label,也不是 Pod 所在的业务标签。所以第一件事是把命名空间标签打出来:

kubectl get ns --show-labels

再确认选择器视角下的集合与预期一致:

# 等价于 NotIn:[env] 里不含 sandbox
kubectl get ns -l 'env notin (sandbox)' --show-labels

# 等价于 DoesNotExist:完全没有 env 这个 label
kubectl get ns -l '!env' --show-labels

# 单独确认被排除的命名空间确实带了 label
kubectl get ns sandbox --show-labels

最容易出问题的是 DoesNotExist。它判断的是"命名空间对象上这个 label 键缺失",不是"命名空间不存在"。两者语义完全不同:

  • 命名空间存在但没有该 label → DoesNotExist 匹配,会被纳入(或排除,取决于你把它写在哪个字段)。
  • 命名空间根本不存在 → 它不会出现在任何匹配结果里,DoesNotExist 也不会因为"不存在"而命中它。

所以如果排除逻辑写的是 DoesNotExist,而目标命名空间恰好有很多没打 label 的,匹配集合会比想象的大得多;反过来,如果以为"没有 label 的命名空间会被排除",实际是"没有 label 的都会被选中"。这类写法在 1.35.10 前后表现不一致,很容易被误判成 bug 复发。

升级后要观察落点

修复改变的是调度决策,不是对象状态,滚动升级不会自动把已有 Pod 重新调度。需要在升级后关注:

kubectl get pod -o wide -n prod
kubectl describe pod
-n prod    # 看 Events 里的 FailedScheduling 与调度结果
  • 原本因"假冲突"卡在 Pending 的 Pod,升级后可能立刻能调度成功,副本数补齐,这是修复生效的正常表现。
  • 原本能调度但落点集中的,重启或扩缩容后会出现新的分布。
  • 如果有依赖"错误避让"形成的拓扑分布(比如靠 bug 意外凑出的打散效果),升级后分布会变,需要在发布窗口里观察而不是等告警。

已运行的 Pod 不会因为升级调度器而迁移,落点变化只发生在新建、重建、扩缩容和驱逐之后。

同段其它条目(背景)

  • kube-apiserver 现在接受最多 16384 个 request header value,避免 Go 1.27 的 500 默认值拒绝带 500 个以上 group 的 impersonated/proxied 用户并返回 431。(#142772)
  • 修复 Pod .status.resourceClaimStatuses 在多 claim 场景下在部分列表之间抖动。(#142023)
  • 修复 DRA PrepareResources 部分失败重试时给 CRI 传重复 CDI device ID 导致容器启动失败。(#140954)
  • 1.35 已移除 cgroup v1 支持(failCgroupV1 默认 true);kube-proxy 的 ipvs 模式弃用;containerd 1.x 是最后一个受支持版本。
  • Kubernetes 1.35 使用 Go 1.26 构建。
复制全文 生成海报 Kubernetes 调度 排障

推荐文章

程序员茄子在线接单