news 2026/10/27 11:25:21

Kruise原地升级源码解析:Pod不重建换镜像的调用链与落地实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kruise原地升级源码解析:Pod不重建换镜像的调用链与落地实践

凌晨两点半盯着一个近千副本的服务做滚动更新,看着 Pod 一个接一个重建、IP 一个个换、连接池一个个断——这是我第一次认真去读 Kruise 原地升级源码的场景。Kruise 是跑在 K8s 上的一组扩展控制器,它带来的 CloneSet、Advanced StatefulSet、SidecarSet 都支持一种能力:把 Pod 里容器的镜像直接换掉,但 Pod 本体不销毁、不重建。这个能力在源码层面叫In-Place Update,落地到 Pod 上其实只做了一件事——改spec.containers[*].image,剩下的全部交给 kubelet 去重启容器。

这篇内容面向三类人:一是在生产上被滚动更新抖动折磨过、想换更新策略的运维和平台开发;二是正在读 Kruise 源码、被 readinessGate、annotation 状态机绕晕的同学;三是想评估"我这个服务到底能不能原地升级"的架构同学。我会沿着一条真实的调用链,从 CloneSet 控制器的入口一直讲到 kubelet 的重启动作,把中间每一步为什么这么设计、哪些参数会咬人、哪些坑我踩过,全部摊开说。源码路径以公开仓库的主干为准,不同小版本会有细节差异,但主干逻辑是稳定的。

1. 先分清"重建"和"原地升级"两件事

1.1 原生滚动更新为什么必然换 Pod

K8s 里 Pod 是最小调度单元,它的spec在创建之后绝大部分字段是不可变的。这不是实现偷懒,而是设计约束:Pod 一旦被调度到某个节点、分配了 IP、挂载了 volume、绑定了设备,它的运行时身份就和这些资源绑死了,随便改字段会让 kubelet 的本地状态和 apiserver 的期望状态对不上。所以 Deployment 的滚动更新走的是"新建 ReplicaSet、新 Pod 起来、旧 Pod 删掉"这条路,本质是换身份,不是改内容。

原生 API 明确允许修改的 Pod 字段只有极少数几个:spec.containers[*].image、spec.initContainers[*].image、spec.activeDeadlineSeconds、spec.tolerations(只允许追加)、spec.terminationGracePeriodSeconds、spec.readinessGates(只允许追加),以及spec.containers[*].resources的部分场景。这份清单是原地升级能够成立的全部物理基础。Kruise 没有绕开 API server 的校验去做越权修改,它就是老老实实利用了"image 可变"这一个口子。

这也解释了一个常见误解:很多人以为原地升级是 Kruise 发明了什么黑科技,其实它做的是编排层面的事——决定什么时候改、改哪些 Pod、改完之后怎么确认改成功了、改的过程中怎么保证流量安全。真正把容器换掉的是 kubelet 这个原生组件。理解这一点,后面所有设计就都顺了。

1.2 原地升级真正保住了什么

我把原地升级的价值分成三层来看。第一层是网络身份:Pod IP 不变、DNS 记录不变、Service 的 Endpoints 里那条记录不变、NetworkPolicy 的匹配不变。对于长连接服务、gRPC 流式调用、注册中心里已经注册了 IP 的服务,这一层的价值最大——不会出现"客户端还在往老 IP 发请求、老 IP 已经没了"的窗口。

第二层是节点侧的资源绑定:Pod 还在原来那台机器上,emptyDir的数据还在,hostPath挂载还在,本地 SSD 缓存还在,GPU、RDMA 网卡、大页内存这些设备绑定还在,甚至 Pod 级别的 cgroup 资源配额也没变。特别是emptyDir,很多人误以为容器重启会丢,其实emptyDir的生命周期跟 Pod 走,容器重启时它原封不动。这一层对训练任务、本地缓存型服务、需要设备独占的负载非常关键。

第三层是调度与拓扑:Pod 不会被重新调度,所以它跟同节点上其他 Pod 的亲和关系、跟节点的拓扑分布、跟本地存储的绑定关系全部保持。对于跨可用区部署、机架感知的服务,这一层能避免"重新调度跑到别的机架去了"这种偶发问题。

反过来,原地升级保不住的东西也要说清楚:容器的可写层会丢(每一次容器重启都是一次全新的 rootfs),容器内的临时文件、进程内存状态、未落盘的日志缓冲全部清空。所以有状态服务如果依赖容器内本地磁盘做持久化,原地升级同样会丢数据,这一点跟重建没区别。

1.3 什么时候不该用原地升级

我见过太多团队一上来就把所有服务切成原地升级,然后踩坑。以下几种情况我会明确建议走重建:

  • 升级需要改env、command、args、resources、ports、volumeMounts。这些字段在 Pod spec 里不可变,Kruise 也没法原地改,只能重建。
  • 升级需要换镜像仓库的imagePullSecrets。较新版本的 Kruise 支持在InPlaceUpdateState里记录NextContainerImages和引用信息来间接处理,但老版本不支持,稳妥做法还是重建。
  • 服务对容器逃逸、镜像供应链安全有强隔离要求,希望每次升级都换一个干净的沙箱。原地升级复用了原来的容器运行时沙箱配置,隔离性上不会比原来更强。
  • 镜像体积特别大(比如好几个 G),节点上没有预热,原地升级时拉镜像的耗时可能比重建还长,因为重建至少能调度到一台已经缓存了镜像的节点。这种情况要么配 ImagePullJob 预热,要么老实用重建。

判断标准其实就一句话:这次变更是不是"只换镜像"。是,原地升级划算;不是,别硬来。

2. 从控制器入口看一次升级怎么被触发

2.1 CloneSet 的 diff 计算与分批划分

CloneSet 的升级入口在pkg/controller/cloneset/sync/cloneset_update.go。整个流程可以拆成"算 diff → 分批 → 执行"三段。算 diff 的核心函数是calculateDiffs,它遍历当前 CloneSet 名下的所有 Pod,对每一个 Pod 判断它需不需要更新。

判断依据是 Pod 上的controller-revision-hash标签和当前 CloneSet 的updateRevision是否一致。不一致就是旧版本 Pod,需要更新。然后是第二个判断:这个 Pod 能不能原地升级?如果新旧两个 ControllerRevision 之间的差异只落在容器镜像上,就标记为InPlaceUpdate;如果差异涉及了不可变字段,就标记为Recreate;如果这个 Pod 已经在更新中,标记为NotUpdate跳过。

这里有个细节值得看一眼:Kruise 在做版本对比时,会先把两个 revision 的spec.template做一次结构化 diff,过滤掉metadata里的噪声字段,再逐字段比对。这个 diff 逻辑在pkg/util/diffutil里,它决定了"这次升级到底能不能原地做"。如果 diff 结果显示只有 image 变了,控制器才会走原地升级分支,否则会降级成重建。这个降级机制很关键,它保证了即使你改错了字段,控制器也不会静默地把变更丢掉。

2.2 partition、maxUnavailable、优先级策略怎么配合

算完 diff 之后,calculateDiffs还要做三件事:按partition过滤、按maxUnavailable限流、按优先级和打散策略排序。

partition的语义是"保留多少个旧版本 Pod 不动"。比如 100 个副本、partition=20,那么控制器会挑 80 个 Pod 升级,留 20 个旧版本。这个参数最常见的用法是灰度:先partition=90升 10%,观察一阵再把partition调小。挑哪 80 个?控制器会对 Pod 列表做一个稳定排序,然后按顺序切分。排序不是随机的,这一点很重要——如果每次 reconcile 排序结果都变,会导致同一个 Pod 被反复选中又反复放弃,升级永远推进不下去。

maxUnavailable控制的是"同一时刻最多允许多少个 Pod 处于不可用状态"。它可以是绝对值,也可以是百分比,CloneSet 里的默认值我记得是 20%。计算逻辑是:用当前可用副本数减去期望副本数,得到"还能再牺牲几个",再和partition的结果取交集。这两个参数一起决定了单批升级的规模。

priorityStrategy在pkg/util/updatesort/priority_sort.go,支持按权重、按创建时间、按 Pod 名字等维度排序。scatterStrategy在同一个目录下,作用是让同一批升级的 Pod 尽量分散到不同的节点、可用区或者自定义 label 分组上。打个比方,maxUnavailable是"一次最多动几个人",scatterStrategy是"动的人要来自不同部门",两者配合才能避免"一次动的全在同一个机架上"这种事故。

2.3 update 阶段真正落到 Pod 上的 patch

分批算完之后,控制器对选中的每个 Pod 调用inplaceupdate包的Update方法。这一步做的事情,用一句话概括:用 JSON Patch 修改 Pod 的镜像字段,同时在 Pod 上留下足够的元信息,让后续的 reconcile 能判断这次更新进行到哪一步了。

具体落了三个东西。第一个是spec.containers[*].image的新值,通过 patch 直接写进去。第二个是 Pod 上的 annotationkruise.io/inplace-update-state,里面是一个 JSON 结构,记录了这次更新的 revision、开始时间戳、更新前各容器的镜像 ID、更新 hash 等。第三个是 Pod status 里的InPlaceUpdateReadycondition,被置为False。这个 condition 是通过 readinessGate 机制生效的,后面单独讲。

把这三样东西分开看,你会发现 Kruise 的设计哲学:Pod spec 是"我想要的",Pod annotation 是"我做到哪了",Pod condition 是"外界该怎么看待我"。三份状态各司其职,控制器每一轮 reconcile 都靠这三份状态的交叉比对来推进流程。这种状态外置的思路,比在控制器内存里维护一个升级进度表要可靠得多,因为控制器重启后状态不会丢。

3. inplaceupdate 包的三段式状态机

3.1 Update:改镜像、记状态、摘流量

Update是入口,它接收一个 Pod 和一个UpdateOptions。UpdateOptions里带着新 revision、新的容器镜像映射、gracePeriodSeconds、preCheck钩子、preReady钩子和一个UpdateContext(用户自定义的透传数据)。

执行顺序我拆开讲。第一步,构造新的 Pod 对象:复制原 Pod,把spec.containers里需要变的容器镜像替换成新值。第二步,把更新前的容器状态序列化进InPlaceUpdateState.LastContainerStatuses,key 是容器名,value 里带着旧的ImageID。这个记录的价值在于:后面判断更新是否完成时,不能只比"当前镜像等于目标镜像",还要能区分"这个容器本来就是这个镜像",否则一个没变的 sidecar 会被误判成已完成。第三步,把这个 state 序列化后 patch 到 Pod 的 annotation 上。第四步,patch Pod status,把InPlaceUpdateReady置为False。

关于第四步的时机,有个容易忽略的点:把 condition 置False和改镜像不是原子操作,两者之间存在毫秒级的时间窗口。而 Endpoints 控制器把 Pod 从 Service 里摘除本身又是异步的,通常要几百毫秒到几秒。所以严格来说,原地升级并不保证"零流量穿透",容器重启的那一瞬间,可能还有极少量请求打进来。这就是gracePeriodSeconds存在的意义——配置一个等待窗口,先把 Pod 标成 NotReady,等 Endpoints 摘干净了再真正改镜像。生产上如果服务对错误率极其敏感,这个值建议配 5 到 10 秒。

preCheck钩子的位置在"决定要不要改镜像"之前。它是 CloneSet 的spec.lifecycle.preCheck配置的,支持 exec 和 httpGet 两种形式。钩子返回失败,本次更新就不会执行,控制器会记录一个 Event 然后等下一轮重试。典型用法是检查下游依赖是否就绪、检查这个 Pod 是不是正在处理长任务。

3.2 Refresh:怎么判断容器真的换完了

Refresh是每轮 reconcile 都会调的方法,它的任务是回答一个问题:这个 Pod 的镜像到底换完了没有?判断逻辑在IsPodAllContainerUpdated里,核心是对每个需要更新的容器做三重校验。

第一重,看status.containerStatuses里这个容器的State.Running是不是非空。容器如果还在Waiting(拉镜像中)或者Terminated(挂了),直接判定未完成。第二重,比较containerStatus.Image和spec.containers[i].Image是不是一致。这里要注意,kubelet 上报的Image字段是它自己规范化之后的镜像字符串,有时候和你 spec 里写的格式不完全一样,所以不能简单地做字符串相等。第三重,如果 spec 里写的是 digest(带@sha256:的形式),那就改比containerStatus.ImageID,看它是不是以这个 digest 结尾。

这个比对逻辑看着简单,实际上是最容易出问题的地方。我踩过的坑:镜像用了:latest这种浮动 tag,kubelet 每次都重新拉,但containerStatus.Image始终是:latest,控制器会认为"没变化",于是永远不触发重启;反过来,如果镜像仓库返回的 manifest 每次 digest 都不同(有些构建系统会给同一 tag 打不同 digest),控制器又会误判成"已经换了"。所以用原地升级,镜像 tag 必须是不可变的,最好是v1.2.3这种语义化版本,或者干脆用 digest 部署。

Refresh还有一个副作用:它会把当前看到的容器状态回写到 annotation 的LastContainerStatuses里。这个回写是为了处理"部分容器已经更新完、部分还没"的中间态。如果控制器中途重启,靠这份记录就能接着往下走,不会把已经换好的容器再判断成没换。

3.3 Complete:把 readinessGate 翻回来

当Refresh返回"全部更新完成"时,控制器调用Complete。这一步做的事情很少但很关键:把 Pod status 里的InPlaceUpdateReadycondition 置为True。

为什么需要这一步?因为 Pod 上挂着 readinessGate。K8s 的规则是:只要 Pod 的spec.readinessGates里声明了某个 condition,那这个 condition 必须是True,Pod 的整体Ready才会是True。Kruise 在创建 Pod 时会通过 mutating webhook 自动给 Pod 注入readinessGates: [{conditionType: InPlaceUpdateReady}]。所以整个升级期间,只要这个 condition 是False,Pod 就一直是 NotReady,Endpoints 里就不会有它。

这个设计非常巧妙:它用 K8s 原生的就绪探针机制实现了"升级期间自动摘流量",不需要 Kruise 自己去操作 Service 或者 Endpoints,也不依赖任何额外的组件。你甚至可以把InPlaceUpdateReady理解成一个由控制器手动控制的、超大号的 readiness probe。

Complete之后还有收尾动作:控制器会更新 Pod 上的controller-revision-hash标签,把它改成新版本。注意这个标签的更新是在Complete之后,不是之前。顺序反了会导致下一轮 reconcile 认为这个 Pod 已经是新版本、跳过它,但容器其实还没换完。这种"标签先于实际状态"的 bug 在自研控制器里非常常见,Kruise 这里的顺序是对的。

4. kubelet 侧发生了什么

4.1 容器 hash 变化与重启路径

控制器把 Pod spec 的镜像改掉之后,apiserver 会把这个变更同步给节点上的 kubelet。kubelet 的SyncPod流程里有一个关键函数computePodActions,它会把当前 Pod spec 里每个容器的期望配置算成一个 hash(containerHash),再和本地已经运行的容器的 hash 比对。hash 的输入包括镜像、command、args、env、ports、resources 等字段——但因为 Pod spec 里只有镜像能改,所以实际触发 hash 变化的只有镜像。

hash 不一致,kubelet 就会对这个容器执行"停止旧容器、启动新容器"的动作。注意,这个动作的粒度是容器级的,不是 Pod 级。一个 Pod 里有三个容器,只有镜像变了的那个会被重启,另外两个完全不受影响。这也是原地升级比重建更细粒度的体现。

停止旧容器时会走正常的优雅终止流程:发 SIGTERM、等terminationGracePeriodSeconds、超时再 SIGKILL。所以你的应用必须正确处理 SIGTERM,否则每次原地升级都会产生一次硬杀。这一点跟滚动更新一样,不是原地升级带来的新问题,但因为原地升级"看起来没重建",很多人会忘记容器其实真的重启了。

启动新容器之前,kubelet 会先检查本地有没有目标镜像。有就直接用,没有就拉。拉取策略由imagePullPolicy决定:IfNotPresent在本地有同 tag 镜像时不拉,Always每次都拉。原地升级场景下,如果你改了 tag,IfNotPresent也会重新拉,所以不用担心本地缓存旧镜像的问题。

4.2 镜像拉取与启动失败的观测点

原地升级失败最常见的原因就是镜像拉不下来。表现是 Pod 的containerStatuses[i].state.waiting.reason变成ErrImagePull或ImagePullBackOff,Pod 一直不 Ready,InPlaceUpdateReady永远是False,控制器卡在这一步反复重试。

排查路径我一般是这么走的:先kubectl describe pod看 Events,里面会有拉取失败的详细原因(认证失败、镜像不存在、网络超时、manifest 格式不对);再看kubectl get pod -o yaml里status.containerStatuses的imageID和image,确认 kubelet 到底在拉哪个地址;如果 spec 里用的是私有仓库的短名,还要确认imagePullSecrets是否在 Pod 上、是否在正确的 namespace 里。

还有一种不那么明显的失败:镜像拉下来了,容器也起来了,但启动就崩,进入CrashLoopBackOff。这时候State.Running是空的,Refresh永远判不过,控制器会一直等。这种场景下 Pod 会一直挂着 NotReady,业务流量全部切走,等于这个 Pod 下线了。所以原地升级之前,新镜像必须在测试环境跑通过,别指望控制器帮你兜底。

4.3 gracePeriodSeconds 与 preCheck/preReady 钩子的实际时序

把几个时间点串起来看,一次完整的原地升级时序是这样的:

  1. 控制器算出这个 Pod 要升级,执行preCheck钩子。失败则终止,等下一轮。
  2. preCheck通过,进入gracePeriodSeconds等待窗口。这个窗口里 Pod 的InPlaceUpdateReady已经被置False,Endpoints 正在把它摘掉。
  3. 窗口结束,patch Pod 的镜像字段。
  4. kubelet 感知到变化,停止旧容器、拉新镜像、启动新容器。
  5. 新容器通过 readiness probe,containerStatuses里的状态变成 Running。
  6. 控制器Refresh判定全部更新完成。
  7. 执行preReady钩子。失败则 Pod 保持 NotReady,等下一轮重试。
  8. preReady通过,Complete把InPlaceUpdateReady置True,Pod 重新 Ready,重新加入 Endpoints。

preReady这个钩子的位置很讲究:它在新容器已经起来、但 Pod 还没对外的那个瞬间执行。典型用法是做一次业务层面的自检,比如"确认这个实例能连上数据库""确认本地缓存已经预热完成",通过了才放流量进来。如果没有这个钩子,新容器一通过 readiness probe 就直接接流量,冷启动的抖动会直接打到用户身上。

5. 动手跑一遍并排查

5.1 环境准备与 CRD 安装

要复现这套流程,需要一个能装 CRD 的 K8s 集群,版本建议 1.22 以上。安装 Kruise 最省事的方式是用官方提供的 helm chart,它会一次性把 CRD、controller、webhook 全部装好。装完之后验证三件事:kubectl get crd | grep kruise能看到 CloneSet、SidecarSet 等 CRD;kubectl -n kruise-system get pod里的控制器全部 Running;kubectl get mutatingwebhookconfiguration里有 Kruise 的配置。第三点很重要,因为 readinessGate 的自动注入就是靠这个 webhook 完成的,如果 webhook 没装好,后面 Pod 不会自动挂上 readinessGate,整个原地升级的流量保护就失效了。

装完之后我建议先跑一个最小 Demo:三个副本的 CloneSet,镜像用一个稳定的小体积镜像,然后手动改镜像触发一次升级,观察 Pod 的 IP 有没有变、condition 怎么翻转。这一步花不了十分钟,但能把整个链路跑通,后面再上生产就有底了。

5.2 一份可抄的 CloneSet 配置

apiVersion: apps.kruise.io/v1alpha1 kind: CloneSet metadata: name: demo-app spec: replicas: 3 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: readinessGates: - conditionType: InPlaceUpdateReady containers: - name: main image: registry.example.com/demo/app:v1.0.0 imagePullPolicy: IfNotPresent readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 3 periodSeconds: 2 updateStrategy: type: InPlaceIfPossible partition: 0 maxUnavailable: 1 inPlaceUpdateStrategy: gracePeriodSeconds: 5 priorityStrategy: orderPriority: - orderedKey: app weightedPriority: weight: 1

几个点解释一下。type: InPlaceIfPossible表示优先尝试原地升级,如果 diff 里有不可变字段就自动降级成重建。readinessGates建议显式写上,虽然有 webhook 自动注入,但显式写更利于排查问题,也方便你在副本里看清这个 Pod 到底挂了哪些 gate。maxUnavailable: 1表示一次只动一个,适合刚开始验证;确认没问题再调大。gracePeriodSeconds: 5给了 Endpoints 足够的摘除时间。

5.3 观测命令与状态变化

升级过程中我常盯三样东西。第一是 Pod 的 IP:

kubectl get pod -l app=demo-app -o custom-columns=NAME:.metadata.name,IP:.status.podIP,NODE:.spec.nodeName,READY:.status.conditions[?(@.type=="Ready")].status

升级前后对比,IP 和 NODE 都应该完全不变。如果变了,说明走的是重建路径,得回头看 diff 是不是包含了不可变字段。

第二是 condition 的翻转:

kubectl get pod <pod-name> -o jsonpath='{range .status.conditions[*]}{.type}={.status} {end}'

升级过程中你会看到InPlaceUpdateReady=False和Ready=False,升级完成后两者都变回 True。如果InPlaceUpdateReady一直是 False,说明控制器认为更新还没完成,这时候去看 annotation 里的 state:

kubectl get pod <pod-name> -o jsonpath='{.metadata.annotations.kruise\.io/inplace-update-state}' | jq .

这个 JSON 里的lastContainerStatuses和revision能告诉你更新卡在哪个容器上。

第三是 controller 的日志和 Event:

kubectl -n kruise-system logs -l control-plane=cloneset-controller --tail=200 kubectl get events --field-selector involvedObject.name=<pod-name> --sort-by=.lastTimestamp

原地升级的关键节点都会打 Event,包括开始更新、preCheck 失败、preReady 失败等。

5.4 常见问题速查表

现象可能原因排查动作
Pod 被重建了,IP 变了diff 里包含不可变字段,自动降级成重建对比新旧 ControllerRevision 的 template,确认只改了 image
升级卡住不动,condition 一直 False新容器 CrashLoopBackOff,State.Running为空kubectl describe pod看容器最后状态和日志
升级卡住,镜像拉取失败私有仓库认证、镜像名拼错、网络不通看 Events 里的ErrImagePull详情,检查imagePullSecrets
升级后 condition 变 True 但服务仍有 5xxgracePeriodSeconds 太小,Endpoints 没摘干净调大到 10 秒,或给 readinessProbe 加 initialDelay
部分 Pod 一直不升级partition 设置过大,或 priorityStrategy 排序把某些 Pod 排到了后面检查partition值,确认副本数和 partition 的关系
同一 Pod 反复被选中升级镜像 tag 是浮动的,每次 digest 都不同改用不可变 tag 或 digest 部署
preCheck 一直失败钩子里的检查条件不满足,比如依赖服务不可用看 controller 日志里的钩子返回内容,手动执行一遍钩子里的命令

再补两个表格里放不下、但很值得说的经验。

第一个是关于maxUnavailable的计算。很多人以为配了 2 就是"一次动 2 个",实际上控制器算的是"当前可用数 - 期望数 + 2",如果集群里已经有 Pod 因为别的原因 NotReady,这个预算会被吃掉,导致实际能动的 Pod 数量变少甚至为 0。排查"为什么一个都没升"的时候,先看看kubectl get cloneset的AVAILABLE列和replicas是不是一致。

第二个是关于回滚。原地升级的回滚就是把spec.template里的镜像改回旧值,控制器会再走一遍原地升级流程把容器换回去。但这里有个陷阱:如果新版本镜像已经把数据格式改了、旧版本容器读不了,回滚会直接失败。所以有状态服务的原地升级,必须保证新旧版本的数据格式兼容,或者升级前先做好数据迁移和备份。

6. 几个源码里值得单独拎出来的设计

6.1 为什么用 annotation 而不是 finalizer 记录状态

Kruise 用kruise.io/inplace-update-state这个 annotation 记录更新状态,而不是用 finalizer。原因很实际:finalizer 会阻止 Pod 被删除,如果更新过程中控制器挂了,Pod 就永远删不掉,运维得手动去摘 finalizer,很容易出事。annotation 没有这个副作用,它只是一份记录,控制器重启后读一下就能恢复上下文,读不到就当成一次新更新重新开始,最坏情况是多重启一次容器,不会造成资源泄漏。

这个取舍我觉得很值得借鉴:做控制器时,能不用 finalizer 就不用。finalizer 适合用在"删除前必须做某件事"的场景,比如清理外部资源;单纯的状态记录用 annotation 或者 status 字段更安全。

6.2 readinessGate 注入为什么放在 webhook 里

readinessGate 需要在 Pod 创建时写进 spec,创建之后就不能再加了(readinessGates只允许追加,但实际使用中追加的时机很微妙)。Kruise 选择在 mutating webhook 里做这件事,因为 webhook 是 Pod 创建链路上唯一能在对象落库前修改 spec 的地方。这个位置的选择还有一个好处:它不区分 Pod 是由 CloneSet 创建的还是由 Advanced StatefulSet 创建的,只要带了对应的 label,就统一注入,逻辑复用得很干净。

如果你自己写类似的控制器,这个模式可以直接抄:用 webhook 做创建时的注入,用 controller 做运行时的状态推进。两者职责边界清楚,调试起来也容易定位问题在哪个环节。

6.3 Advanced StatefulSet 和 SidecarSet 的差异点

Advanced StatefulSet 的原地升级实现在pkg/controller/statefulset/stateful_set_control.go,复用的是同一个inplaceupdate包,差异主要在批次划分上:它有严格的序号顺序(默认从大到小),还支持podUpdatePolicy和partition组合出更复杂的灰度策略。因为 StatefulSet 的 Pod 通常有身份依赖(比如主从、分片),所以它不会像 CloneSet 那样做任意的打散和优先级排序,顺序语义更强。

SidecarSet 的原地升级在pkg/controller/sidecarset/sidecarset_processor.go,它的特殊之处在于 sidecar 容器是 webhook 注入的,不在用户的原始 Pod template 里。所以它在更新前要先确认pod.status.containerStatuses里确实有这个 sidecar 容器,否则 patch 一个不存在的容器名会直接报错。另外 SidecarSet 是按 namespace 和 label selector 匹配 Pod 的,一个 Pod 可能被多个 SidecarSet 命中,所以它在更新时要处理"多个 SidecarSet 各自更新各自的容器"这种并发场景,靠的是每个 SidecarSet 只 patch 自己负责的那部分容器。

7. 生产落地前我会做的几件事

第一件,先在非核心服务上跑一周。选一个日请求量中等、有完整监控的服务,把更新策略切成InPlaceIfPossible,观察几个指标:升级期间的服务错误率、单个 Pod 的升级耗时、有没有出现 condition 卡住的情况。一周下来基本能暴露所有配置问题。

第二件,把gracePeriodSeconds和 readinessProbe 的参数调好。我的经验值是gracePeriodSeconds配 5 到 10 秒,readinessProbe 的initialDelaySeconds配 3 到 5 秒,failureThreshold给 3 次。这三个参数一起决定了"新容器起来后多久才接流量",配小了容易把冷启动的请求打崩,配大了升级变慢。

第三件,给镜像打上不可变 tag。这条是硬性要求,前面已经说过原因。如果团队用的是流水线自动构建,让流水线在 push 镜像时自动生成一个基于 commit sha 的 tag,部署时用这个 tag,比手工维护版本号可靠得多。

第四件,提前预热镜像。如果集群节点多、镜像大,配一个 ImagePullJob 在升级前把所有节点的镜像拉好,原地升级的耗时能缩短一半以上。这一步在跨机房、跨可用区的集群里尤其值得做。

第五件,准备好回滚预案。原地升级的回滚虽然就是把镜像改回去,但要确认三件事:新版本有没有做不可逆的数据变更、旧镜像在仓库里还在不在、回滚时的maxUnavailable是不是够大能快速切回去。我见过一次回滚因为旧镜像被清理了而卡了半小时,教训很实在。

最后说个我自己的体会。原地升级这个能力,表面上是省了几秒钟的 Pod 重建时间,实际价值在于它把"升级"从一次身份变更降级成了一次内容变更。身份不变,上下游的假设就都不用改,注册中心不用重新注册、长连接不用重连、告警规则不用调整、容量评估不用重算。这种"变更半径"的缩小,才是它在大规模集群里真正值钱的地方。至于源码,读一遍就够,剩下的时间花在把参数调对、把监控补齐上,收益高得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 11:24:41

中位值滤波:原理、优化与工业应用实践

1. 中位值滤波法&#xff1a;为什么它比平均值更适合你的数据&#xff1f; 第一次接触中位值滤波是在2018年处理工业传感器数据时。当时产线上的温度传感器每隔几分钟就会突然跳变几十度&#xff0c;用传统的移动平均法处理后&#xff0c;这些异常值仍然会显著影响结果。直到一…

作者头像 李华
网站建设 2026/10/27 11:23:51

从分数匹配到朗之万采样:NCSN图像生成全流程拆解

前阵子有位朋友在复现图像生成模型时卡住了&#xff0c;他把论文里的公式抄了三遍&#xff0c;代码也照着写了一遍&#xff0c;结果训练损失一路往下掉&#xff0c;采样出来的图却是一团彩色噪点。我去看他代码&#xff0c;问题出在噪声级别和采样步长的配比上——这套东西的坑…

作者头像 李华
网站建设 2026/10/27 11:23:45

信息学竞赛核心能力与高效备赛指南

1. 信息学竞赛的底层逻辑与能力要求 信息学竞赛本质上是一场在有限时间内解决复杂计算问题的脑力马拉松。与日常编程不同&#xff0c;它考察的是选手在高压环境下快速分析问题、设计算法并实现代码的综合能力。根据我多年带队参赛的经验&#xff0c;优秀选手通常具备三个核心特…

作者头像 李华
网站建设 2026/10/27 11:23:36

解决IntelliJ IDEA 2025.3 Maven依赖爆红问题

1. 问题现象与背景分析 最近升级到IntelliJ IDEA 2025.3版本后&#xff0c;不少开发者遇到了一个恼人的问题&#xff1a;每次打开项目时&#xff0c;Maven依赖都会出现大面积爆红&#xff08;红色波浪线提示&#xff09;&#xff0c;尽管这些依赖实际上是可以正常下载和使用的。…

作者头像 李华
网站建设 2026/10/27 11:23:23

Linux内核rootkit原理:系统调用劫持与内核态权限控制

简介&#xff1a;这是一份面向Linux内核安全与逆向学习者的轻量级Rootkit开发实践资源&#xff0c;适合具备C语言基础和Linux系统编程经验的中初级开发者深入理解内核模块机制、进程管理与隐藏技术。资源包含46个文件&#xff0c;以9个可编译运行的sample示例为核心&#xff0c…

作者头像 李华
网站建设 2026/10/27 11:23:21

从 git 到 co-:英语词源拆解与软件命名自嘲

前阵子有人拿着一个单词来问我&#xff0c;说他在英式剧里看到一句 “You silly git”&#xff0c;转头打开终端又看见 git status &#xff0c;实在接受不了一个词同时承担这两种身份。顺带他还问了另一串词&#xff1a;grill、practice、drill、evacuate、premises&#xf…

作者头像 李华