Kubernetes 1.31 云驱动树内集成移除:三个真实报错与迁移清单
升级到 Kubernetes 1.31 之后,很多人的集群在重启节点时突然卡住:节点一直 NotReady,Pod 停在 Pending,调度器事件里只有一行 node(s) had untolerated taint。看起来像调度器坏了,其实是 1.31 做了一件影响面很大的事:把云厂商集成的全部树内实现从核心代码里删除。
这不是小改动。Kubernetes 官方在 2024 年 7 月 19 日发布的 v1.31 变更预告里,用了整整一节讲「Removal of all in-tree integrations with cloud providers」,并明确写道这是 KEP-2395 外部化进程的终点——而这个进程从 v1.26 就开始了。v1.31.0 本身于 2024 年 8 月 13 日发布。
先说结论,这篇要帮你搞清四件事:
- 报错一:
unknown cloud provider "aws"究竟从哪来,为什么 1.31 之后你传任何厂商名都失败; - 报错二:
no volume plugin matched name: kubernetes.io/rbd,即 1.31 连带删掉的 CephFS/RBD 树内卷插件; - 报错三:
node(s) had untolerated taint {...},以及为什么官方博客自己给的 CCM 示例在 1.31 上就跑不起来; - 一套可以直接复制执行的迁移命令和 toleration 配置写法。
本文的所有报错原文都取自 Kubernetes v1.31 分支的源码字面量(可逐条回溯到具体文件与行),不是凭印象转述的二手经验。本机没有可用的 Kubernetes 集群,因此我没有声称「实测复现了集群故障」,而是做了一件更可控的事:把上游的污点/容忍匹配逻辑逐行复刻成脚本,用官方文档与官方博客里的原始 YAML 跑对拍。脚本在文末,读者可以自己跑一遍验证每一条结论。
TL;DR:
- 1.31 起
--cloud-provider只接受空字符串或external,其他任何厂商名都会报unknown cloud provider "xxx"; - 同版本还删掉了
kubernetes.io/rbd与kubernetes.io/cephfs两个树内卷插件,老 StorageClass 会报no volume plugin matched name: ...; - 官方文档的 CCM 示例(
ccm-example.yaml)是正确的,但官方 2025-02-14 博客里的 Deployment 示例漏了node-role.kubernetes.io/control-plane,在 kubeadm 1.31 集群上会卡住; - 写 toleration 时
operator留空等于Equal,此时value必须精确写成"true",否则不生效——这是最容易踩的一个坑。
一、1.31 到底删掉了什么
官方 v1.31 变更预告的措辞很直接:树内支持「will be removed as part of the v1.31 release」,这标志着 KEP-2395 描述的「所有云厂商集成从核心外部化」流程完成。
这个结论不只停留在博客文本上。我用 GitHub API 直接列了两个分支的目录做对照:
# 对比 1.30 与 1.31 的卷插件目录
curl -s "https://api.github.com/repos/kubernetes/kubernetes/contents/pkg/volume?ref=release-1.30" \
| grep -o '"name": "[^"]*"'
curl -s "https://api.github.com/repos/kubernetes/kubernetes/contents/pkg/volume?ref=release-1.31" \
| grep -o '"name": "[^"]*"'
release-1.30 的输出里有 cephfs、rbd、iscsi、portworx、flexvolume 等条目;release-1.31 的输出只剩 csimigration、fc、hostpath、nfs、util、validation 这些。rbd 和 cephfs 两个目录整体消失了。
云厂商注册表也一样:
# 1.30 存在,1.31 返回 404
curl -s -o /dev/null -w "%{http_code}\n" \
"https://api.github.com/repos/kubernetes/kubernetes/contents/pkg/cloudprovider/providers?ref=release-1.30"
curl -s -o /dev/null -w "%{http_code}\n" \
"https://api.github.com/repos/kubernetes/kubernetes/contents/pkg/cloudprovider/providers?ref=release-1.31"
有意思的是,1.30 的那个目录里其实早就只剩一个 GCE 导入了:
//go:build !providerless
package cloudprovider
import (
// Cloud providers
_ "k8s.io/legacy-cloud-providers/gce"
)
AWS、Azure、OpenStack、vSphere 早就迁到各自的独立仓库了,1.31 只是把最后这层尾巴也剪掉。
还有一个容易被忽略的开关。DisableCloudProviders 这个特性门控在 1.31 转 GA 并锁定默认值:
// pkg/features/kube_features.go (release-1.31)
// owner: @andrewsykim
// alpha: v1.22
// beta: v1.29
// GA: v1.31
DisableCloudProviders featuregate.Feature = "DisableCloudProviders"
// ...
DisableCloudProviders: {Default: true, PreRelease: featuregate.GA, LockToDefault: true},
官方特性门控文档里的中文说明把后果讲得很清楚:
> 启用此特性门控会停用 kube-apiserver、kube-controller-manager 和 kubelet 中与 --cloud-provider 命令行参数相关的功能。
>
> 在 Kubernetes v1.31 及更高版本中,--cloud-provider 的唯一有效值是空字符串(没有云提供商集成)或"external"(通过单独的 cloud-controller-manager 进行集成)。
LockToDefault: true 意味着这个门控在 1.31 里不能再关。所以「先把 flag 改回去,滚回来再慢慢迁移」这条路,在 1.31 上是不存在的。
顺带一提,1.31 同批移除的还有:kubelet 的 --keep-terminated-pod-volumes(2017 年就 deprecated 了,PR #122082)、CephFS 卷插件、Ceph RBD 卷插件及其 CSI 迁移支持,以及 kube-scheduler 里 AzureDiskLimits/CinderLimits/EBSLimits/GCEPDLimits 四个非 CSI 限额插件(改用 NodeVolumeLimits)。
💣 踩坑实录:三个真实报错与修法
报错一:unknown cloud provider "aws"
现象:节点上的 kubelet 或控制面组件启动即失败,日志里是:
unknown cloud provider "aws"
原因:这个报错来自 v1.31 的 staging/src/k8s.io/cloud-provider/plugins.go,在 InitCloudProvider 函数里:
func InitCloudProvider(name string, configFilePath string) (Interface, error) {
var cloud Interface
var err error
if name == "" {
return nil, nil
}
if IsExternal(name) {
klog.Info("External cloud provider specified")
return nil, nil
}
// ... GetCloudProvider(name, config) ...
if err != nil {
return nil, fmt.Errorf("could not init cloud provider %q: %v", name, err)
}
if cloud == nil {
return nil, fmt.Errorf("unknown cloud provider %q", name)
}
return cloud, nil
}
逻辑链条很清楚:名字非空 → 不是 external → 去 providers map 里查 → 1.31 里这个 map 已经没有厂商条目了(注册表目录已被删除)→ GetCloudProvider 返回 nil, nil → 走到 cloud == nil 分支 → 抛出 unknown cloud provider %q。
同文件里 IsExternal 的定义也说明了 在这份代码里,非空取值只认 external:
const externalCloudProvider = "external"
// Detects if the string is an external cloud provider
func IsExternal(name string) bool {
return name == externalCloudProvider
}
解决方案:把厂商名换掉。
# 1. 先看清当前所有组件的 --cloud-provider 取值
grep -rn "cloud-provider" /etc/kubernetes/manifests/ /var/lib/kubelet/config.yaml 2>/dev/null
# 2. 改成 external(厂商信息交给独立的 cloud-controller-manager)
# kubeadm 管理的集群:写进 ClusterConfiguration
如果是 kubeadm 集群,1.31 的 kubeadm 配置 API 里已经没有 cloudProvider 字段了(v1beta3 和 v1beta4 的 ClusterConfiguration 结构体里都查不到这个名字),所以正确做法是让 kubeadm 默认走 external,然后单独部署对应厂商的 CCM。
报错二:no volume plugin matched name: kubernetes.io/rbd
现象:Pod 卡在 ContainerCreating,事件里是:
no volume plugin matched name: kubernetes.io/rbd
原因:来自 v1.31 的 pkg/volume/plugins.go,FindPluginByName 函数:
// FindPluginByName fetches a plugin by name. If no plugin is found, returns error.
func (pm *VolumePluginMgr) FindPluginByName(name string) (VolumePlugin, error) {
pm.mutex.RLock()
defer pm.mutex.RUnlock()
var match VolumePlugin
if v, found := pm.plugins[name]; found {
match = v
}
pm.refreshProbedPlugins()
if plugin, found := pm.probedPlugins[name]; found {
if match != nil {
return nil, fmt.Errorf("multiple volume plugins matched: %s and %s", match.GetPluginName(), plugin.GetPluginName())
}
match = plugin
}
if match == nil {
return nil, fmt.Errorf("no volume plugin matched name: %s", name)
}
return match, nil
}
kubernetes.io/rbd 和 kubernetes.io/cephfs 这两个名字,在 1.30 的源码里是硬编码的常量:
// release-1.30 pkg/volume/rbd/rbd.go
const (
rbdPluginName = "kubernetes.io/rbd"
// ...
)
// release-1.30 pkg/volume/cephfs/cephfs.go
const (
cephfsPluginName = "kubernetes.io/cephfs"
)
1.31 把这两个插件连同它们的 CSI 迁移支持一起删了,所以挂在上面的老 StorageClass 再也找不到对应的插件实现。
解决方案:改用 CSI 驱动。Ceph 官方维护的 ceph-csi 同时提供 RBD 与 CephFS 两个驱动。
# 1. 列出还在用树内 provisioner 的 StorageClass
kubectl get storageclass \
-o custom-columns='NAME:.metadata.name,PROVISIONER:.provisioner' \
| grep -E "kubernetes.io/(rbd|cephfs)"
# 2. 确认 CSI 驱动已装好(ceph-csi 官方提供 rbd.csi.ceph.com 与 cephfs.csi.ceph.com)
kubectl get csidrivers
# 3. 迁移完成后,旧 StorageClass 不再被引用即可清理
官方对这一步的措辞是「making the rbd volume type non-functional」,并建议直接重新部署应用到新驱动上。
报错三:node(s) had untolerated taint,以及官方示例自己跑不通
这是三个报错里最值得展开的一个,因为它同时牵扯到污点取值、容忍匹配规则,以及官方示例的一个真实缺陷。
现象:调度器事件里出现:
0/3 nodes are available: 3 node(s) had untolerated taint {node.cloudprovider.kubernetes.io/uninitialized: true}. preemption: ...
原因:报错格式串来自 v1.31 的 pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go,Filter 函数:
func (pl *TaintToleration) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
node := nodeInfo.Node()
taint, isUntolerated := v1helper.FindMatchingUntoleratedTaint(node.Spec.Taints, pod.Spec.Tolerations, helper.DoNotScheduleTaintsFilterFunc())
if !isUntolerated {
return nil
}
errReason := fmt.Sprintf("node(s) had untolerated taint {%s: %s}", taint.Key, taint.Value)
return framework.NewStatus(framework.UnschedulableAndUnresolvable, errReason)
}
要让这个报错不出现,toleration 必须命中污点。而 uninitialized 污点的真实取值写在 v1.31 的 pkg/kubelet/kubelet_node_status.go 的 initialNode() 里:
if kl.externalCloudProvider {
taint := v1.Taint{
Key: cloudproviderapi.TaintExternalCloudProvider,
Value: "true",
Effect: v1.TaintEffectNoSchedule,
}
nodeTaints = append(nodeTaints, taint)
}
常量定义在 staging/src/k8s.io/cloud-provider/api/well_known_taints.go:
const (
// TaintExternalCloudProvider sets this taint on a node to mark it as unusable,
// when kubelet is started with the "external" cloud provider, until a controller
// from the cloud-controller-manager intitializes this node, and then removes
// the taint
TaintExternalCloudProvider = "node.cloudprovider.kubernetes.io/uninitialized"
)
关键点:Value 是 "true",不是空字符串。 这决定了 toleration 怎么写。
匹配规则在 staging/src/k8s.io/api/core/v1/toleration.go 的 ToleratesTaint,注释写得很清楚:
// ToleratesTaint checks if the toleration tolerates the taint.
// The matching follows the rules below:
//
// 1. Empty toleration.effect means to match all taint effects,
// otherwise taint effect must equal to toleration.effect.
// 2. If toleration.operator is 'Exists', it means to match all taint values.
// 3. Empty toleration.key means to match all taint keys.
// If toleration.key is empty, toleration.operator must be 'Exists';
// this combination means to match all taint values and all taint keys.
func (t *Toleration) ToleratesTaint(taint *Taint) bool {
if len(t.Effect) > 0 && t.Effect != taint.Effect {
return false
}
if len(t.Key) > 0 && t.Key != taint.Key {
return false
}
// TODO: Use proper defaulting when Toleration becomes a field of PodSpec
switch t.Operator {
// empty operator means Equal
case "", TolerationOpEqual:
return t.Value == taint.Value
case TolerationOpExists:
return true
default:
return false
}
}
// empty operator means Equal 这行注释是很多人踩坑的根源。operator 留空不是「宽松匹配」,而是精确等值匹配。 于是:
| 写法 | 是否命中 uninitialized 污点 | 原因 |
|---|---|---|
key + effect,无 operator、无 value | ❌ 不命中 | operator=Equal,比对 "" == "true" 失败 |
key + value: "true",无 operator | ✅ 命中 | operator=Equal,比对 "true" == "true" 成功 |
key + operator: Exists | ✅ 命中 | Exists 忽略 value |
key + operator: Equal + value: "true" | ✅ 命中 | 显式等价于留空 |
官方博客的示例在 1.31 上确实跑不起来
我把上面这套逻辑逐行复刻成脚本,用官方原始 YAML 跑了一遍(脚本见文末,5 组断言全过)。结果有点出乎意料:
官方文档的示例是对的。 kubernetes.io/docs/tasks/administer-cluster/running-cloud-controller/ 里的 ccm-example.yaml 三条 toleration 全部命中,其中一条正是 value: "true" 加留空 operator——它能工作正是因为污点 value 恰好也是 "true"。
但官方 2025-02-14 博客《The Cloud Controller Manager Chicken and Egg Problem》里的 Deployment 示例是坏的。 它的五条 toleration 是:
- effect: NoSchedule
- effect: NoExecute
- effect: NoExecute
- effect: NoSchedule
- effect: NoSchedule
tolerations:
key: node-role.kubernetes.io/master
operator: Exists
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 120
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 120
key: node.cloudprovider.kubernetes.io/uninitialized
operator: Exists
key: node.kubernetes.io/not-ready
operator: Exists
它只容忍了 node-role.kubernetes.io/master。但 master 污点在 Kubernetes 1.25 就已经被移除了,1.31 的 kubeadm 施加的是 node-role.kubernetes.io/control-plane。
这个 1.25 的时间点有上游一手证据:kubeadm 1.25 的 CHANGELOG 写明「Kubeadm: stop applying the node-role.kubernetes.io/master:NoSchedule taint to control plane nodes for new clusters. Remove the taint from existing control plane nodes during "kubeadm upgrade apply"」(PR #110095,@neolit123,2022-05-18 合入,milestone v1.25,PR 标题为 kubeadm: cleanup the "master" taint on CP nodes during upgrade)。
而 1.31 施加的到底是哪个污点,证据在 v1.31 的 cmd/kubeadm/app/constants/constants.go:
const (
// LabelNodeRoleControlPlane specifies that a node hosts control-plane components
LabelNodeRoleControlPlane = "node-role.kubernetes.io/control-plane"
)
var (
// ControlPlaneTaint is the taint to apply on the PodSpec for being able to run that Pod on the control-plane
ControlPlaneTaint = v1.Taint{
Key: LabelNodeRoleControlPlane,
Effect: v1.TaintEffectNoSchedule,
}
// ...
)
markcontrolplane 的单元测试里也留着这个补丁的预期值:
expectedPatch: `{"spec":{"taints":[{"effect":"NoSchedule","key":"node-role.kubernetes.io/control-plane"}]}}`,
而那个博客示例的 nodeSelector 恰恰是 node-role.kubernetes.io/control-plane: ""——只往控制面节点上调度,却没容忍控制面污点,于是 CCM Pod 永远 Pending。复刻脚本算出来的报错正是:
node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }
这正是博客标题里说的 chicken-and-egg 问题的一个真实入口:CCM 起不来 → uninitialized 污点没人清除 → 节点永远调度不了工作负载。
顺带说一句上游其实早就意识到这类语义切换。上游 PR #112821《Avoid tainting with NoSchedule when DisableCloudProviders feature is on》在 2022 年 10 月就合入 1.26,正文里写:
> when we switch on the flag above and when cloud-provider is set to external we add taints "node.cloudprovider.kubernetes.io/uninitialized" with effect: NoSchedule. since in these jobs, we do NOT have an external cloud provider running like we have in this job, there is no one to clear these taints
也就是说,「开了门控之后就不该再依赖这个污点」是上游的设计意图。1.31 把门控锁死后,更不该把集群能否启动押在这个污点上。
修复:一份能直接用的 toleration
综合官方文档的正确写法和上面的匹配规则,建议这样配(注意 control-plane 必须有,master 可以留作兼容):
- key: node.cloudprovider.kubernetes.io/uninitialized
- key: node-role.kubernetes.io/control-plane
- key: node-role.kubernetes.io/master
- key: node.kubernetes.io/not-ready
- key: node.kubernetes.io/unreachable
tolerations:
# 关键:operator 留空 = Equal,所以 value 必须精确写 "true"
value: "true"
effect: NoSchedule
# 控制面污点:kubeadm 1.31 用的是 control-plane
operator: Exists
effect: NoSchedule
# 旧集群兼容,1.25 之后已不再施加
operator: Exists
effect: NoSchedule
# not-ready / unreachable 是 NoExecute 污点,不影响调度判定,但要容忍才不被驱逐
operator: Exists
effect: NoExecute
tolerationSeconds: 120
operator: Exists
effect: NoExecute
tolerationSeconds: 120
另外三点来自官方博客的实操建议:
1. 用 hostNetwork: true。官方原文解释:CCM 需要访问云厂商的 API endpoint,走 host 网络可以「remove the dependency on the networking plugin」,同时绕开 CNI 没起来就访问不到云 API 的死锁。
2. 开 leader election。官方原文:「you must remember to enable leader election, or else your controllers will collide with each other which could lead to nodes not being initialized in the cluster.」
3. 用 nodeSelector / affinity 把 CCM 钉在控制面,并加 podAntiAffinity 避免全挤在一台机器上。
三、迁移清单(可复制执行)
# ── 第 1 步:盘点现状 ──
# 看所有组件当前的 --cloud-provider 取值
grep -rn "cloud-provider" /etc/kubernetes/manifests/ 2>/dev/null
grep -rn "cloud-provider" /var/lib/kubelet/config.yaml 2>/dev/null
# 看有没有已经卡住的节点
kubectl get nodes -o wide
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.taints[*].key}{"\n"}{end}'
# 看还在用树内卷插件的 StorageClass
kubectl get storageclass \
-o custom-columns='NAME:.metadata.name,PROVISIONER:.provisioner' \
| grep -E "kubernetes.io/(rbd|cephfs)"
# ── 第 2 步:先装好厂商 CCM,再动 flag ──
# 顺序很重要:CCM 没就位就改 external,节点会永远卡在 uninitialized 污点上
kubectl apply -f cloud-controller-manager.yaml
kubectl -n kube-system rollout status deploy/cloud-controller-manager --timeout=300s
# ── 第 3 步:把 kubelet 切到 external ──
# kubeadm 集群:kubeadm 1.31 的 ClusterConfiguration 已无 cloudProvider 字段,
# 由 kubeadm 默认管理,必要时手工编辑 kubelet 配置文件
sed -i 's/^cloud-provider:.*/cloud-provider: external/' /var/lib/kubelet/config.yaml
systemctl restart kubelet
# ── 第 4 步:验证污点被清除 ──
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.taints[*].key}{"\n"}{end}'
# 期望:uninitialized 消失,control-plane 保留
# ── 第 5 步:应急兜底(谨慎,会让节点失去调度保护)──
# 只在 CCM 确实无法就位、且你清楚后果时使用
kubectl taint nodes --all node.cloudprovider.kubernetes.io/uninitialized-
⚠️ 第 5 步是把双刃剑:它能让 Pod 调度起来,但同时跳过了「节点尚未被云厂商正确初始化」的信号,官方文档也明确警告过这一点——「in the event that cloud controller manager is not available, new nodes in the cluster will be left unschedulable」。把它当成排障期的临时手段,不要当成长期方案。
四、本文的验证方法与边界
必须说清楚我做了什么、没做什么:
- 没有做的事:本机没有可用的 Kubernetes 集群,我没有启动集群、没有滚动升级、没有抓取真实的 kubelet 日志。因此本文不声称「实测复现了集群故障」。
- 做了的事:把 v1.31 上游的三段源码逻辑——
ToleratesTaint、FindMatchingUntoleratedTaint+DoNotScheduleTaintsFilterFunc、以及调度器Filter()的报错格式串——逐行复刻成脚本,直接喂入官方文档与官方博客的原始 YAML 跑对拍。5 组断言全部通过。 - 复刻脚本:见下一节。改动 YAML 就能复算「这份 toleration 到底能不能让 CCM 被调度」。
- 报错的出处:三个报错的原文全部来自 v1.31 分支源码字面量,均已在文中标注具体文件与函数,可逐条回溯。
- 未覆盖的边界:本文没有讨论各云厂商 CCM 自身的部署差异与版本兼容矩阵;Ceph 场景只覆盖了「树内插件被删、需要迁到 CSI」这一层,没有实测
ceph-csi的具体参数。
五、FAQ
Q:升到 1.31 之后必须部署厂商的 CCM 吗?
A:只有当你确实需要云厂商集成(弹性 IP、负载均衡器、云路由、按可用区打标签、节点自动伸缩)时才需要。如果你的集群跑在裸金属或完全不用云 API 的环境,--cloud-provider 保持空字符串即可,1.31 不会因此报错。
Q:unknown cloud provider 和 no volume plugin matched name 是一回事吗?
A:不是。前者来自 staging/src/k8s.io/cloud-provider/plugins.go 的 InitCloudProvider,管的是云厂商身份;后者来自 pkg/volume/plugins.go 的 FindPluginByName,管的是存储卷插件。1.31 把这两类树内实现一起删了,所以升级时可能同时撞上。
Q:operator 留空和写 Exists 有什么区别?
A:留空等于 Equal(上游注释原文 // empty operator means Equal),会精确比对 value。Exists 忽略 value,匹配该 key 的所有取值。对 uninitialized 污点(value 为 "true")来说,只有「留空 + value 写对」或「Exists」两种能命中。
Q:为什么我的 CCM 报的是 not-ready 而不是 uninitialized?
A:node.kubernetes.io/not-ready 和 node.kubernetes.io/unreachable 在 v1.31 的 pkg/controller/nodelifecycle/node_lifecycle_controller.go 里是 NoExecute 污点,而调度器的 DoNotScheduleTaintsFilterFunc 只看 NoSchedule——所以 not-ready 不会阻止调度,但会触发驱逐。两者要分开处理:前者加 toleration 解决调度,后者加 toleration 加 tolerationSeconds 解决驱逐。
Q:只看官方文档的示例够吗?
A:官方文档的 ccm-example.yaml 在 1.31 上是正确的,但它用的是 DaemonSet 形态且节点选择器是 master。官方博客那份 Deployment 示例则漏了 control-plane 容忍项。抄示例之前,先用文末脚本对自己的 YAML 跑一遍。
六、复刻脚本(可直接跑)
把下面这段存成 taint_match.py,用任意 Python 3 运行。它复刻的是上游 v1.31 的匹配逻辑,污点取值全部来自源码字面量。
# -*- coding: utf-8 -*-
"""Kubernetes 1.31 CCM 调度可行性对拍(复刻上游 release-1.31 逻辑)"""
def tolerates_taint(tol, taint):
# 复刻 staging/src/k8s.io/api/core/v1/toleration.go 的 ToleratesTaint
if len(tol["effect"]) > 0 and tol["effect"] != taint["effect"]:
return False
if len(tol["key"]) > 0 and tol["key"] != taint["key"]:
return False
op = tol["operator"]
if op in ("", "Equal"):
return tol["value"] == taint["value"] # empty operator means Equal
if op == "Exists":
return True
return False
def find_matching_untolerated_taint(taints, tolerations, effect_filter):
# 复刻 component-helpers/scheduling/corev1/helpers.go
for taint in taints:
if not effect_filter(taint):
continue
if not any(tolerates_taint(t, taint) for t in tolerations):
return taint, True
return {}, False
def do_not_schedule_filter(t):
# 复刻 pkg/scheduler/framework/plugins/helper/taint.go
return t["effect"] == "NoSchedule"
# 污点真实取值(来源见正文各节)
REAL_TAINTS = [
{"key": "node.cloudprovider.kubernetes.io/uninitialized", "value": "true", "effect": "NoSchedule"},
{"key": "node-role.kubernetes.io/control-plane", "value": "", "effect": "NoSchedule"},
{"key": "node.kubernetes.io/not-ready", "value": "", "effect": "NoExecute"},
{"key": "node.kubernetes.io/unreachable", "value": "", "effect": "NoExecute"},
]
def check(name, tolerations):
taint, untol = find_matching_untolerated_taint(
REAL_TAINTS, tolerations, do_not_schedule_filter)
if untol:
# 复刻 taint_toleration.go 的 Filter 报错格式串
print("%-42s 不可调度: node(s) had untolerated taint {%s: %s}"
% (name, taint["key"], taint["value"]))
else:
print("%-42s 可调度" % name)
# A 官方文档 ccm-example.yaml
check("官方文档 ccm-example.yaml", [
{"key": "node.cloudprovider.kubernetes.io/uninitialized", "operator": "", "value": "true", "effect": "NoSchedule"},
{"key": "node-role.kubernetes.io/control-plane", "operator": "Exists", "value": "", "effect": "NoSchedule"},
{"key": "node-role.kubernetes.io/master", "operator": "Exists", "value": "", "effect": "NoSchedule"},
])
# B 官方博客 2025-02-14 Deployment
check("官方博客 2025-02-14 Deployment", [
{"key": "node-role.kubernetes.io/master", "operator": "Exists", "value": "", "effect": "NoSchedule"},
{"key": "node.kubernetes.io/unreachable", "operator": "Exists", "value": "", "effect": "NoExecute"},
{"key": "node.kubernetes.io/not-ready", "operator": "Exists", "value": "", "effect": "NoExecute"},
{"key": "node.cloudprovider.kubernetes.io/uninitialized", "operator": "Exists", "value": "", "effect": "NoSchedule"},
{"key": "node.kubernetes.io/not-ready", "operator": "Exists", "value": "", "effect": "NoSchedule"},
])
# C 手写常见漏法:只写 key + effect
check("只写 key+effect(漏 value)", [
{"key": "node.cloudprovider.kubernetes.io/uninitialized", "operator": "", "value": "", "effect": "NoSchedule"},
])
预期输出:
官方文档 ccm-example.yaml 可调度
官方博客 2025-02-14 Deployment 不可调度: node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }
只写 key+effect(漏 value) 不可调度: node(s) had untolerated taint {node.cloudprovider.kubernetes.io/uninitialized: true}
总结与下一步
Kubernetes 1.31 把云厂商集成的树内实现全部移出核心,这件事本身是 KEP-2395 走了五个版本(1.26 到 1.31)后的必然收尾。但它在实操层留下的坑集中在三处:flag 取值只剩两种、树内卷插件彻底消失、CCM 自己可能被官方示例的缺陷卡在 Pending。
如果你的集群正卡在 1.31 升级上,按这个顺序处理:先确认 --cloud-provider 的所有取值 → 确认厂商 CCM 已就位并带 leader election → 确认 toleration 同时覆盖 uninitialized(value 写 "true" 或用 Exists)和 control-plane → 最后才清理老的树内 StorageClass。
关于后续版本:截至本文发稿,Kubernetes 最新 release 已经是 v1.37.1(2026 年 9 月 23 日发布),1.31 之后每个版本都在持续加固这条外部化路径。如果你还在 1.30 及更早的版本上,不要直接跳到最新版本——先在 1.31 这一级把云厂商集成迁到 external,确认集群稳定后再逐级升级,中间的 --cloud-provider 取值变化和 API 弃用项会少很多。
同类的「版本升级踩坑」方法论,可以看本站那篇 PostgreSQL 17 升级实战:逻辑复制免重同步与增量备份,它讲的是如何在升级前做一次可回滚的快照。
想了解 Kubernetes 存储侧的其他升级陷阱,可以看本站此前那篇 Kubernetes 存储排障复盘:PV 泄漏、Terminating 卡死与 Multi-Attach 的 5 个真实报错,它讲的是运行期的问题,与本文的升级迁移期正好互补。
👉 立即参与小米 MiMo 开放平台:国内领先的 AI 大模型开放平台,高性价比推理服务
👉 立即参与阿里云 AI:汇集爆款 AI 产品,热门模型专属权益优惠券助力企业创新加速
📌 本文由 AI 辅助生成并经人工审核发布 | TechPassive — AI 驱动的内容测试站点,专注于效率工具与 SaaS 真实评测
🔗 精选推荐工具
使用以下链接支持我们持续产出高质量内容(点击可直接前往购买):