Pod 调度与绑定通信流程

目标

这篇文档只回答一个核心问题:一个 Pod 是怎么从“等待调度”变成“被某个节点上的 kubelet 感知到”的?

重点放在组件之间的通信与职责边界,而不是调度器内部打分算法细节。

一句话摘要

kube-scheduler 负责为 Pod 选节点,kube-apiserver 负责持久化绑定结果,kubelet 通过监听 API Server 中属于本节点的 Pod 更新来感知这次分配。


1. 流程总览

从通信视角看,这条链路可以拆成 3 个阶段:

  1. 调度决策阶段:scheduler 为 Pod 选择目标节点。
  2. 绑定落盘阶段:scheduler 通过 API Server 把绑定结果写回集群状态。
  3. 节点感知阶段:目标节点上的 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,条件 PodScheduledFalse

通信方向: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. 与后续文档的关系

建议按这个顺序阅读:

  1. 本文:理解 control plane 如何把一个 Pod 交给某个节点
  2. docs/pod-on-node-realization-flow.md:理解 kubelet 如何把期望态变成运行态
  3. docs/kubelet-status-and-event-feedback-flow.md:理解运行结果如何回到控制面
  4. docs/node-heartbeat-and-health-flow.md:理解节点健康信号如何影响控制面判断

8. 代码入口(精简版)

如果读者想从流程跳回实现,可从下面几个入口开始:


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
2
3
kubectl get pods
NAME READY STATUS RESTARTS AGE
pod-1 0/1 Pending 0 5m

排查流程图

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
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看 Pod 详细事件
kubectl describe pod <pod-name> -n <namespace>

# 查看 Pod 调度相关事件
kubectl get events --field-selector involvedObject.name=<pod-name> -n <namespace>

# 查看调度器日志
kubectl logs -n kube-system <scheduler-pod> | grep <pod-name>

# 检查节点资源
kubectl describe nodes | grep -A 5 "Allocated resources"

# 检查节点 taints
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints

问题 2:调度延迟过高

可能原因

  1. Scheduler 负载过高
  2. Extender 响应慢
  3. 大量 Filter/Score 插件
  4. 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
2
3
4
5
6
7
8
# 查看调度器 metrics
kubectl get --raw /metrics | grep scheduler | grep -E "(queue|binding|scheduling)"

# 检查调度队列深度
kubectl get --raw /metrics | grep scheduler_queue_incoming_pods_total

# 检查 API Server 延迟
kubectl get --raw /metrics | grep apiserver_request_duration_seconds

问题 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
2
3
4
5
6
7
8
9
10
11
# 确认 Pod 已绑定
kubectl get pod <pod-name> -o jsonpath='{.spec.nodeName}'

# 检查目标节点 kubelet 日志
journalctl -u kubelet -n 100 | grep <pod-name>

# 在节点上检查 Pod 状态
crictl pods | grep <pod-name>

# 检查 kubelet 与 API Server 连接
curl -k https://<api-server>:6443/api/v1/pods?watch=true&fieldSelector=spec.nodeName=<node-name>

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
2
3
4
5
6
7
8
9
10
11
# 调度问题诊断脚本
echo "=== Pending Pods ===" && \
kubectl get pods --field-selector=status.phase=Pending -A && \
echo -e "\n=== Recent Scheduler Events ===" && \
kubectl get events -A --field-selector reason=Scheduled --sort-by='.lastTimestamp' | tail -20 && \
echo -e "\n=== Unschedulable Events ===" && \
kubectl get events -A --field-selector reason=FailedScheduling --sort-by='.lastTimestamp' | tail -20 && \
echo -e "\n=== Node Resources ===" && \
kubectl top nodes && \
echo -e "\n=== Node Taints ===" && \
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints

11. 面试题与详细解答

11.1 基础概念题

Q1: Kubernetes 调度器是如何为 Pod 选择节点的?

答案

Kubernetes 调度器通过两阶段机制为 Pod 选择节点:

第一阶段:过滤 (Predicates/Filter)

  • 遍历所有节点,检查硬性约束条件
  • 常见过滤条件包括:
    • PodFitsResources:节点资源是否足够
    • PodMatchNodeSelector:nodeSelector 是否匹配
    • PodToleratesNodeTaints:Pod 是否容忍节点 taint
    • CheckNodeCondition:节点状态是否正常
  • 不满足条件的节点被直接排除

第二阶段:打分 (Priorities/Score)

  • 对通过过滤的节点进行打分(0-100分)
  • 常见打分策略:
    • LeastRequestedPriority:资源利用率低优先
    • BalancedResourceAllocation:CPU/内存均衡
    • NodeAffinityPriority:节点亲和性
    • InterPodAffinityPriority:Pod 间亲和性
  • 选择分数最高的节点

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// pkg/scheduler/schedule_one.go:140
func (sched *Scheduler) schedulingCycle(
ctx context.Context,
state *framework.CycleState,
pod *v1.Pod,
...
) (result ScheduleResult, err error) {
// 1. PreFilter 阶段
// 2. Filter 阶段 - 过滤节点
// 3. PostFilter 阶段 - 抢占评估
// 4. PreScore 阶段
// 5. Score 阶段 - 打分
// 6. 选择最高分节点
}

Q2: scheduler 和 kubelet 之间是如何通信的?

答案

关键点:scheduler 和 kubelet 不直接通信,而是通过 API Server 间接通信。

1
Scheduler → API Server → Kubelet

通信流程

  1. Scheduler → API Server

    • Scheduler watch API Server 中 Pending 状态的 Pod
    • 选定节点后,向 API Server 发送 Binding 请求
    • 请求路径:POST /api/v1/namespaces/{ns}/pods/{name}/binding
  2. API Server 处理

    • 校验 Binding 请求
    • 更新 Pod 的 spec.nodeName 字段
    • 写入 etcd 持久化
  3. 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
2
3
4
5
6
1. Scheduler 选定节点 node-1
2. assume: 更新 scheduler cache,标记 Pod 绑定到 node-1
3. 继续调度下一个 Pod(此时 scheduler cache 已反映新的资源使用)
4. 异步提交 binding 到 API Server
5. 如果 binding 成功 → assume 变成事实
6. 如果 binding 失败 → forget 清理 assume 状态,重新调度

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// pkg/scheduler/schedule_one.go
// 1. assume 到 cache
if err := sched.assume(assumedPod, host); err != nil {
return err
}

// 2. 异步绑定
go func() {
err := sched.bind(ctx, assumedPod, host, state, start)
if err != nil {
// 绑定失败,清理 assume 状态
sched.forget(assumedPod)
}
}()

为什么不直接等待绑定完成

  • 如果每次都等待绑定完成,调度器吞吐量会大幅下降
  • 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. 内置插件:配置启用/禁用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    apiVersion: kubescheduler.config.k8s.io/v1
    kind: KubeSchedulerConfiguration
    profiles:
    - schedulerName: default-scheduler
    plugins:
    filter:
    enabled:
    - name: NodeResourcesFit
    - name: NodeAffinity
    disabled:
    - name: NodeName
  2. 自定义插件:编写 Go 代码实现接口

    1
    2
    3
    4
    type FilterPlugin interface {
    Plugin
    Filter(ctx context.Context, state *CycleState, pod *v1.Pod, nodeInfo *NodeInfo) *Status
    }
  3. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 查看 Pod 事件
kubectl describe pod <pod-name> -n <namespace>

# 2. 查看调度失败事件
kubectl get events -A --field-selector reason=FailedScheduling

# 3. 检查节点资源
kubectl describe nodes | grep -A 5 "Allocated resources"

# 4. 检查节点 taints
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints

# 5. 检查 scheduler 日志
kubectl logs -n kube-system <scheduler-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 间接通信,而不是直接通信?

答案

架构优势

  1. 松耦合设计

    • scheduler、kubelet、controller-manager 可以独立部署
    • 组件升级互不影响
    • 支持多 scheduler 共存
  2. 状态一致性

    • API Server + etcd 是唯一的状态存储
    • 避免分布式系统中的状态不一致问题
    • 所有组件通过 watch 获取一致的视图
  3. 故障隔离

    • scheduler 宕机不影响已调度 Pod
    • kubelet 宕机不影响调度器工作
    • 单点故障影响范围小
  4. 可观测性

    • 所有状态变化都记录在 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
2
3
4
5
6
7
# kube-scheduler 配置
leaderElection:
leaderElect: true
leaseDuration: 15s # Lease 有效期
renewDeadline: 10s # 续约超时
retryPeriod: 2s # 重试间隔
resourceLock: leases # 使用 Lease API

关键参数

参数 默认值 说明
leaderElect true 是否启用选举
leaseDuration 15s Leader 身份有效期
renewDeadline 10s 续约必须在此时限内完成
retryPeriod 2s 非 Leader 重试间隔

故障切换时间

  • 理论最长:leaseDuration + retryPeriod ≈ 17s
  • 实际通常:10-15s

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// staging/src/k8s.io/client-go/tools/leaderelection/leaderelection.go
func (le *LeaderElector) Run(ctx context.Context) {
defer func() {
runtime.HandleCrash()
le.config.Callbacks.OnStoppedLeading(ctx)
}()

if !le.acquire(ctx) {
return // 竞选失败
}

ctx, cancel := context.WithCancel(ctx)
defer cancel()
go le.config.Callbacks.OnStartedLeading(ctx)
le.renew(ctx) // 持续续约
}