OpenKruise v1.2:新增 PersistentPodState 实现有状态 Pod 拓扑固定与 IP 复用

语言: CN / TW / HK

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

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

版本解析

Cloud Native

在 v1.2 版本中,OpenKruise 提供了一个名为 PersistentPodState 的新 CRD 和控制器,在 CloneSet status 和 lifecycle hook 中新增字段, 并对 PodUnavailableBudget 做了多重优化。

1. 新增 CRD 和 Con troller- PersistentPodState

随着云原生的发展,越来越多的公司开始将有状态服务(如:Etcd、MQ)进行 Kubernetes 部署。K8s StatefulSet 是管理有状态服务的工作负载,它在很多方面考虑了有状态服务的部署特征。然而,StatefulSet 只能保持有限的 Pod 状态,如:Pod Name 有序且不变,PVC 持久化,并不能满足其它 Pod 状态的保持需求,例如:固定 IP 调度,优先调度到之前部署的 Node 等。典型案例有:

  • 服务发现中间件服务对部署之后的 Pod IP 异常敏感,要求 IP 不能随意改变

  • 数据库服务将数据持久化到宿主机磁盘,所属 Node 改变将导致数据丢失

针对上述描述,Kruise 通过自定义 PersistentPodState CRD,能够保持 Pod 其它相关状态,例如:“固定 IP 调度”。

一个 PersistentPodState 资源对象 YAML 如下:

apiVersion: apps.kruise.io/v1alpha1
kind: PersistentPodState
metadata:
name: echoserver
namespace: echoserver
spec:
targetRef:
# 原生k8s 或 kruise StatefulSet
# 只支持 StatefulSet 类型
apiVersion: apps.kruise.io/v1beta1
kind: StatefulSet
name: echoserver
# required node affinity,如下:Pod重建后将强制部署到同Zone
requiredPersistentTopology:
nodeTopologyKeys:
failure-domain.beta.kubernetes.io/zone[,other node labels]
# preferred node affinity,如下:Pod重建后将尽量部署到同Node
preferredPersistentTopology:
- preference:
nodeTopologyKeys:
kubernetes.io/hostname[,other node labels]
# int, [1 - 100]
weight: 100

“固定 IP 调度”应该是比较常见的有状态服务的 K8s 部署要求,它的含义不是“指定 Pod IP 部署”,而是要求 Pod 在第一次部署之后,业务发布或机器驱逐等常规性运维操作都不会导致 Pod IP 发生变化。达到上述效果,首先就需要 K8s 网络组件能够支持 Pod IP 保留以及尽量保持 IP 不变的能力,本文将 flannel 网络组件中的 Host-local 插件做了一些代码改造, 使之能够达到同 Node 下保持 Pod IP 不变的效果,相关原理就不在此陈述,代码请参考: host-local [ 2]

“固定 IP 调度”好像有网络组件支持就好了,这跟 PersistentPodState 有什么关系呢?因为,网络组件实现"Pod IP 保持不变"都有一定的限制, 例如:flannel 只能支持同 Node 保持 Pod IP 不变。但是,K8s 调度的最大特性就是“不确定性”,所以“如何保证 Pod 重建后调度到同 Node 上”就是 PersistentPodState 解决的问题。

另外,你可以通过在 StatefulSet 或 Advanced StatefulSet 上面新增如下的 annotations,来让 Kruise 自动为你的 StatefulSet 创建 PersistentPodState 对象,从而避免了手动创建所有 PersistentPodState 的负担。

apiVersion: apps.kruise.io/v1alpha1
kind: StatefulSet
metadata:
annotations:
# 自动生成PersistentPodState对象
kruise.io/auto-generate-persistent-pod-state: "true"
# preferred node affinity,如下:Pod重建后将尽量部署到同Node
kruise.io/preferred-persistent-topology: kubernetes.io/hostname[,other node labels]
# required node affinity,如下:Pod重建后将强制部署到同Zone
kruise.io/required-persistent-topology: failure-domain.beta.kubernetes.io/zone[,other node labels]

2. CloneSet 针对百分比形式 partition 计算逻辑变化,新增 status 字段

过去,CloneSet 通过 “向上取整” 的方式来计算它的 partition 数值(当它是百分比形式的数值时),这意味着即使你将 partition 设置为一个小于 100% 的百分比,CloneSet 也有可能不会升级任何一个 Pod 到新版本。比如,对于一个 replicas=8 和 partition=90% 的 CloneSet 对象,它所计算出的实际 partition 数值是 8(来自 8 * 90% 向上取整), 因此它暂时不会执行升级动作。这有时候会为用户来带困惑,尤其是对于使用了一些 rollout 滚动升级组件的场景,比如 Kruise Rollout 或 Argo。

因此,从 v1.2 版本开始,CloneSet 会保证在 partition 是小于 100% 的百分比数值时,至少有 1 个 Pod 会被升级,除非 CloneSet 处于 replicas <= 1 的情况。

不过,这样会导致用户难以理解其中的计算逻辑,同时又需要在 partition 升级的时候知道期望升级的 Pod 数量,来判断该批次升级是否完成。

所以我们另外又在 CloneSet status 中新增了 expectedUpdatedReplicas 字段,它可以很直接地展示基于当前的 partition 数值,期望有多少 Pod 会被升级。对于用户来说:

只需要比对 status.updatedReplicas >= status.expectedUpdatedReplicas 以及另外的 updatedReadyReplicas 来判断当前发布阶段是否达到完成状态。

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:
replicas: 8
updateStrategy:
partition: 90%
status:
replicas: 8
expectedUpdatedReplicas: 1
updatedReplicas: 1
updatedReadyReplicas: 1

3. 在 lifecycle hook 阶段设置 Pod not-ready

Kruise 在早先的版本中提供了 lifecycle hook 功能,其中 CloneSet 和 Advanced StatefulSet 都支持了 PreDelete、InPlaceUpdate 两种 hook, Advanced DaemonSet 目前只支持 PreDelete hook。

过去,这些 hook 只会将当前的操作卡住,并允许用户在 Pod 删除之前或者原地升级的前后来做一些自定义的事情(比如将 Pod 从服务端点中摘除)。但是,Pod 在这些阶段中很可能还处于 Ready 状态,此时将它从一些自定义的 service 实现中摘除, 其实一定程度上有点违背 Kubernetes 的常理,一般来说它只会将处于 NotReady 状态的 Pod 从服务端点中摘除。

因此,这个版本我们在 lifecycle hook 中新增了 markPodNotReady 字段,它控制了 Pod 在处于 hook 阶段的时候是否会被强制设为 NotReady 状态。

type LifecycleStateType string
// Lifecycle contains the hooks for Pod lifecycle.
type Lifecycle struct
// PreDelete is the hook before Pod to be deleted.
PreDelete *LifecycleHook `json:"preDelete,omitempty"`
// InPlaceUpdate is the hook before Pod to update and after Pod has been updated.
InPlaceUpdate *LifecycleHook `json:"inPlaceUpdate,omitempty"`
}
type LifecycleHook struct {
LabelsHandler map[string]string `json:"labelsHandler,omitempty"`
FinalizersHandler []string `json:"finalizersHandler,omitempty"`


/********************** FEATURE STATE: 1.2.0 ************************/
// MarkPodNotReady = true means:
// - Pod will be set to 'NotReady' at preparingDelete/preparingUpdate state.
// - Pod will be restored to 'Ready' at Updated state if it was set to 'NotReady' at preparingUpdate state.
// Default to false.
MarkPodNotReady bool `json:"markPodNotReady,omitempty"`
/*********************************************************************/
}

对于配置了 markPodNotReady: true 的 PreDelete hook,它会在 PreparingDelete 阶段的时候将 Pod 设置为 NotReady, 并且这种 Pod 在我们重新调大 replicas 数值的时候无法重新回到 normal 状态。

对于配置了 markPodNotReady: true 的 InPlaceUpdate hook,它会在 PreparingUpdate 阶段将 Pod 设置为 NotReady, 并在 Updated 阶段将强制 NotReady 的状态去掉。

4. PodUnavailableBudget 支持自定义 workload 与性能优化

Kubernetes 自身提供了 PodDisruptionBudget 来帮助用户保护高可用的应用,但它只能防护 eviction 驱逐一种场景。对于多种多样的不可用操作,PodUnavailableBudget 能够更加全面地防护应用的高可用和 SLA,它不仅能够防护 Pod 驱逐,还支持其他如删除、原地升级等会导致 Pod 不可用的操作。

过去,PodUnavailableBudget 仅仅支持一些特定的 workload,比如 CloneSet、Deployment 等,但它不能够识别用户自己定义的一些未知工作负载。

从 v1.2 版本开始,PodUnavailableBudget 支持了保护任意自定义工作负载的 Pod,只要这些工作负载声明了 scale subresource 子资源。

在 CRD 中,scale 子资源的声明方式如下:

    subresources:
scale:
labelSelectorPath: .status.labelSelector
specReplicasPath: .spec.replicas
statusReplicasPath: .status.replicas

不过,如果你的项目是通过 kubebuilder 或 operator-sdk 生成的,那么只需要在你的 workload 定义结构上加一行注解并重新 make manifests 即可:

// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.labelSelector

另外,PodUnavailableBudget 还通过关闭 client list 时候的默认 DeepCopy 操作,来提升了在大规模集群中的运行时性能。

5. 其他改动

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

社区参与

Cloud Native

非常欢迎你通过 Github/Slack/钉钉/微信 等方式加入我们来参与 OpenKruise 开源社区。你是否已经有一些希望与我们社区交流的内容呢?可以在我们的社区双周会

https://shimo.im/docs/gXqmeQOYBehZ4vqo)上分享你的声音,或通过以下渠道参与讨论:

  • 加入社区 Slack channel (English)

    https://kubernetes.slack.com/?redir=%2Farchives%2Fopenkruise

  • 加入社区钉钉群:搜索群号 23330762 (Chinese)

  • 加入社区微信群(新):添加用户 openkruise 并让机器人拉你入群 (Chinese)

参考链接:

[1] OpenKruise:

https: //openkruise.io/

[2] host-local:

https: //github.com/openkruise/samples

[3] Github release : 

https://github.com/openkruise/kruise/releases

戳原文,查看 OpenKruise 项目 github 主页!!