Skip to main content

OpenKruise v1.1:功能增强与上游对齐,大规模场景性能优化

· 阅读需要 1 分钟
Siyu Wang

云原生应用自动化管理套件、CNCF Sandbox 项目 -- OpenKruise,近期发布了 v1.1 版本。

OpenKruise 是针对 Kubernetes 的增强能力套件,聚焦于云原生应用的部署、升级、运维、稳定性防护等领域。 所有的功能都通过 CRD 等标准方式扩展,可以适用于 1.16 以上版本的任意 Kubernetes 集群。单条 helm 命令即可完成 Kruise 的一键部署,无需更多配置。

版本解析

在 v1.1 版本中,OpenKruise 对不少已有功能做了扩展与增强,并且优化了在大规模集群中的运行性能。以下对 v1.1 的部分功能做简要介绍。

值得注意的是,OpenKruise v1.1 已经将 Kubernetes 代码依赖版本升级到 v1.22,这意味着用户可以在 CloneSet 等工作负载的 pod template 模板中使用 up to v1.22 的新字段等, 但用户安装使用 OpenKruise 所兼容的 Kubernetes 集群版本仍然保持在 >= v1.16。

1. 原地升级支持容器顺序优先级

去年底发布的 v1.0 版本,OpenKruise 引入了容器启动顺序控制功能, 它支持为一个 Pod 中的多个容器定义不同的权重关系,并在 Pod 创建时按照权重来控制不同容器的启动顺序。

在 v1.0 中,这个功能仅仅能够作用于每个 Pod 的创建阶段。当创建完成后,如果对 Pod 中多个容器做原地升级,则这些容器都会被同时执行升级操作。

最近一段时间,社区与 LinkedIn 等公司做过一些交流,获得了更多用户使用场景的输入。 在一些场景下,Pod 中多个容器存在关联关系,例如业务容器升级的同时,Pod 中其他一些容器也需要升级配置从而关联到这个新版本; 或是多个容器避免并行升级,从而保证如日志采集类的 sidecar 容器不会丢失业务容器中的日志等。

因此,在 v1.1 版本中 OpenKruise 支持了按容器优先级顺序的原地升级。 在实际使用过程中,用户无需配置任何额外参数,只要 Pod 在创建时已经带有了容器启动优先级,则不仅在 Pod 创建阶段,会保证高优先级容器先于低优先级容器启动; 并且在单次原地升级中,如果同时升级了多个容器,会先升级高优先级容器,等待它升级启动完成后,再升级低优先级容器。

这里的原地升级,包括修改 image 镜像升级与修改 env from metadata 的环境变量升级,详见原地升级介绍

总结来说

  • 对于不存在容器启动顺序的 Pod,在多容器原地升级时没有顺序保证。
  • 对于存在容器启动顺序的 Pod:
    • 如果本次原地升级的多个容器具有不同的启动顺序,会按启动顺序来控制原地升级的先后顺序。
    • 如果本地原地升级的多个容器的启动顺序相同,则原地升级时没有顺序保证。

例如,一个包含两个不同启动顺序容器的 CloneSet 如下:

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
metadata:
...
spec:
replicas: 1
template:
metadata:
annotations:
app-config: "... config v1 ..."
spec:
containers:
- name: sidecar
env:
- name: KRUISE_CONTAINER_PRIORITY
value: "10"
- name: APP_CONFIG
valueFrom:
fieldRef:
fieldPath: metadata.annotations['app-config']
- name: main
image: main-image:v1
updateStrategy:
type: InPlaceIfPossible

当我们更新 CloneSet,将其中 app-config annotation 和 main 容器的镜像修改后, 意味着 sidecar 与 main 容器都需要被更新,Kruise 会先原地升级 Pod 来将其中 sidecar 容器重建来生效新的 env from annotation。

接下来,我们可以在已升级的 Pod 中看到 apps.kruise.io/inplace-update-state annotation 和它的值:

{
"revision": "{CLONESET_NAME}-{HASH}", // 本次原地升级的目标 revision 名字
"updateTimestamp": "2022-03-22T09:06:55Z", // 整个原地升级的初次开始时间
"nextContainerImages": {"main": "main-image:v2"}, // 后续批次中还需要升级的容器镜像
// "nextContainerRefMetadata": {...}, // 后续批次中还需要升级的容器 env from labels/annotations
"preCheckBeforeNext": {"containersRequiredReady": ["sidecar"]}, // pre-check 检查项,符合要求后才能原地升级后续批次的容器
"containerBatchesRecord":[
{"timestamp":"2022-03-22T09:06:55Z","containers":["sidecar"]} // 已更新的首个批次容器(它仅仅表明容器的 spec 已经被更新,例如 pod.spec.containers 中的 image 或是 labels/annotations,但并不代表 node 上真实的容器已经升级完成了)
]
}

当 sidecar 容器升级成功之后,Kruise 会接着再升级 main 容器。最终你会在 Pod 中看到如下的 apps.kruise.io/inplace-update-state annotation:

{
"revision": "{CLONESET_NAME}-{HASH}",
"updateTimestamp": "2022-03-22T09:06:55Z",
"lastContainerStatuses":{"main":{"imageID":"THE IMAGE ID OF OLD MAIN CONTAINER"}},
"containerBatchesRecord":[
{"timestamp":"2022-03-22T09:06:55Z","containers":["sidecar"]},
{"timestamp":"2022-03-22T09:07:20Z","containers":["main"]}
]
}

通常来说,用户只需要关注其中 containerBatchesRecord 来确保容器是被分为多批升级的。 如果这个 Pod 在原地升级的过程中卡住了,你可以检查 nextContainerImages/nextContainerRefMetadata 字段,以及 preCheckBeforeNext 中前一次升级的容器是否已经升级成功并 ready 了。

2. StatefulSetAutoDeletePVC 功能

从 Kubernetes v1.23 开始,原生的 StatefulSet 加入了 StatefulSetAutoDeletePVC 功能,即根据给定策略来选择保留或自动删除 StatefulSet 创建的 PVC 对象参考文档

因此,v1.1 版本的 Advanced StatefulSet 从上游同步了这个功能,允许用户通过 .spec.persistentVolumeClaimRetentionPolicy 字段来指定这个自动清理策略。 这需要你在安装或升级 Kruise 的时候,启用 StatefulSetAutoDeletePVC feature-gate 功能。

apiVersion: apps.kruise.io/v1beta1
kind: StatefulSet
spec:
...
persistentVolumeClaimRetentionPolicy: # optional
whenDeleted: Retain | Delete
whenScaled: Retain | Delete

其中,两个策略字段包括:

  • whenDeleted:当 Advanced StatefulSet 被删除时,对 PVC 的保留/删除策略。
  • whenScaled:当 Advanced StatefulSet 发生缩容时,对缩容 Pod 关联 PVC 的保留/删除策略。

每个策略都可以配置以下两种值:

  • Retain(默认值):它的行为与过去 StatefulSet 一样,在 Pod 删除时对它关联的 PVC 做保留。
  • Delete:当 Pod 删除时,自动删除它所关联的 PVC 对象。

除此之外,还有几个注意点:

  1. StatefulSetAutoDeletePVC 功能只会清理由 volumeClaimTemplate 中定义和创建的 PVC,而不会清理用户自己创建或关联到 StatefulSet Pod 中的 PVC。
  2. 上述清理只发生在 Advanced StatefulSet 被删除或主动缩容的情况下。例如 node 故障导致的 Pod 驱逐重建等,仍然会复用已有的 PVC。

3. Advanced DaemonSet 重构并支持生命周期钩子

早先版本的 Advanced DaemonSet 实现与上游控制器差异较大,例如对于 not-ready 和 unschedulable 的节点需要额外配置字段来选择是否处理,这对于我们的用户来说都增加了使用成本和负担。

在 v1.1 版本中,我们对 Advanced DaemonSet 做了一次小重构,将它与上游控制器重新做了对齐。 因此,Advanced DaemonSet 的所有默认行为会与原生 DaemonSet 基本一致,用户可以像使用 Advanced StatefulSet 一样,通过修改 apiVersion 就能很方便地将一个原生 DaemonSet 修改为 Advanced DaemonSet 来使用。

另外,我们还为 Advanced DaemonSet 增加了生命周期钩子,首先支持 preDelete hook,来允许用户在 daemon Pod 被删除前执行一些自定义的逻辑。

apiVersion: apps.kruise.io/v1alpha1
kind: DaemonSet
spec:
...
# define with label
lifecycle:
preDelete:
labelsHandler:
example.io/block-deleting: "true"

当 DaemonSet 删除一个 Pod 时(包括缩容和重建升级):

  • 如果没有定义 lifecycle hook 或者 Pod 不符合 preDelete 条件,则直接删除。
  • 否则,会先将 Pod 更新为 PreparingDelete 状态,并等待用户自定义的 controller 将 Pod 中关联的 label/finalizer 去除,再执行 Pod 删除。

4. Disable DeepCopy 性能优化

默认情况下,我们在使用 controller-runtime 来编写 Operator/Controller 时, 使用其中 sigs.k8s.io/controller-runtime/pkg/client Client 客户端来 get/list 查询对象(typed),都是从内存 Informer 中获取并返回,这是大部分人都知道的。

但很多人不知道的是,在这些 get/list 操作背后,controller-runtime 会将从 Informer 中查到的所有对象做一次 deep copy 深拷贝后再返回。

这个设计的初衷,是避免开发者错误地将 Informer 中的对象直接篡改。在深拷贝之后,无论开发者对 get/list 返回的对象做了任何修改,都不会影响到 Informer 中的对象,后者只会从 kube-apiserver 的 ListWatch 请求中同步。

但是在一些很大规模的集群中,OpenKruise 中各个控制器同时在运行,同时每个控制器还存在多个 worker 执行 Reconcile,可能会带来大量的 deep copy 操作。 例如集群中有大量应用的 CloneSet,而其中一些 CloneSet 下管理的 Pod 数量非常多,则每个 worker 在 Reconcile 的时候都会 list 查询一个 CloneSet 下的所有 Pod 对象,再加上多个 worker 并行操作, 可能造成 kruise-manager 瞬时的 CPU 和 Memory 压力陡增,甚至在内存配额不足的情况下有发生 OOM 的风险。

在上游的 controller-runtime 中,我在去年已经提交合并了 DisableDeepCopy 功能,包含在 controller-runtime v0.10 及以上的版本。 它允许开发者指定某些特定的资源类型,在做 get/list 查询时不执行深拷贝,而是直接返回 Informer 中的对象指针。

例如下述代码,在 main.go 中初始化 Manager 时,为 cache 加入参数即可配置 Pod 等资源类型不做深拷贝。

    mgr, err := ctrl.NewManager(cfg, ctrl.Options{
...
NewCache: cache.BuilderWithOptions(cache.Options{
UnsafeDisableDeepCopyByObject: map[client.Object]bool{
&v1.Pod{}: true,
},
}),
})

但在 Kruise v1.1 版本中,我们没有选择直接使用这个功能,而是将 Delegating Client 重新做了封装, 从而使得开发者可以在任意做 list 查询的地方通过 DisableDeepCopy ListOption 来指定单次的 list 操作不做深拷贝。

    if err := r.List(context.TODO(), &podList, client.InNamespace("default"), utilclient.DisableDeepCopy); err != nil {
return nil, nil, err
}

这样做的好处是使用上更加灵活,避免为整个资源类型关闭深拷贝后,众多社区贡献者在参与开发的过程中如果没有注意到则可能会错误修改 Informer 中的对象。

5. 其他改动

你可以通过 Github release 页面,来查看更多的改动以及它们的作者与提交记录。

社区参与

非常欢迎你通过 Github/Slack/钉钉/微信 等方式加入我们来参与 OpenKruise 开源社区。 你是否已经有一些希望与我们社区交流的内容呢? 可以在我们的社区双周会上分享你的声音,或通过以下渠道参与讨论:

  • 加入社区 Slack channel (English)
  • 加入社区钉钉群:搜索群号 23330762 (Chinese)
  • 加入社区微信群(新):添加用户 openkruise 并让机器人拉你入群 (Chinese)