本文旨在为中高级工程师与架构师深度剖析 Kubernetes 调度体系中的核心机制——Taint(污点)与 Toleration(容忍度)。我们将超越“是什么”的 سطح,深入探讨“为什么”与“如何工作”。通过结合操作系统调度原理、分布式系统设计哲学与一线工程实践中的真实场景(如 GPU 资源池隔离、节点维护、高可用保障),我们将从内核调度器的 Predicate/Priority 算法,一直剖析到 Node Controller 与 Kubelet 的协同工作流,并最终给出可落地的架构演进路径。
现象与问题背景
在管理一个有一定规模的 Kubernetes 集群时,工程师很快会遇到一个经典问题:如何精确地控制 Pod 在哪些 Node 上运行,或者更准确地说,不在哪些 Node 上运行?简单的标签选择(NodeSelector/Affinity)是一种“吸引”模型,即 Pod 声明它“想去”哪里。但这并不能解决“排斥”问题。
考虑以下几个典型的真实场景:
- 专用硬件资源隔离:集群中包含一部分安装了高性能 GPU 的节点,专门用于机器学习训练。这些节点资源昂贵,我们绝不希望普通的无状态 Web 应用被调度上去,浪费宝贵的 GPU 算力并干扰训练任务。
- 关键组件的稳定性保障:为了保证 Kubernetes 控制平面(Control Plane)的稳定,我们不希望用户的业务应用与 `api-server`、`etcd` 等核心组件竞争 Master 节点上的 CPU 和内存资源。
- 节点维护与优雅下线:当需要对某个节点进行内核升级、硬件更换等维护操作时,我们首先希望不再有新的 Pod 被调度到该节点上。其次,需要将节点上已有的 Pods 优雅地驱逐(evict)到其他健康节点。
- 应对节点故障:当一个节点因网络分区或宕机而变得 `NotReady` 时,系统需要一种机制,在等待一段时间后,自动将该节点上的 Pod(尤其是单副本的有状态应用)迁移走,以恢复服务。
以上所有场景的核心诉求,都是为节点附加一种“排斥”属性,阻止不符合条件的 Pod 被调度上来,甚至驱逐已经运行的 Pod。这正是 Taint 与 Toleration 机制的设计初衷。它为节点(Node)赋予了“污点”,只有“容忍”这些污点的 Pod 才有资格被调度至此。
关键原理拆解
要理解 Taint 与 Toleration,我们必须回到计算机科学的基础——操作系统的调度原理,并将其延伸到分布式环境。Kubernetes Scheduler 本质上是一个复杂的分布式资源调度器。它的核心任务是为每一个处于 `Pending` 状态的 Pod,从集群所有可用 Node 中找到一个最合适的“家”。
这个过程可以被抽象为两个核心阶段:
- Filtering (过滤阶段):也称为 `Predicates`。在这一阶段,调度器会遍历所有节点,执行一系列预定义的过滤函数。例如,检查节点是否有足够的 CPU/Memory,节点是否满足 Pod 的 `nodeSelector` 或 `nodeAffinity` 要求。任何一个过滤函数检查不通过,该节点就会被直接排除。Taint 与 Toleration 机制主要在这一阶段生效。
- Scoring (打分阶段):也称为 `Priorities`。在过滤阶段之后,所有幸存下来的节点都是“合格”的。打分阶段会为这些合格的节点进行评分,选出分数最高的节点。评分策略包括 `Prefer` 类型的亲和性规则、镜像是否已在本地存在、负载均衡等。
- NoSchedule:这是最常见的 Effect。它告诉调度器,任何不能容忍此 Taint 的 Pod,都不应该被调度到这个节点上。这是一个纯粹的调度期(schedule-time)约束,它不影响任何已经运行在节点上的 Pod。
- PreferNoSchedule:这是一个“软”约束。它表示调度器应尽量避免将不容忍此 Taint 的 Pod 调度到该节点上。如果集群中实在没有其他更合适的节点,调度器仍然可能将 Pod 调度过来。从实现上看,它作用于 Scoring 阶段,而不是 Filtering 阶段——它会给不满足条件的节点一个较低的分数,但不会直接淘汰它。
- NoExecute:这是最“霸道”的 Effect。它不仅在调度期拒绝新的 Pod,还会影响到已经运行在节点上的 Pod。当一个节点被添加了 `NoExecute` 效果的 Taint,Node Controller 会检查该节点上所有正在运行的 Pod:如果一个 Pod 不能容忍这个 Taint,它将被驱逐(Evict)。这正是实现节点维护和故障迁移的核心机制。
- 管理员通过 `kubectl taint node …` 命令为一个节点添加了 `key=value:NoSchedule` 污点。该信息被持久化到 etcd 中。
- 一个用户创建了新的 Pod,该 Pod 进入 `Pending` 状态。
- `kube-scheduler` 监听到这个 `Pending` Pod,开始为其寻找合适的节点。
- 进入 Filtering 阶段,调度器执行 `TaintToleration` 过滤函数。
- 该函数从其本地缓存中读取目标节点的 Taints 列表和 Pod 的 Tolerations 列表。
- 它会进行匹配检查:对于节点上的每一个 Taint,Pod 是否至少有一个 Toleration 能够“容忍”它。如果节点上的任何一个 Taint 都无法被 Pod 容忍,该节点在此次调度中被判定为不合格。
- 最终,只有通过所有 Predicates(包括 TaintToleration)的节点,才会进入后续的 Scoring 阶段。
- 一个事件发生,导致节点被添加了 `NoExecute` 污点。这可能是管理员手动添加(用于维护),也可能是 `kubelet` 上报了异常状态(如 `NotReady`),`Node Controller` 自动为该 Node 对象添加了如 `node.kubernetes.io/unreachable:NoExecute` 的污点。
- `Node Controller` 持续监控 Node 对象的变化。当它检测到一个带有 `NoExecute` 效果的 Taint 被添加时,它会触发一个协调循环(Reconciliation Loop)。
- 该控制器会列出(List)该节点上所有正在运行的 Pod。
- 对于每一个 Pod,它会检查其 `tolerations` 字段,判断是否能容忍这个新的 `NoExecute` Taint。
- 如果 Pod 不能容忍:`Node Controller` 会立即通过 API Server 发起对该 Pod 的驱逐(Eviction)请求。这本质上是优雅地删除 Pod,其 ReplicaSet 或 StatefulSet 控制器会负责在其他节点上重建一个新的副本。
- 如果 Pod 能容忍,但设置了 `tolerationSeconds`:这是一个宽限期。`Node Controller` 不会立即驱逐 Pod,而是会启动一个计时器。如果在 `tolerationSeconds` 定义的秒数内,该 `NoExecute` Taint 被移除了(例如,节点恢复了健康),则驱逐流程被取消。如果计时器到期 Taint 依然存在,Pod 才会被驱逐。这对于有状态应用应对网络抖动等短暂故障至关重要。
- Equal:要求 `key`, `value`, `effect` 都完全匹配。这是默认值。
- Exists:只要求 `key` 和 `effect` 匹配,不关心 `value` 的具体内容。这在希望容忍一类污点时非常有用,例如容忍所有因为硬件问题产生的污点,而不管具体问题是什么。
- Taint/Toleration(推模型):控制权在 Node。Node 通过 Taint 主动地“推开”(排斥)Pod。这是从集群管理员的视角出发,用于管理节点池,强制隔离。一个 Taint 会影响到所有没有对应 Toleration 的 Pod,影响面广。
- Node Affinity(拉模型):控制权在 Pod。Pod 通过 Affinity 声明自己“想要去”(吸引)什么样的 Node。这是从应用开发者的视角出发,为自己的应用选择最合适的运行环境。一个 Affinity 规则只影响定义它的 Pod,影响范围精确。
- Taint the nodes:为所有 GPU 节点打上 `gpu=true:NoSchedule` 的 Taint。这一步确保了任何“不速之客”(普通 Pod)都不会被调度上来。
- Add Node Affinity to pods:为所有需要 GPU 的 Pod 添加 `nodeAffinity`,要求它们必须被调度到带有 `label: gpu=true` 的节点上。
- Add Toleration to pods:同时,为这些 GPU Pod 添加对 `gpu=true:NoSchedule` Taint 的 Toleration。
- 定制化的 `tolerationSeconds`:对于关键的有状态应用,根据其数据同步和恢复的特性,为其 Pod 设置一个更长的 `tolerationSeconds`(例如 15 分钟)。这给了系统更充足的时间等待节点自行恢复,避免了在短暂中断下的过度反应。
- 结合 Pod Disruption Budgets (PDB):PDB 限制了在自愿中断(如节点排空)期间,一个应用可以同时有多少个 Pod 不可用。当 `kubectl drain`(它会给节点添加 `NoExecute` Taint)执行时,它会尊重 PDB 的约定,确保应用的整体可用性不被破坏。
- 理解系统默认Tolerations:Kubernetes 会为所有 Pod 默认添加对某些 Node Condition Taints 的 Toleration,例如 `node.kubernetes.io/not-ready` 和 `node.kubernetes.io/unreachable`,默认的 `tolerationSeconds` 是 300 秒。了解并可能覆盖这些默认值,是高级运维能力的一部分。
- “所有创建在 `data-science` 命名空间下的 Pod,必须拥有对 `workload=ml:NoSchedule` Taint 的容忍度。”
- “禁止任何非 `kube-system` 命名空间的 Pod 容忍 `node-role.kubernetes.io/master` Taint。”
-
想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
交易系统整体解决方案。 -
如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
产品与服务
中关于交易系统搭建与定制开发的介绍。 -
需要针对现有架构做评估、重构或从零规划,可以通过
联系我们
和架构顾问沟通细节,获取定制化的技术方案建议。
end of ol>
Taint/Toleration 是一个典型的 `Predicate`,它为 Node 提供了一种强大的否决权。一个 Taint 由三部分组成:`Key`、`Value` 和 `Effect`。
<key>=<value>:<effect>
其中,`Effect` 是理解其行为模式的关键,它定义了污点的“毒性”等级:
从分布式系统角度看,`NoSchedule` 和 `PreferNoSchedule` 是关于资源放置(Placement)的策略,而 `NoExecute` 则是关于故障检测与恢复(Failure Detection & Recovery)的策略。它将节点的健康状况(如 `NotReady`, `DiskPressure`)与 Pod 的生命周期管理直接关联起来。
系统架构总览
Taint 与 Toleration 机制的实现,并非由 `kube-scheduler` 单独完成,而是由 `kube-scheduler`、`API Server`、`Node Controller` 和 `kubelet` 等多个控制平面组件协同工作的结果。我们以 `NoSchedule` 和 `NoExecute` 两种效果为例,描绘其工作流。
`NoSchedule` 工作流:调度期的静态检查
这是一个相对简单的流程,完全由 `kube-scheduler` 主导:
`NoExecute` 工作流:运行期的动态驱逐
这个流程更为复杂,引入了 `Node Controller` 作为关键参与者,体现了 Kubernetes 的主动健康管理能力:
这个双重工作流清晰地展示了 Kubernetes 设计的精妙之处:调度期的约束由高度优化的 `kube-scheduler` 负责,而运行期的动态调整则由专门的控制器(`Node Controller`)异步处理,实现了职责分离和系统的鲁棒性。
核心模块设计与实现
作为工程师,我们需要深入到 API 对象定义和伪代码层面,才能真正掌握其精髓。
首先是命令行操作,这是最直观的交互方式:
# 为 node-01 添加一个 NoSchedule 的污点
$ kubectl taint nodes node-01 gpu=true:NoSchedule
# 查看节点的 Taints
$ kubectl describe node node-01 | grep Taints
# 移除污点
$ kubectl taint nodes node-01 gpu:NoSchedule-
接下来,是在 Pod 的 YAML 中定义 `tolerations`:
apiVersion: v1
kind: Pod
metadata:
name: my-ml-pod
spec:
containers:
- name: cuda-container
image: nvidia/cuda:11.4.0-base
tolerations:
- key: "gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"
在上面的例子中,Pod 声明了它可以“容忍”`key`为`gpu`、`value`为`true`、`effect`为`NoSchedule`的污点。`operator` 字段是匹配逻辑的关键,它有两个可能的值:
API 对象与匹配逻辑
在 Kubernetes 的源码中(`k8s.io/api/core/v1/types.go`),Taint 和 Toleration 的数据结构定义清晰地揭示了它们的组成:
// Taint a taint on a node.
type Taint struct {
Key string
Value string
Effect TaintEffect
}
// Toleration is a toleration for a taint.
type Toleration struct {
Key string
Operator TolerationOperator
Value string
Effect TaintEffect
// TolerationSeconds is the period of time the toleration
// tolerates a NoExecute taint.
TolerationSeconds *int64
}
调度器中 `TaintToleration` Predicate 的核心逻辑可以用以下伪代码来表示。其时间复杂度为 O(T * L),其中 T 是节点上的 Taints 数量,L 是 Pod 上的 Tolerations 数量。由于这两个数通常都很小,这个 Predicate 的性能开销极低。
// IsTaintTolerated checks if the pod tolerates the taint.
// A pod tolerates a taint if it has a toleration that matches the taint.
func IsTaintTolerated(pod *v1.Pod, taint *v1.Taint) bool {
// 遍历 Pod 的所有 Toleration
for _, toleration := range pod.Spec.Tolerations {
// 1. 首先检查 Effect 是否匹配。
// 如果 Toleration 没有指定 Effect,它可以匹配所有 Effect。
// 如果指定了,就必须精确匹配。
if len(toleration.Effect) > 0 && toleration.Effect != taint.Effect {
continue
}
// 2. 检查 Key 和 Operator
if len(toleration.Key) == 0 {
// 如果 Toleration 的 Key 为空,它可以容忍任何 Key 的 Taint (Operator 必须是 Exists)
if toleration.Operator == v1.TolerationOpExists {
return true
}
continue
}
// 如果 Toleration 的 Key 不为空,则必须与 Taint 的 Key 匹配
if toleration.Key != taint.Key {
continue
}
// 3. 根据 Operator 检查 Value
switch toleration.Operator {
case v1.TolerationOpExists:
// Operator 是 Exists,Key 匹配即可,Value 无所谓
return true
case v1.TolerationOpEqual:
// Operator 是 Equal,Value 必须完全相等
if toleration.Value == taint.Value {
return true
}
default:
// 默认继续检查下一个 Toleration
}
}
// 遍历完所有 Toleration 都没找到匹配的,则该 Taint 不被容忍
return false
}
特别需要强调 `TolerationSeconds`。这是一个仅对 `NoExecute` 效果有效的字段。当一个 Pod 容忍了一个 `NoExecute` 污点并且设置了此字段,它是在告诉 Node Controller:“请给我一段缓冲时间”。这个设计在应对云环境中常见的网络抖动、短暂的节点无响应等场景时,极大地提高了有状态应用的可用性,避免了不必要的、昂贵的 Pod 重新调度和数据恢复过程。
性能优化与高可用设计
在掌握了 Taint/Toleration 的基础后,架构师必须思考其在复杂系统中的权衡(Trade-off)与组合应用。
Taint/Toleration vs. Node Affinity/Anti-Affinity
这是最常被混淆的一对概念,但它们的哲学完全不同。可以总结为“推”与“拉”的区别。
在实践中,两者往往结合使用,以达到最强的控制效果。例如,要创建一个专用的 GPU 节点池:
通过这个“Taint+Affinity”的组合拳,我们不仅阻止了无关 Pod 的进入,还确保了目标 Pod 精确地被调度到这个专用池中,形成了一个逻辑上完全隔离的资源分区。
`NoExecute` 与有状态应用的高可用
`NoExecute` 是一把双刃剑。它赋予了集群强大的自愈能力,但也可能对有状态应用(如数据库、消息队列)造成冲击。当一个承载着主数据库实例的节点因为网络抖动而被标记为 `unreachable` 时,默认的 5 分钟 `tolerationSeconds` 可能会触发不必要的故障转移(failover)。
高可用设计策略:
架构演进与落地路径
在团队中引入和应用 Taint/Toleration 机制,应该遵循一个循序渐进的演进路径,避免一步到位带来的管理混乱。
阶段一:基础隔离(保护核心组件)
这是所有 Kubernetes 集群的默认实践。Master 节点天生就被打上了 `node-role.kubernetes.io/master:NoSchedule` 的 Taint。这一阶段的目标是让团队理解这个最基础的隔离机制,并意识到不能随意移除 Master 节点的 Taint。
阶段二:专用资源池划分(手动管理)
当集群中出现异构硬件(GPU、高IOPS磁盘等)时,进入此阶段。由集群管理员手动为这些专用节点添加 `NoSchedule` 类型的 Taint。应用团队则负责为他们的 Pod 添加相应的 Toleration 和 NodeAffinity。这个阶段的特点是规则静态、手动配置,适用于规模不大的集群。
阶段三:自动化运维与动态响应(拥抱 `NoExecute`)
团队开始使用 `kubectl drain` 进行节点维护,并理解其背后的 `NoExecute` Taint 原理。同时,开始关注由系统自动添加的 Node Condition Taints(如 `unreachable`),并为关键的有状态应用配置合理的 `tolerationSeconds`,以提高其在节点短暂故障时的韧性。运维的重点从静态配置转向动态的、事件驱动的响应。
阶段四:策略即代码(Policy-as-Code)
在大型多租户集群中,手动管理 Taint 和 Toleration 容易出错且难以审计。此阶段引入策略引擎,如 OPA/Gatekeeper 或 Kyverno。通过定义 Admission Controller 的规则,来自动化地实施调度策略。例如:
这使得调度策略变成了可审查、可版本控制的代码,极大地提升了大规模集群的治理水平和安全性。
通过遵循这条演进路径,团队可以逐步、安全地利用 Taint 与 Toleration 这一强大武器,从简单的资源隔离,到构建一个真正健壮、自愈、策略驱动的智能调度平台。