OpenKruise 是基于 CRD 的拓展,包含了很多应用工作负载和运维增强能力,本系列文章会从源码和底层原理上解读各个组件,以帮助大家更好地使用和理解 OpenKruise。让我们开始 OpenKruise 的源码之旅吧!
1. 背景
OpenKruise 是针对 Kubernetes 的增强能力套件,聚焦于云原生应用的部署、升级、运维、稳定性防护等领域。OpenKruise 提供的绝大部分能力都是基于 CRD 扩展来定义,它们不存在于任何外部依赖,可以运行在任意纯净的 Kubernetes 集群中。它包含了一系列增强版本的 Workloads(工作负载),比如 CloneSet、Advanced StatefulSet、Advanced DaemonSet、BroadcastJob 等, 它们不仅支持类似于 Kubernetes 原生 Workloads 的基础功能,还提供了如原地升级、可配置的扩缩容/发布策略、并发操作等。
其中原地升级是 OpenKruise 的核心功能, 它只需要使用新的镜像重建 Pod 中的特定容器,整个 Pod 以及其中的其他容器都不会被影响。因此它带来了更快的发布速度,以及避免了对其他 Scheduler、CNI、CSI 等组件的负面影响, 像 CloneSet、AdvancedStatefulSet、AdvancedDaemonSet、SidecarSet 的热更新机制,ContainerRestartRequest 等功能都依赖原地升级。理解原地升级之后再去研究其他组件就会事半功倍,所以本文首先带大家分析原地升级的源码,来一窥其底层原理。
有关原地升级的使用和介绍可以先阅读这篇文档,下面让我们开始解读源码。
2. 源码解读
2.1 Before Pod Update
2.1.1 reconcile 入口函数
我们以 CloneSet 为例,当 CloneSet 更新后,相应的 controller 感知到资源变化,此时代码会走到 cloneset_controller.go
的 doReconcile
函数,该函数是处理 CloneSet 更新的主干入口。
1 | func (r *ReconcileCloneSet) doReconcile(request reconcile.Request) (res reconcile.Result, retErr error) |
2.1.2 syncCloneSet
经过一系列检查后,执行到 syncCloneSet
, 该函数主要是处理 CloneSet 的 scale 和 update pod 的细节, 我们这里只关注 update 操作。
1 | func (r *ReconcileCloneSet) syncCloneSet( |
Kruise 专门为这两个操作声明了两个 interface,
1 | // Interface for managing pods scaleing and updating. |
cloneset_update.go
中实现了上述接口。
1 | func (c *realControl) Update(cs *appsv1alpha1.CloneSet, |
2.1.3 Pod 状态检查
在对 pod 执行真正的 Update 之前,Kruise 做了很多的校验,比如更新 pod 的 lifecycle 的 state,设置可以更新的最大数量,过滤掉不符合 update 条件的 pod等。下面代码的注释中给出了详细的分析。
有关 lifecycle(生命周期钩子) 的更多介绍,可以继续阅读 这篇文档
1 | for i, pod := range pods { |
这里有关于 PUB 的详细介绍,感兴趣的可以继续深入了解。
2.2 Check About Pod Inplace Update
2.2.1 选择 UpdateStrategy = Inplace
进入到 updatePod
函数,开始升级 Pod。要想使用原地升级机制,必须在 CloneSet 的 Spec 中指定 UpdateStrategy 的 Type 为 InPlaceIfPossible
或者 InPlaceOnly
。
1 | if cs.Spec.UpdateStrategy.Type == appsv1alpha1.InPlaceIfPossibleCloneSetUpdateStrategyType || |
2.2.2 CanUpdateInPlace 检查是否满足原地升级的条件
当 Kruise workload 的升级类型名为 InplaceOnly
的时候,表示强制使用原地升级,如果不满足原地升级条件,就会报错; 如果是 InPlaceIfPossible
,它意味着 Kruise 会尽量对 Pod 采取原地升级,如果不能则退化到重建升级。
只有满足以下的改动条件会被允许执行原地升级:
- 更新 workload 中的
spec.template.metadata.*
,比如 labels/annotations,Kruise 只会将 metadata 中的改动更新到存量 Pod 上。 - 更新 workload 中的
spec.template.spec.containers[x].image
,Kruise 会原地升级 Pod 中这些容器的镜像,而不会重建整个 Pod。 - 从 Kruise v1.0 版本开始(包括 v1.0 alpha/beta),更新
spec.template.metadata.labels/annotations
并且 container 中有配置 env from 这些改动的 labels/anntations,Kruise 会原地升级这些容器来生效新的 env 值。
否则,其他字段的改动,比如 spec.template.spec.containers[x].env 或 spec.template.spec.containers[x].resources,都是会回退为重建升级。
完成这项检查的代码如下:
1 | func defaultCalculateInPlaceUpdateSpec(oldRevision, newRevision *apps.ControllerRevision, opts *UpdateOptions) *UpdateSpec { |
defaultCalculateInPlaceUpdateSpec
会计算出新旧两个版本的差异,如果 diff 中只包含第2步骤中的改动,就执行原地升级。
2.2.3 更新 Pod Readiness-gate
符合原地升级条件的 pod 都会在 condition 中增加 InPlaceUpdateReady
的 ConditionType,开始原地升级的时候,将该值置为 false,如果 Pod 上层有 Service 的话,就会自动将准备升级的 pod 从 Endpoint 上摘下,避免升级过程中有流量损失。
1 | if containsReadinessGate(pod) { |
2.3 Begin Inplace update
2.3.1 Pod annotation 中记录升级信息
1 | inPlaceUpdateState := appspub.InPlaceUpdateState{ |
2.3.2 根据配置设置 GracefulPeriod
原地升级中提供了 graceful period 选项,作为优雅原地升级的策略。用户如果配置了 gracePeriodSeconds
这个字段,控制器在原地升级的过程中会先把 Pod status 改为 not-ready,然后等一段时间(gracePeriodSeconds),最后再去修改 Pod spec 中的镜像版本。 这样,就为 endpoints-controller 这些控制器留出了充足的时间来将 Pod 从 endpoints 端点列表中去除。
1 | if spec.GraceSeconds <= 0 { |
2.3.3 Update Pod
在这里就调用 UpdatePod
方法开始真正对 Pod 做升级了。
1 | newPod, updateErr := c.podAdapter.UpdatePod(clone) |
以上原地升级相关的逻辑都是由 kruise_manager
组件负责的,但是当执行 UpdatePod
后,Kruise_manager
就只负责正常的 workload 状态更新,Container 的更新由原地升级的核心组件 Kruise-demaon
接管。
2.4 kruise_daemon
当 Kubelet 收到一个 Pod 创建之后,通过 CRI(Container Runtime Interface) , CNI 以及类似的公共接口(例如 CSI)来调用底层真正的接口实现者去完成操作。对于容器运行时来说,是通过 CRI 接口调用底层真正的 Runtime 运行时来完成对容器的创建和启动镜像拉取这些操作。其中 CRI 是 Kubernetes1.5 之后加入的一个新功能,由协议缓冲区和 gRPC API 组成,提供了一个明确定义的抽象层,它的目的是对于 Kubelet 能屏蔽底下 Runtime 实现的细节而只显示所需的接口。
Kruise_daemon
就是一个全新的组件,作为 DaemonSet 部署到每个节点上,可以连接到节点上的 CRI API,来拓展 Kubernetes 容器进行时的操作, 它也可以调用 CRI 这一层来实现 Container Runtime 层面的能力,比如它可以拉镜像,可以重启容器。
2.4.1 Kruise_daemon 入口
kruise_daemon
的入口函数在 kruise/cmd/daemon/main.go
中,
1 | d, err := daemon.NewDaemon(cfg, *bindAddr) |
进入到 NewDaemon
函数中看一眼,发现这个 daemon 服务本质上也是几个 controller,用来监听相应的 Pod 变化并执行操作。
1 | // kruise/pkg/daemon/daemon.go |
2.4.2 计算 PlainHash 或者 ExtractedEnvFromMetadataHash
2.4.2.1 如果只是 image update 的话,只需要更新 image 字段,kubelet 来执行 preStop 和 container restart。这是因为 Kubelet 在创建每个容器时,会为容器计算一个 hash
值,当上层修改了容器的 image 之后,Kubelet 就认为容器的 hash
值发生了变化。当 Kubelet 发现 Pod spec 中容器的 hash 值和实际的,如 container 对应的 hash
值不一致时,就会把旧的容器停掉,用新的镜像再重建新的容器,从而实现容器的原地升级的能力。
2.4.2.2 从 Kruise v1.0
版本开始(包括 v1.0 alpha/beta),更新 spec.template.metadata.labels/annotations
并且 container 中有配置 env from 这些改动的 labels/anntations,Kruise 会原地升级这些容器来生效新的 env 值。也就是修改环境变量 kruise 也支持原地重启,这部分工作就是由 kruise daemon 来完成的,核心的代码在 kruise/pkg/daemon/containermeta/container_meta_controller.go
中。
- 开启 InPlaceUpdateEnvFromMetadata feature gate
1 | if utilfeature.DefaultFeatureGate.Enabled(features.InPlaceUpdateEnvFromMetadata) |
计算 ExtractedEnvFromMetadataHash
1
containerMeta.Hashes.ExtractedEnvFromMetadataHash, err = envHasher.GetCurrentHash(containerSpec, envGetter)
将 container ID 传到 restarter 的处理队列中
1
c.restarter.queue.AddRateLimited(status.ID)
restarter controller 专门用来处理需要原地重启的 container 队列,核心逻辑在 sync
函数中,上一步中加到队列的 containerID
就会在这里被处理。
1 | func (c *restartController) sync(containerID kubeletcontainer.ContainerID) error |
- 执行 killCOntainer
1 | func (m *genericRuntimeManager) KillContainer(pod *v1.Pod, containerID kubeletcontainer.ContainerID, containerName string, message string, gracePeriodOverride *int64) error |
KillContainer
中有两个重要的操作,首先调用对旧的 container 执行 preStop
(如果有的话),然后调用容器运行时接口 StopContainer
将容器停止。
1 | // Run the pre-stop lifecycle hooks if applicable and if there is enough time to run it |
2.4.3 kubelet image pull & create container
在上一步中容器被停止后,kubelet 会开始新建容器,然后更新容器的状态。
2.4.4 kruise manager 同步 pod 状态
与此同时,相关的 controller 也在同步 Pod/workloads 升级的状态。
1 | if err = r.statusUpdater.UpdateCloneSetStatus(instance, &newStatus, filteredPods); err != nil { |
2.4.5 kubelet 标记 Pod Ready
完成更新后,由 kubelet 标记 Pod Ready,kruise manager 将相应的 worklod 同步为更新完成。
至此,整个原地升级的流程就完成了。不难发现,原地升级除了用到原生 kubernetes 提供的 informer controller 机制外,最核心的也是最有亮点的地方就是巧妙地实现了一个类似 kubelet 的 plugin - kruise daemon
, 为我们提供了一种全新的 Kubernetes 容器运行时 operations 的拓展思路。
3. 源码流程图
以上的代码可以总结简化为下面这张图:
4. 总结
原地升级是 OpenKruise 的核心能力,是其他组件和功能实现的基石。了解其底层实现原理和源码能够扩宽自己的技术视野,加深对 kubernetes 的理解,给我们提供了一种新的拓展 kubelet 的新思路。同时,当我们在使用 Openkruise 遇到问题的时候,了解源码也有助于问题的排查。