WEBKT

打破 PLEG 抖动噩梦:Kubelet syncPod 核心机制与 CRI 异步化演进深度解析

11 0 0 0

在 Kubernetes 大规模集群的管理实践中,任何一位资深 SRE 或 K8s 研发工程师,大概率都遭遇过那个令人头疼的报错——PLEG is unhealthy

伴随而来的,通常是节点变为 NotReady、Pod 状态大面积卡死在 ContainerCreating、甚至整个集群控制面发生连锁雪崩。这些表象的背后,核心症结往往直指 Kubelet 最关键的控制循环:syncPod 流程,以及它与容器运行时(CRI)之间那脆弱的、同步阻塞式的通信机制。

要彻底解决高并发、大规模场景下的 Pod 启动延迟与节点虚假离线,必须深入 Kubelet 源码的底层,厘清 syncPod 的串行本质,并探讨如何通过 CRI 异步化进行极限优化。


剖析 syncPod:Kubelet 核心状态机的单线程忧虑

在 Kubelet 的架构中,每个 Pod 的生命周期变迁都由对应的 podWorker 驱动。podWorker 接收到变更事件后,最终会调用 kubeRuntimeManager.syncPod 方法。

我们可以将 syncPod 理解为一个高精度的“状态对齐器”。它的终极任务是:计算出当前 Pod 的实际运行状态与 API Server 期望的 Spec 状态之间的差距,并通过调用 CRI 接口,把实际状态一步步“逼近”到期望状态。

+-----------------------------------------------------------------------------+
|                                  syncPod                                    |
+-----------------------------------------------------------------------------+
                                       |
        +------------------------------+------------------------------+
        |                              |                              |
        v                              v                              v
+---------------+              +---------------+              +---------------+
| 1. 计算状态差 |              | 2. 销毁无用容器 |              | 3. 创建 Sandbox|
| Compute Delta |              | Kill Pod/Cont |              | RunPodSandbox |
+---------------+              +---------------+              +---------------+
                                                                      |
                               +--------------------------------------+
                               |
                               v
                       +---------------+              +---------------+
                       | 4. 拉取镜像   |              | 5. 启动新容器 |
                       | PullImage     | -----------> | StartContainer|
                       +---------------+              +---------------+

这个看似清晰的线性工作流,在实际运行中隐藏着巨大的性能陷阱:全链路的同步阻塞

  1. 串行执行的 CRI 调用:从 RunPodSandbox(创建沙箱环境)、PullImage(拉取镜像)、CreateContainer(创建容器)到 StartContainer(启动容器),这些操作都是串行发送给 Containerd 或 Docker 的。
  2. 缺乏并发的内部容器初始化:如果一个 Pod 里定义了多个业务容器,默认情况下,这些容器的创建和启动也是顺序进行的。
  3. 网络与 I/O 抖动敏感:上述任何一个 CRI 步骤由于磁盘 I/O 挂起、网络延迟、或者容器运行时自身锁竞争导致响应变慢,整个 syncPod 协程(Goroutine)就会被死死卡住。

由于 podWorker 为每个 Pod 分配了独立的协程,单个 Pod 被卡住本不该影响其他 Pod。但致命的是,Kubelet 内部还存在一个全局的生命周期事件生成器(PLEG,Pod Lifecycle Event Generator)


PLEG 的性能死结与 CRI 同步机制

PLEG 的职责是定期(默认 1 秒)调用 CRI 的 ListPodSandboxListContainers 接口,获取节点上所有容器的最新状态,并与本地缓存比对,生成事件(如 ContainerStartedContainerDied)发送给 podWorkers

+--------------------------------------------------------------------+
|                       PLEG Relist Loop                             |
+--------------------------------------------------------------------+
  |
  +--> 1. 发起 CRI ListContainers() / ListPodSandbox() ---> (等待 gRPC 响应)
  |
  +--> 2. 比较本地缓存与最新状态,生成 Event
  |
  +--> 3. 更新全局时间戳 (Timestamp)

syncPod 发生大量阻塞,底层容器运行时(如 Containerd)因为处理积压的创建请求而出现性能瓶颈或锁竞争时,PLEG 发起的 ListContainers gRPC 调用同样会被阻塞。

如果 PLEG 的一次 relist 耗时超过了 3 秒,Kubelet 就会判定 PLEG 处于不健康状态,上报 PLEG is unhealthy。随后,节点控制器可能会将该节点标记为 NotReady,触发集群的大规模无意义漂移。

同步阻塞式 CRI 设计,正是导致高并发场景下 Kubelet 脆弱不堪的底层根源。


演进之路:从 Evented PLEG 到异步化探索

为了打破这一僵局,Kubernetes 社区及各大云厂商在过去几个版本中进行了漫长的技术演进。

1. 从主动轮询到事件驱动:Evented PLEG (KEP-3386)

传统的 PLEG 依赖周期性轮询(Polling),不管容器状态变没变,每秒都要调用一次 CRI,这在单节点运行上百个容器时会产生巨大的无用开销。

为了解决这一问题,社区引入了 Evented PLEG

  • 机制:利用 CRI 本身的 Event Stream 机制(GetContainerEvents),让容器运行时将状态变更主动“推送”给 Kubelet。
  • 效果:极大减少了 PLEG 周期性轮询的频率。在容器状态未发生频繁变动时,避免了对 Containerd 的无效查询,释放了大量的 gRPC 通道带宽和运行时 CPU。

2. 核心瓶颈的残留:syncPod 的串行重石

虽然 Evented PLEG 减轻了状态查询的负担,但并没有从根本上解决 syncPod 内部创建容器时的同步阻塞问题。

例如,当调度器在瞬间向一个节点调度了 50 个 Pod,Kubelet 会并发启动 50 个 podWorker。这时,50 个 syncPod 流程同时向 Containerd 发起 RunPodSandboxPullImage 请求。

  • 每一个 Pod 内部的 Init Container 必须顺序执行。
  • 即使没有 Init Container,普通的多个业务容器在 syncPod 代码中也往往是串行初始化的。
  • 当底层存储介质(如云盘)出现 I/O 写入延迟时,CRI 调用时延从毫秒级飙升到数秒,瞬间导致并发处理通道拥堵。

优化空间:如何对 Kubelet 与 CRI 进行极限改造?

面对高并发、高弹性的边缘计算、Serverless 容器或 AI 微调训练场景,默认的 Kubelet 架构仍显吃力。以下是几个经过业界大规模验证的高级优化方向:

优化一:非阻塞式/解耦式镜像拉取(Decoupled Image Pulling)

syncPod 的耗时占比中,PullImage 通常占据了 80% 以上。默认情况下,syncPod 会同步等待镜像拉取完毕。

优化方案:

  • PullImage 逻辑从主 syncPod 控制循环中剥离。
  • 当检测到镜像不存在时,Kubelet 向一个异步的镜像拉取队列投递任务,并立即结束当前的 syncPod 周期,将 Pod 状态设置为 ImagePullBackOffPending (Pulling)
  • 异步拉取协程负责向 CRI 发送 PullImage 请求。拉取完成后,触发一个事件重新激活 podWorker 执行 syncPod
  • 这样可以避免 podWorker 的物理协程因为拉取超大镜像(如几十G的 AI 基础镜像)而被长期霸占,提升了节点整体的响应吞吐。

优化二:引入客户端异步 Shim(CRI Async Proxy/Shim)

在不侵入性修改 Kubelet 核心代码的前提下,在 Kubelet 与 Containerd 之间架设一个本地的高性能 CRI Proxy(如基于 Rust/Go 编写的 Unix Socket 代理)。

+---------+         +-----------------+         +--------------------+
| Kubelet | ------> |  CRI Async Shim | ------> | Containerd (gRPC)  |
+---------+         +-----------------+         +--------------------+
                         |
                         +---> 1. 拦截 RunPodSandbox / CreateContainer
                         |
                         +---> 2. 立即返回 Mock 成功响应 (状态为 Starting)
                         |
                         +---> 3. 后台真实、并发地向 Containerd 提交请求
  • 延迟写回:当 Kubelet 发起 RunPodSandbox 时,Proxy 拦截该请求并将其入队,同时向 Kubelet 返回一个“已接收/处理中”的模拟响应。
  • 并发控制与合并:Proxy 内部实现更精细的并发控制(如 Token Bucket),限制发送到真实运行时的并发请求数,防止瞬时过载。
  • 对于幂等的查询操作(如 ListContainers),Proxy 在本地维护高一致性的快照缓存,直接秒级返回,彻底杜绝 PLEG 因运行时阻塞而超时。

优化三:Kubelet 内部容器并发启动(Concurrent Container Spin-up)

修改 Kubelet 源码中对多容器的处理逻辑。在 pkg/kubelet/kuberuntime/kuberuntime_container.go 中,将顺序创建容器的代码改造为利用 sync.WaitGroup 或 Go 协程池进行并发初始化。

// 概念性改造代码示例
g, ctx := errgroup.WithContext(parentCtx)
for _, container := range pod.Spec.Containers {
    container := container
    g.Go(func() error {
        // 异步并发执行容器创建与启动
        return m.startContainer(ctx, podSandboxID, &container, ...)
    })
}
if err := g.Wait(); err != nil {
    // 统一处理错误
}

注意:在改造时需要保留对 Init 容器和具有依赖关系的 Ephemeral 容器的拓扑排序,仅对无依赖的常规业务容器进行并发化。

优化四:精细化读写锁与状态缓存

Kubelet 在运行过程中,需要频繁查询 Pod 卷(Volumes)挂载状态、设备分配情况(GPU/NIC)以及 Cgroup 路径信息。这些操作往往伴随着全局锁或文件 I/O 锁。

  • 优化策略:将 Kubelet 内部的全局大锁拆分为分段锁(Segmented Lock),或者使用无锁的数据结构(如原子指针 atomic.Valuesync.Map)来存储 Pod 的最新 Runtime 状态。
  • 缓存加速:对于不经常变动的设备信息及路径信息,增加短期内存缓存,减少向系统内核(sysfs/cgroupfs)和容器运行时的查询频率。

总结与落地建议

Kubelet 的 syncPod 异步化与 CRI 性能优化,是 Kubernetes 步入深水区后的必经之路。

对于绝大多数企业级生产环境,我们建议:

  1. 优先开启 Evented PLEG 功能:这是社区原生提供的、性价比最高的抗抖动方案。
  2. 调优底层组件参数:合理配置 Containerd 的 max_concurrent_downloads(最大并发下载限制),避免并发拉取打爆磁盘 I/O。
  3. 架构层解耦:在极致的高弹、超大规模场景下,考虑引入自主研发的 CRI 异步代理层,或者针对 Kubelet 源码进行定制化的镜像异步化改造。

只有将这一条核心管道彻底疏通,Kubernetes 节点才能真正告别“虚假离线”的阴霾,发挥出极致的调度弹性。

云原生极客 KubernetesKubeletCRI

评论点评