K8s-Pod调度与绑定流程
Pod 调度与绑定通信流程
目标
这篇文档只回答一个核心问题:一个 Pod 是怎么从“等待调度”变成“被某个节点上的 kubelet 感知到”的?
重点放在组件之间的通信与职责边界,而不是调度器内部打分算法细节。
一句话摘要
kube-scheduler 负责为 Pod 选节点,kube-apiserver 负责持久化绑定结果,kubelet 通过监听 API Server 中属于本节点的 Pod 更新来感知这次分配。
1. 流程总览
从通信视角看,这条链路可以拆成 3 个阶段:
- 调度决策阶段:scheduler 为 Pod 选择目标节点。
- 绑定落盘阶段:scheduler 通过 API Server 把绑定结果写回集群状态。
- 节点感知阶段:目标节点上的 kubelet 观察到这个 Pod 已经分配给自己,开始进入节点侧处理。
Mermaid:总览图
flowchart LR
A[Pending Pod] --> B[kube-scheduler]
B --> C[select target node]
C --> D[kube-apiserver]
D --> E[persist spec.nodeName]
E --> F[target node kubelet]
F --> G[PodConfig / update channel]
G --> H[podManager / podWorkers]
端到端架构全景图
flowchart TB
subgraph 用户侧[“用户侧”]
U1[kubectl / API 客户端]
end
subgraph 控制面[“控制面 (Control Plane)”]
API[kube-apiserver]
ETCD[(etcd)]
SCHED[kube-scheduler]
CTRL[kube-controller-manager]
API <--> ETCD
SCHED --> API
CTRL --> API
end
subgraph 调度器内部[“调度器内部架构”]
direction TB
SQ[待调度队列<br/>PriorityQueue]
SC[schedulingCycle<br/>调度周期]
BC[bindingCycle<br/>绑定周期]
CACHE[Scheduler Cache<br/>节点信息缓存]
SC --> BC
CACHE --> SC
end
subgraph 节点侧[“节点侧 (Worker Node)”]
direction TB
KL[kubelet]
PC[PodConfig]
PM[podManager]
PW[podWorkers]
RT[容器运行时<br/>CRI]
KL --> PC
PC --> PM
PM --> PW
PW --> RT
end
U1 -->|创建 Pod| API
API -->|watch Pod| SCHED
API -->|watch Pod| CTRL
SCHED --> SQ
SQ --> SC
BC -->|POST /binding| API
API -->|watch Node.POD| KL
组件通信协议图
flowchart LR
subgraph 通信协议
direction TB
P1[“Informer/Watch<br/>(长连接)”]
P2[“REST API<br/>(HTTP)”]
P3[“gRPC<br/>(CRI)”]
end
subgraph 通信路径
API_SCH[“API Server ↔ Scheduler”] --> P1
API_KL[“API Server ↔ Kubelet”] --> P1
SCH_API[“Scheduler → API Server”] --> P2
KL_API[“Kubelet → API Server”] --> P2
KL_RT[“Kubelet → Runtime”] --> P3
end
这张图想表达什么
- scheduler 只负责”做决定”和”提交绑定”。
- API Server 是控制面共享事实来源。
- kubelet 并不是被 scheduler 直接调用,而是通过 watch 自己感知变化。
- 所有组件通过 API Server 间接通信,实现松耦合架构。
2. 分阶段通信流程
阶段一:scheduler 选节点
这个阶段的本质是:scheduler 在本地做出一个决策,而不是直接改变集群状态。
调度框架扩展点流程图
flowchart TB
subgraph 调度周期["Scheduling Cycle (调度周期)"]
direction TB
A[Pod 从队列取出] --> B[QueueSort 插件]
B --> C[PreFilter 插件]
C --> D{Filter 插件}
D -->|节点通过| E[PostFilter 插件]
D -->|节点不通过| D2[排除节点]
E --> F[PreScore 插件]
F --> G[Score 插件]
G --> H[NormalizeScore 插件]
H --> I[选择最优节点]
I --> J[Reserve 插件]
J --> K[Permit 插件]
end
subgraph 绑定周期["Binding Cycle (绑定周期)"]
direction TB
K --> L[PreBind 插件]
L --> M[Bind 插件]
M --> N[PostBind 插件]
end
subgraph 插件示例["内置插件示例"]
direction LR
P1[NodeResourcesFit]
P2[NodeName]
P3[NodeAffinity]
P4[TaintToleration]
P5[PodTopologySpread]
P6[ImageLocality]
P7[DefaultBinder]
end
D -.-> P1
D -.-> P2
D -.-> P3
D -.-> P4
G -.-> P5
G -.-> P6
M -.-> P7
步骤 1.1:Pod 进入待调度队列
当一个 Pod 被创建但没有指定 spec.nodeName 时,它会被 apiserver 的 informer 推入 scheduler 的待调度队列。此时 Pod 状态是 Pending,条件 PodScheduled 为 False。
通信方向:apiserver → scheduler(通过 informer/watch)
步骤 1.2:scheduler 取出 Pod
scheduler 的主循环从队列中取出一个 Pod,开始一次调度周期(schedulingCycle)。此时 Pod 仍然只是”等待被决定”。
通信方向:scheduler 内部队列 → scheduler 主循环
步骤 1.3:执行预过滤(PreFilter)
在真正过滤节点之前,调度框架会先执行 PreFilter 插件。这些插件可以做一些前置检查或数据准备,比如检查 Pod 是否符合某些调度约束。
通信方向:scheduler 内部插件链
步骤 1.4:执行过滤(Filter)
Filter 插件会遍历所有候选节点,逐一检查每个节点是否满足 Pod 的硬性约束(比如资源够不够、nodeSelector 是否匹配、taint 是否容忍等)。不满足的节点会被直接排除。
通信方向:scheduler 内部,读取 scheduler cache 中的节点视图
步骤 1.5:执行打分(Score)
对过滤后剩下的可行节点,Score 插件会根据策略打分(比如资源利用率、亲和性、反亲和性等)。分数越高,表示这个节点越适合运行这个 Pod。
通信方向:scheduler 内部,基于 scheduler cache 中的数据计算
步骤 1.6:选择最优节点
scheduler 根据打分结果,选择分数最高的节点作为目标节点(SuggestedHost)。如果有多个节点分数相同,通常会随机选一个。
通信方向:scheduler 内部决策
步骤 1.7:本地 assume(乐观前进)
选出目标节点后,scheduler 会在自己的 cache 中假设这个 Pod 已经绑定到该节点。这样做是为了提高吞吐量——在真正向 apiserver 提交绑定之前,scheduler 就可以继续调度其他 Pod 了。
关键理解:assume 只是 scheduler 本地的乐观假设,不代表集群状态已经更新。如果后续绑定失败,scheduler 需要清理这个假设状态。
通信方向:scheduler 内部 cache 更新
阶段二:通过 API Server 完成绑定
这个阶段的本质是:把 scheduler 的本地决策变成集群中的共享状态。
步骤 2.1:进入绑定周期(bindingCycle)
调度周期结束后,scheduler 进入绑定周期。这个周期负责把”选定的节点”真正写回控制面。
通信方向:scheduler 内部状态流转
步骤 2.2:执行 PreBind 插件
在真正绑定之前,PreBind 插件有机会做一些前置工作,比如准备存储卷(CSI attach)或网络配置。如果 PreBind 失败,整个绑定周期会中止,Pod 可能会被重新调度。
通信方向:scheduler → 外部系统(如 CSI driver)或 scheduler 内部
步骤 2.3:执行 Bind 插件
默认的 DefaultBinder 会向 API Server 提交一个 v1.Binding 请求,把 Pod -> Node 的关系写进控制面。这个请求不是更新整个 Pod 对象,而是通过 Pod/Binding 子资源完成绑定。
通信方向:scheduler → API Server(HTTP POST 到 /api/v1/namespaces/{ns}/pods/{name}/binding)
步骤 2.4:API Server 接收并校验请求
API Server 收到 Binding 请求后,会校验:
- Pod 是否存在
- Pod 是否已经绑定
- 目标 Node 是否存在
- 请求者是否有权限执行绑定
通信方向:API Server 内部校验
步骤 2.5:持久化 spec.nodeName
校验通过后,API Server 更新 Pod 的 spec.nodeName 字段,并写入 etcd。此时 Pod 的节点归属在集群层面正式生效。
通信方向:API Server → etcd
步骤 2.6:返回绑定结果
API Server 返回成功响应给 scheduler。scheduler 收到成功后,绑定周期结束;如果失败,scheduler 会清理之前 assume 的状态,并可能重新入队。
通信方向:API Server → scheduler
阶段三:目标 kubelet 感知到 Pod
这个阶段的本质是:kubelet 通过观察控制面状态,发现有一个 Pod 被分配给了自己。
Kubelet 内部 Pod 处理流程图
flowchart TB
subgraph API通信["API Server 通信层"]
API[kube-apiserver]
WATCH[Watch 连接<br/>过滤 nodeName==本节点]
end
subgraph PodSource["Pod 来源聚合层"]
direction TB
AS[apiserver source]
FS[file source<br/>静态Pod]
HS[http source]
PC[PodConfig<br/>聚合器]
CH[updates channel]
end
subgraph KubeletCore["Kubelet 核心处理层"]
direction TB
KL[Kubelet.SyncLoop]
PM[podManager<br/>期望态视图]
PW[podWorkers<br/>状态机驱动]
end
subgraph Runtime["运行时执行层"]
direction TB
RM[kubeGenericRuntimeManager]
CRI[CRI RuntimeService]
RT[容器运行时<br/>containerd/cri-o]
end
API -->|watch| WATCH
WATCH --> AS
FS --> PC
HS --> PC
AS --> PC
PC -->|PodUpdate| CH
CH --> KL
KL --> PM
PM -->|UpdatePod| PW
PW -->|SyncPod| RM
RM -->|gRPC| CRI
CRI -->|gRPC| RT
步骤 3.1:kubelet 启动时注册 apiserver pod source
kubelet 在启动时,会通过 makePodSourceConfig 注册多个 Pod 来源,其中最重要的就是 apiserver source。这个 source 会 watch API Server 中属于本节点的 Pod。
通信方向:kubelet 启动时初始化,设置 watch 连接
步骤 3.2:apiserver pod source 开始 watch
apiserver pod source 建立一个长连接,持续监听 Pod 的变化事件(ADD/UPDATE/DELETE)。它会过滤出 spec.nodeName == 本节点名称 的 Pod。
通信方向:kubelet → API Server(通过 informer/watch)
步骤 3.3:收到 Pod 更新事件
当 scheduler 完成绑定后,API Server 会通过 watch 机制推送 Pod 更新事件给所有订阅者。目标节点的 kubelet 收到这个事件。
通信方向:API Server → kubelet(通过 watch 推送)
步骤 3.4:过滤并确认属于本节点
kubelet 的 apiserver source 会再次检查 Pod 的 spec.nodeName,确保这个 Pod 确实是分配给自己的,而不是其他节点。
通信方向:kubelet 内部过滤逻辑
步骤 3.5:送入 update channel
确认后,Pod 更新会被送入 kubelet 的内部更新通道。这个通道是 PodConfig 聚合多个 source(apiserver、file、HTTP)后对外提供的统一入口。
通信方向:kubelet 内部 channel
步骤 3.6:podManager 更新本地视图
podManager 收到更新后,会维护本地期望态视图,包括 Pod 的元数据索引、mirror pod 映射等。
通信方向:kubelet 内部,podManager 接收更新
步骤 3.7:podWorkers 接收 UpdatePod
最后,Pod 更新被传递给 podWorkers.UpdatePod(...),正式进入节点侧生命周期处理。从此开始,这个 Pod 会经过 SyncPod、创建 sandbox、启动容器等步骤。
通信方向:kubelet 内部,podWorkers 接管
小结:这条链路的 3 个关键通信边界
| 边界 | 谁和谁通信 | 通过什么方式 |
|---|---|---|
| scheduler ↔ API Server | scheduler 提交绑定,API Server 持久化 | HTTP POST /pods/{name}/binding |
| API Server ↔ kubelet | kubelet watch Pod 变化 | informer/watch 长连接 |
| scheduler ↔ kubelet | 不直接通信 | 通过 API Server 间接 |
核心结论:scheduler 与 kubelet 之间没有点对点通知,而是通过 API Server 进行间接通信。
2.5 完整流程图
下面这张图把本文所有步骤串在一起,方便一眼看完整个通信链路:
flowchart TD
subgraph 控制面["控制面(Control Plane)"]
A1[Pod created] --> A2[API Server]
A2 --> A3[scheduler informer]
A3 --> A4[待调度队列]
end
subgraph 调度器["调度器(Scheduler)"]
A4 --> B1[取出 Pod]
B1 --> B2[PreFilter 插件]
B2 --> B3[Filter 插件]
B3 --> B4[Score 插件]
B4 --> B5[选择最优节点]
B5 --> B6[assume 到 cache]
B6 --> B7[bindingCycle]
B7 --> B8[PreBind 插件]
B8 --> B9[Bind 插件]
end
subgraph 绑定["绑定(Binding)"]
B9 --> C1[POST /pods/{name}/binding]
C1 --> C2[API Server 校验]
C2 --> C3[写入 etcd]
C3 --> C4[spec.nodeName 生效]
end
subgraph 节点侧["节点侧(Kubelet)"]
C4 --> D1[kubelet watch]
D1 --> D2[过滤本节点 Pod]
D2 --> D3[PodConfig channel]
D3 --> D4[podManager]
D4 --> D5[podWorkers.UpdatePod]
end
2.6 与其他组件的交互
这条链路不只是 scheduler 和 kubelet 的事,还涉及多个系统的协作:
与 Controller Manager 的交互
虽然本文聚焦调度绑定,但 scheduler 的行为会受到多个控制器的影响:
| 控制器 | 如何影响调度 |
|---|---|
| ReplicaSet Controller | 创建/删除 Pod,触发调度 |
| Node Lifecycle Controller | 更新 Node taint,影响调度过滤 |
| PV Controller | 绑定 PVC 到 PV,影响带存储的 Pod 调度 |
| Service Controller | 更新 Endpoint,间接影响亲和性调度 |
关键理解:scheduler 是被动的,它响应的是控制面状态变化。控制器通过创建/删除 Pod 或修改 Node 状态来间接影响调度。
与 CSI(Container Storage Interface)的交互
在 PreBind 阶段,scheduler 可能会与 CSI driver 交互:
flowchart LR
A[PreBind 插件] --> B{Pod 有 PVC?}
B -->|是| C[检查 PV 已绑定]
C --> D{需要等待?}
D -->|是| E[阻塞直到就绪]
D -->|否| F[继续绑定]
B -->|否| F
关键理解:如果 Pod 声明了 PVC,scheduler 在 PreBind 阶段会等待 PV 绑定完成。这是调度延迟的常见原因之一。
与 Scheduler Extender 的交互
除了内置插件,scheduler 还支持 extender 机制,允许外部系统参与调度决策:
flowchart LR
A[Filter 阶段] --> B[内置插件]
B --> C[Extender HTTP 调用]
C --> D[外部过滤服务]
D --> E[返回可行节点]
| 阶段 | Extender 可以做什么 |
|---|---|
| Filter | 进一步排除节点 |
| Prioritize | 给节点打分 |
| Bind | 自定义绑定逻辑 |
关键理解:Extender 通过 HTTP 与 scheduler 通信,会增加调度延迟。
与 PriorityClass 的交互
Pod 的 priority 会影响调度顺序和抢占:
flowchart TD
A[高优先级 Pod 等待] --> B{有足够资源?}
B -->|否| C{可以抢占?}
C -->|是| D[驱逐低优先级 Pod]
D --> E[调度高优先级 Pod]
C -->|否| F[继续等待]
B -->|是| E
关键理解:PriorityClass 由管理员创建,scheduler 在调度时会考虑优先级。
与 Descheduler 的交互
Descheduler 是一个独立组件,它会根据策略驱逐已经运行的 Pod:
flowchart LR
A[Descheduler] --> B[分析集群状态]
B --> C{违反策略?}
C -->|是| D[驱逐 Pod]
D --> E[Pod 重新进入调度队列]
E --> F[scheduler 重新调度]
关键理解:Descheduler 不直接与 scheduler 通信,而是通过驱逐 Pod 来间接触发重新调度。
交互总结表
| 外部系统 | 交互方式 | 影响什么 |
|---|---|---|
| Controller Manager | 创建/删除 Pod,修改 Node | 触发调度,影响过滤 |
| CSI driver | PreBind 阶段检查 | 存储就绪才能绑定 |
| Scheduler Extender | HTTP 调用 | 参与过滤/打分/绑定 |
| PriorityClass | Pod spec 字段 | 调度顺序和抢占 |
| Descheduler | 驱逐 Pod | 触发重新调度 |
3. 关键时序图
Mermaid:主时序图
sequenceDiagram
participant Pod
participant Scheduler
participant APIServer
participant Kubelet
participant PodConfig
participant PodWorkers
Pod->>Scheduler: enter scheduling queue
Scheduler->>Scheduler: schedulingCycle()
Scheduler->>Scheduler: select target node
Scheduler->>Scheduler: assume Pod on node (local cache)
Scheduler->>Scheduler: bindingCycle()
Scheduler->>APIServer: Pods().Bind(v1.Binding)
APIServer-->>APIServer: persist spec.nodeName
Kubelet->>APIServer: watch Pods for this node
APIServer-->>PodConfig: deliver Pod update
PodConfig-->>Kubelet: update channel
Kubelet->>PodWorkers: UpdatePod(...)
时序图里的 3 个关键点
assume只是 scheduler 的本地乐观前进,不代表集群状态已经更新。- 真正让“Pod 属于某节点”生效的是 API Server 持久化绑定结果。
- kubelet 的动作起点不是 scheduler 的通知,而是“watch 到属于本节点的 Pod 更新”。
4. 异常与分支路径
主链路之外,更值得读者理解的是:这条通信链并不总是一次成功。很多看起来像“调度成功了但节点没反应”的现象,实际上发生在分支路径里。
Mermaid:异常/分支路径图
flowchart TD
A[Pod waiting for scheduling] --> B{scheduler finds feasible node?}
B -->|No| C[remain Pending / requeue]
B -->|Yes| D[assume node in scheduler cache]
D --> E{binding succeeds at API Server?}
E -->|No| F[forget assumed state / retry scheduling]
E -->|Yes| G[spec.nodeName persisted]
G --> H{kubelet on target node has observed update?}
H -->|No| I[visible in API but not yet processed on node]
H -->|Yes| J[enter podWorkers flow]
读这张图时要关注什么
- 选点失败:Pod 会继续处于 Pending,并等待后续调度周期。
- assume 后绑定失败:scheduler 会清理本地假设状态,再重试或重新入队。
- 绑定成功但 kubelet 尚未感知:这是控制面状态与节点侧处理之间常见的短暂时间差。
5. 这个流程里谁负责什么
| 组件 | 主要职责 | 不负责什么 |
|---|---|---|
kube-scheduler |
选节点、发起绑定 | 不直接启动容器,不直接通知 kubelet |
kube-apiserver |
保存 Pod 与 Node 的绑定关系,作为共享状态源 | 不执行节点本地生命周期 |
kubelet |
观察属于本节点的 Pod 更新,并开始节点侧处理 | 不参与全局调度决策 |
podWorkers |
接管节点侧 Pod 生命周期推进 | 不决定 Pod 应该去哪个节点 |
6. 常见误解
误解 1:scheduler 会直接把任务发给 kubelet
不是。两者之间的关键交汇点是 API Server。
误解 2:Pod 一旦被选中节点,就等于 kubelet 已经开始处理
不是。中间至少还隔着”绑定成功”和”kubelet watch 到更新”两个步骤。
误解 3:assume 就是最终结果
不是。assume 只是 scheduler 为了提高吞吐量做的本地 cache 前移。
误解 4:这条链路已经包含了容器创建
没有。本文只覆盖”调度绑定到 kubelet 感知”。容器创建属于下一条链路。
7. 版本差异说明
这条链路在 Kubernetes 1.27/1.28/1.29 中有一些值得注意的变化:
1.27 → 1.28 变化
| 变化点 | 说明 |
|---|---|
| DRA PodSchedulingContext → PodScheduling | 动态资源分配(DRA)API 重命名,影响 PreBind 阶段 |
| SchedulingHint 改进 | scheduler 对 Pod 更新事件的响应更精确,减少不必要的重调度 |
1.28 → 1.29 变化
| 变化点 | 说明 |
|---|---|
| 抢占逻辑修复 | 修复了抢占时 victim pod 因状态 patch 不正确导致删除失败的问题 |
| Filter 插件优化 | 某些 Filter 插件的性能改进,减少调度延迟 |
向后兼容性
| 版本 | 兼容性注意 |
|---|---|
| 1.27+ | DRA 需要 1.26+ 的 scheduler 和 kubelet 配合 |
| 1.28+ | 如果使用 DRA,需要更新客户端代码适配 PodScheduling API |
| 1.29+ | 抢占修复只影响特定场景(preemptor 和 victim QoS 不同时) |
需要关注的废弃
| 废弃项 | 替代方案 | 移除版本 |
|---|---|---|
scheduler.alpha.kubernetes.io/critical-pod annotation |
使用 PriorityClass | 已在 1.28 移除 |
7. 与后续文档的关系
建议按这个顺序阅读:
- 本文:理解 control plane 如何把一个 Pod 交给某个节点
docs/pod-on-node-realization-flow.md:理解 kubelet 如何把期望态变成运行态docs/kubelet-status-and-event-feedback-flow.md:理解运行结果如何回到控制面docs/node-heartbeat-and-health-flow.md:理解节点健康信号如何影响控制面判断
8. 代码入口(精简版)
如果读者想从流程跳回实现,可从下面几个入口开始:
- 调度周期:
pkg/scheduler/schedule_one.go:140schedulingCycle(...)
- 绑定周期:
pkg/scheduler/schedule_one.go:263bindingCycle(...)
- 默认绑定器:
pkg/scheduler/framework/plugins/defaultbinder/default_binder.go:51Bind(...)
- kubelet 注册 apiserver pod source:
pkg/kubelet/kubelet.go:283makePodSourceConfig(...)
9. 详细时序图
9.1 正常调度绑定完整时序图
sequenceDiagram
autonumber
participant User as 用户/kubectl
participant API as kube-apiserver
participant ETCD as etcd
participant SCH as kube-scheduler
participant CACHE as Scheduler Cache
participant KL as Kubelet
User->>API: POST /api/v1/namespaces/default/pods
API->>ETCD: 写入 Pod (spec.nodeName="")
ETCD-->>API: 确认
API-->>User: 201 Created
Note over API,SCH: Informer watch 触发
API-->>SCH: watch 推送 Pod ADDED 事件
SCH->>SCH: Pod 入队 PriorityQueue
Note over SCH: 调度周期开始
SCH->>SCH: Pop Pod from queue
SCH->>SCH: schedulingCycle()
Note over SCH,CACHE: PreFilter 阶段
SCH->>CACHE: 读取节点信息
CACHE-->>SCH: 返回节点列表
Note over SCH: Filter 阶段 (遍历所有节点)
loop 每个候选节点
SCH->>SCH: 运行 Filter 插件
alt 节点通过
SCH->>SCH: 保留节点
else 节点不通过
SCH->>SCH: 排除节点
end
end
Note over SCH: Score 阶段
SCH->>SCH: 对可行节点打分
SCH->>SCH: 选择最高分节点
Note over SCH: Assume (乐观缓存)
SCH->>CACHE: 假设 Pod 已绑定到节点
Note over SCH: 绑定周期开始
SCH->>SCH: bindingCycle()
Note over SCH: PreBind 阶段
alt 有 PVC
SCH->>SCH: 等待 PV 绑定
end
Note over SCH: Bind 阶段
SCH->>API: POST /api/v1/namespaces/default/pods/<pod>/binding
API->>ETCD: 更新 Pod.spec.nodeName
ETCD-->>API: 确认
API-->>SCH: 200 OK
Note over SCH: PostBind 阶段
SCH->>SCH: 完成绑定周期
Note over API,KL: Kubelet watch 触发
API-->>KL: watch 推送 Pod UPDATE 事件
KL->>KL: 过滤 spec.nodeName == 本节点
KL->>KL: PodConfig 聚合
KL->>KL: podManager 更新
KL->>KL: podWorkers.UpdatePod()
Note over KL: Pod 进入节点侧处理流程
9.2 调度失败重试时序图
sequenceDiagram
autonumber
participant API as kube-apiserver
participant SCH as kube-scheduler
participant CACHE as Scheduler Cache
participant EXT as External System
API-->>SCH: watch 推送 Pod ADDED
SCH->>SCH: schedulingCycle()
Note over SCH,CACHE: Filter 阶段
SCH->>CACHE: 获取节点列表
CACHE-->>SCH: 返回节点
loop 每个节点
SCH->>SCH: Filter 插件检查
end
alt 无可用节点
SCH->>SCH: 记录 Unschedulable 原因
SCH->>SCH: Pod 进入 unschedulableQ
Note over SCH: 等待下次调度机会
SCH->>SCH: 定期重试或等待事件触发
end
Note over SCH,EXT: 或者 PreBind 失败场景
SCH->>SCH: 选定节点成功
SCH->>CACHE: Assume Pod
Note over SCH: PreBind 阶段
SCH->>EXT: 检查/准备资源
EXT-->>SCH: 失败 (如 CSI 超时)
Note over SCH: 绑定失败处理
SCH->>CACHE: Forget (清理 assume 状态)
SCH->>SCH: 记录失败原因
SCH->>SCH: Pod 重新入队
Note over SCH: 等待重试
9.3 抢占调度时序图
sequenceDiagram
autonumber
participant API as kube-apiserver
participant SCH as kube-scheduler
participant CACHE as Scheduler Cache
Note over SCH: 高优先级 Pod 等待
API-->>SCH: watch 推送高优先级 Pod
SCH->>SCH: schedulingCycle()
Note over SCH: Filter 阶段 - 无可用节点
SCH->>SCH: 所有节点资源不足
Note over SCH: PostFilter 阶段 - 抢占评估
SCH->>SCH: 计算抢占候选
SCH->>SCH: 选择 victim Pods
Note over SCH: 抢占执行
loop 每个 victim Pod
SCH->>API: DELETE /api/v1/namespaces/<ns>/pods/<victim>
API-->>SCH: 200 OK
end
Note over SCH: 等待资源释放
SCH->>SCH: 抢占者重新入队
API-->>SCH: watch 推送 victim Pod DELETED
SCH->>CACHE: 更新节点资源视图
Note over SCH: 重新调度
SCH->>SCH: schedulingCycle()
SCH->>SCH: Filter - 节点现在有足够资源
SCH->>SCH: 继续正常绑定流程
10. 故障排查指南
10.1 常见问题与诊断方法
问题 1:Pod 一直处于 Pending 状态
症状:
1 | kubectl get pods |
排查流程图:
flowchart TD
A[Pod Pending] --> B{检查调度事件}
B --> B1[kubectl describe pod]
B1 --> C{有调度失败事件?}
C -->|0/1 nodes are available| D[资源不足]
D --> D1[增加节点资源]
D --> D2[减少 Pod 资源请求]
D --> D3[清理未使用的 Pod]
C -->|node(s) didn't match Pod's node affinity| E[亲和性不匹配]
E --> E1[检查 nodeSelector]
E --> E2[检查 nodeAffinity]
E --> E3[检查节点 labels]
C -->|node(s) had taints| F[Taint/Toleration 问题]
F --> F1[检查节点 taints]
F --> F2[为 Pod 添加 tolerations]
C -->|pod has unbound immediate PersistentVolumeClaims| G[PVC 未绑定]
G --> G1[检查 PV 是否存在]
G --> G2[检查 StorageClass]
G --> G3[检查 PVC 配置]
C -->|no volume plugin matched| H[存储插件问题]
H --> H1[检查 CSI driver 安装]
诊断命令:
1 | # 查看 Pod 详细事件 |
问题 2:调度延迟过高
可能原因:
- Scheduler 负载过高
- Extender 响应慢
- 大量 Filter/Score 插件
- API Server 响应慢
排查步骤:
flowchart TD
A[调度延迟高] --> B{检查 Scheduler 指标}
B --> C[查看调度延迟 metrics]
C --> C1{延迟在哪里?}
C1 -->|schedulingCycle| D[Filter/Score 阶段慢]
D --> D1[检查自定义插件]
D --> D2[检查 Extender 响应]
C1 -->|bindingCycle| E[绑定阶段慢]
E --> E1[检查 PreBind 插件]
E --> E2[检查 API Server 响应]
C1 -->|queue waiting| F[队列等待长]
F --> F1[检查调度队列深度]
F --> F2[增加 scheduler 并发]
诊断命令:
1 | # 查看调度器 metrics |
问题 3:Pod 绑定成功但 kubelet 未处理
症状:Pod 已绑定到节点,但节点上没有容器运行
排查流程图:
flowchart TD
A[绑定成功但未运行] --> B{检查 spec.nodeName}
B -->|已设置| C{检查 kubelet 状态}
B -->|未设置| D[绑定过程异常]
C -->|正常| E{检查 kubelet watch}
E --> E1[检查 kubelet 日志]
E1 --> E2{有 Pod 更新事件?}
E2 -->|无| F[watch 连接问题]
F --> F1[检查与 API Server 连接]
F --> F2[检查 kubelet 权限]
E2 -->|有| G{检查 podWorkers}
G --> G1[检查 SyncPod 错误]
C -->|异常| H[重启 kubelet]
诊断命令:
1 | # 确认 Pod 已绑定 |
10.2 关键日志关键词
| 组件 | 日志关键词 | 含义 |
|---|---|---|
| Scheduler | Attempting to bind |
开始绑定 |
| Scheduler | Successfully bound |
绑定成功 |
| Scheduler | Failed to bind |
绑定失败 |
| Scheduler | assume pod |
乐观缓存 |
| Scheduler | no nodes available |
无可用节点 |
| Kubelet | SyncPod |
开始同步 Pod |
| Kubelet | Pod update received |
收到 Pod 更新 |
10.3 配置参数参考
| 参数 | 默认值 | 说明 |
|---|---|---|
--concurrency |
1 | 调度并发数 |
--bind-timeout |
600s | 绑定超时时间 |
--profile |
true | 启用性能分析 |
percentageOfNodesToScore |
50% | 打分节点百分比 |
10.4 一键诊断命令
1 | # 调度问题诊断脚本 |
11. 面试题与详细解答
11.1 基础概念题
Q1: Kubernetes 调度器是如何为 Pod 选择节点的?
答案:
Kubernetes 调度器通过两阶段机制为 Pod 选择节点:
第一阶段:过滤 (Predicates/Filter)
- 遍历所有节点,检查硬性约束条件
- 常见过滤条件包括:
PodFitsResources:节点资源是否足够PodMatchNodeSelector:nodeSelector 是否匹配PodToleratesNodeTaints:Pod 是否容忍节点 taintCheckNodeCondition:节点状态是否正常
- 不满足条件的节点被直接排除
第二阶段:打分 (Priorities/Score)
- 对通过过滤的节点进行打分(0-100分)
- 常见打分策略:
LeastRequestedPriority:资源利用率低优先BalancedResourceAllocation:CPU/内存均衡NodeAffinityPriority:节点亲和性InterPodAffinityPriority:Pod 间亲和性
- 选择分数最高的节点
代码实现:
1 | // pkg/scheduler/schedule_one.go:140 |
Q2: scheduler 和 kubelet 之间是如何通信的?
答案:
关键点:scheduler 和 kubelet 不直接通信,而是通过 API Server 间接通信。
1 | Scheduler → API Server → Kubelet |
通信流程:
Scheduler → API Server:
- Scheduler watch API Server 中 Pending 状态的 Pod
- 选定节点后,向 API Server 发送 Binding 请求
- 请求路径:
POST /api/v1/namespaces/{ns}/pods/{name}/binding
API Server 处理:
- 校验 Binding 请求
- 更新 Pod 的
spec.nodeName字段 - 写入 etcd 持久化
API Server → Kubelet:
- Kubelet 通过 informer/watch 监听 Pod 变化
- 过滤
spec.nodeName == 本节点的 Pod - 触发 podWorkers 处理
为什么这样设计:
- 解耦:scheduler 和 kubelet 可以独立部署和升级
- 一致性:API Server 是唯一的状态存储
- 可靠性:即使 scheduler 宕机,已调度的 Pod 仍可正常运行
11.2 进阶原理题
Q3: 什么是 scheduler 的 assume 机制?为什么要这样设计?
答案:
assume 机制是 scheduler 在提交绑定到 API Server 之前,先在本地缓存中”假设”Pod 已经绑定到选定节点。
设计目的:提高调度吞吐量
工作流程:
1 | 1. Scheduler 选定节点 node-1 |
代码实现:
1 | // pkg/scheduler/schedule_one.go |
为什么不直接等待绑定完成:
- 如果每次都等待绑定完成,调度器吞吐量会大幅下降
- assume 让调度器可以在绑定进行时继续调度其他 Pod
- 绑定失败的概率很低,assume 策略整体效率更高
Q4: 调度框架 (Scheduling Framework) 是什么?如何扩展调度器?
答案:
Scheduling Framework 是 Kubernetes 1.19+ 引入的可扩展调度框架,将调度过程拆分为多个扩展点,允许通过插件方式自定义调度逻辑。
扩展点列表:
flowchart LR
A[QueueSort] --> B[PreFilter]
B --> C[Filter]
C --> D[PostFilter]
D --> E[PreScore]
E --> F[Score]
F --> G[NormalizeScore]
G --> H[Reserve]
H --> I[Permit]
I --> J[PreBind]
J --> K[Bind]
K --> L[PostBind]
| 扩展点 | 用途 | 示例场景 |
|---|---|---|
QueueSort |
Pod 排序 | PriorityClass 排序 |
PreFilter |
预处理 | 检查 Pod 依赖 |
Filter |
过滤节点 | 资源检查、亲和性 |
PostFilter |
抢占评估 | 高优先级抢占 |
Score |
打分 | 负载均衡策略 |
Reserve |
预留资源 | assume 阶段 |
Permit |
批准/拒绝 | 等待依赖 Pod |
PreBind |
绑定前准备 | 卷挂载准备 |
Bind |
执行绑定 | 自定义绑定逻辑 |
PostBind |
绑定后清理 | 清理临时资源 |
扩展方式:
内置插件:配置启用/禁用
1
2
3
4
5
6
7
8
9
10
11apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
plugins:
filter:
enabled:
- name: NodeResourcesFit
- name: NodeAffinity
disabled:
- name: NodeName自定义插件:编写 Go 代码实现接口
1
2
3
4type FilterPlugin interface {
Plugin
Filter(ctx context.Context, state *CycleState, pod *v1.Pod, nodeInfo *NodeInfo) *Status
}Scheduler Extender:通过 HTTP 调用外部服务(旧方式)
11.3 故障排查题
Q5: Pod 一直处于 Pending 状态,如何排查?
答案:
排查步骤:
flowchart TD
A[Pod Pending] --> B[kubectl describe pod]
B --> C{查看 Events}
C -->|FailedScheduling| D[调度失败]
D --> D1{失败原因}
D1 -->|0/N nodes available| E[资源不足]
E --> E1[增加节点/减少请求]
E --> E2[检查节点资源]
D1 -->|node(s) didn't match| F[约束不匹配]
F --> F1[检查 nodeSelector]
F --> F2[检查 nodeAffinity]
F --> F3[检查 taint/toleration]
D1 -->|no volume plugin| G[存储问题]
G --> G1[检查 PVC 状态]
G --> G2[检查 StorageClass]
C -->|无 Events| H[scheduler 问题]
H --> H1[检查 scheduler Pod 状态]
H --> H2[检查 scheduler 日志]
诊断命令:
1 | # 1. 查看 Pod 事件 |
常见原因及解决:
| 原因 | 症状 | 解决方案 |
|---|---|---|
| 资源不足 | 0/N nodes are available | 增加节点/减少 requests |
| 节点 taint | node(s) had taints | 添加 toleration |
| 亲和性不匹配 | node(s) didn’t match affinity | 调整 nodeAffinity |
| PVC 未绑定 | pod has unbound PVC | 检查 PV/StorageClass |
| scheduler 故障 | 无调度事件 | 检查 scheduler Pod |
11.4 设计思想题
Q6: 为什么 Kubernetes 选择通过 API Server 间接通信,而不是直接通信?
答案:
架构优势:
松耦合设计
- scheduler、kubelet、controller-manager 可以独立部署
- 组件升级互不影响
- 支持多 scheduler 共存
状态一致性
- API Server + etcd 是唯一的状态存储
- 避免分布式系统中的状态不一致问题
- 所有组件通过 watch 获取一致的视图
故障隔离
- scheduler 宕机不影响已调度 Pod
- kubelet 宕机不影响调度器工作
- 单点故障影响范围小
可观测性
- 所有状态变化都记录在 API Server
- 便于审计和调试
- 支持事件回放
权衡考虑:
| 方面 | 间接通信 | 直接通信 |
|---|---|---|
| 延迟 | 较高(多一跳) | 较低 |
| 可靠性 | 高(持久化) | 依赖连接 |
| 扩展性 | 好 | 差 |
| 复杂度 | 中(watch 机制) | 低 |
| 适用场景 | 大规模分布式 | 小规模集中式 |
结论:对于 Kubernetes 的规模和场景,间接通信的优势远大于其带来的额外延迟。
Q7: 调度器的高可用是如何实现的?
答案:
Kubernetes 调度器采用 Leader Election 机制实现高可用。
工作原理:
sequenceDiagram
participant S1 as Scheduler-1
participant S2 as Scheduler-2
participant API as kube-apiserver
participant ETCD as etcd
Note over S1,S2: 多实例启动,竞争 Leader
S1->>API: 尝试创建/更新 Lease
S2->>API: 尝试创建/更新 Lease
alt S1 获得锁
API-->>S1: 成功,成为 Leader
API-->>S2: 失败,成为 Follower
S1->>S1: 开始调度工作
S2->>S2: 等待,定期重试
else S1 Lease 过期
API-->>S1: Lease 过期
S2->>API: 尝试获取 Lease
API-->>S2: 成功,成为新 Leader
end
配置方式:
1 | # kube-scheduler 配置 |
关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
leaderElect |
true | 是否启用选举 |
leaseDuration |
15s | Leader 身份有效期 |
renewDeadline |
10s | 续约必须在此时限内完成 |
retryPeriod |
2s | 非 Leader 重试间隔 |
故障切换时间:
- 理论最长:
leaseDuration + retryPeriod≈ 17s - 实际通常:10-15s
代码实现:
1 | // staging/src/k8s.io/client-go/tools/leaderelection/leaderelection.go |

