13518219792

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

K8s增强版工作负载OpenKruise之CloneSet

K8s 增强版工作负载 OpenKruise 之 CloneSet

作者:k8s技术圈 2023-04-04 07:25:46

云计算

云原生 Kubernetes 集群中。Kubernetes 自身提供的一些应用部署管理功能,对于大规模应用与集群的场景这些功能是远远不够的,OpenKruise 弥补了 Kubernetes 在应用部署、升级、防护、运维等领域的不足。

OpenKruise(https://openkruise.io) 是一个基于 Kubernetes 的扩展套件,主要聚焦于云原生应用的自动化,比如部署、发布、运维以及可用性防护。OpenKruise 提供的绝大部分能力都是基于 CRD 扩展来定义的,它们不存在于任何外部依赖,可以运行在任意纯净的 Kubernetes 集群中。Kubernetes 自身提供的一些应用部署管理功能,对于大规模应用与集群的场景这些功能是远远不够的,OpenKruise 弥补了 Kubernetes 在应用部署、升级、防护、运维等领域的不足。

OpenKruise 提供了以下的一些核心能力:

架构

下图是 OpenKruise 的整体架构:

架构

所有 OpenKruise 的功能都是通过 Kubernetes CRD 来提供的。其中 Kruise-manager 是一个运行控制器和 webhook 的中心组件,它通过 Deployment 部署在 kruise-system 命名空间中。 从逻辑上来看,如 cloneset-controller、sidecarset-controller 这些的控制器都是独立运行的,不过为了减少复杂度,它们都被打包在一个独立的二进制文件、并运行在 kruise-controller-manager-xxx 这个 Pod 中。除了控制器之外,kruise-controller-manager-xxx 中还包含了针对 Kruise CRD 以及 Pod 资源的 admission webhook。Kruise-manager 会创建一些 webhook configurations 来配置哪些资源需要感知处理、以及提供一个 Service 来给 kube-apiserver 调用。

从 v0.8.0 版本开始提供了一个新的 Kruise-daemon 组件,它通过 DaemonSet 部署到每个节点上,提供镜像预热、容器重启等功能。

安装

这里我们同样还是使用 Helm 方式来进行安装,需要注意从 v1.0.0 开始,OpenKruise 要求在 Kubernetes >= 1.16 以上版本的集群中安装和使用。

首先添加 charts 仓库:

 helm repo add openkruise https://openkruise.github.io/charts/
helm repo update

然后执行下面的命令安装最新版本的应用:

 helm upgrade --install kruise openkruise/kruise --version 1.3.0

该 charts 在模板中默认定义了命名空间为 kruise-system,所以在安装的时候可以不用指定,如果你的环境访问 DockerHub 官方镜像较慢,则可以使用下面的命令将镜像替换成阿里云的镜像:

 helm upgrade --install kruise openkruise/kruise --set manager.image.repository=openkruise-registry.cn-shanghai.cr.aliyuncs.com/openkruise/kruise-manager --version 1.3.0

应用部署完成后会在 kruise-system 命名空间下面运行 2 个 kruise-manager 的 Pod,同样它们之间采用 leader-election 的方式选主,同一时间只有一个提供服务,达到高可用的目的,此外还会以 DaemonSet 的形式启动 kruise-daemon 组件:

 kubectl get pods -n kruise-system
NAME READY STATUS RESTARTS AGE
kruise-controller-manager-7d78fc5c97-d6mbb 1/1 Running 0 52s
kruise-controller-manager-7d78fc5c97-wccbn 1/1 Running 0 52s
kruise-daemon-9f94k 1/1 Running 0 52s
kruise-daemon-bqj69 1/1 Running 0 52s
kruise-daemon-h95pf 1/1 Running 0 52s

如果不想使用默认的参数进行安装,也可以自定义配置,可配置的 values 值可以参考 charts 文档 https://github.com/openkruise/charts 进行定制。

CloneSet

CloneSet 控制器是 OpenKruise 提供的对原生 Deployment 的增强控制器,在使用方式上和 Deployment 几乎一致,如下所示是我们声明的一个 CloneSet 资源对象:

# cloneset-demo.yaml
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
metadata:
name: cs-demo
spec:
replicas: 3
selector:
matchLabels:
app: cs
template:
metadata:
labels:
app: cs
spec:
containers:
- name: nginx
image: nginx:alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80

直接创建上面的这个 CloneSet 对象:

 kubectl apply -f cloneset-demo.yaml
kubectl get cloneset cs-demo
NAME DESIRED UPDATED UPDATED_READY READY TOTAL AGE
cs-demo 3 3 0 0 3 8s
kubectl describe cloneset cs-demo
Name: cs-demo
Namespace: default
Labels:
Annotations:
API Version: apps.kruise.io/v1alpha1
Kind: CloneSet
# ......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 21s cloneset-controller succeed to create pod cs-demo-jfx5s
Normal SuccessfulCreate 21s cloneset-controller succeed to create pod cs-demo-kg9p2
Normal SuccessfulCreate 21s cloneset-controller succeed to create pod cs-demo-n72fr

该对象创建完成后我们可以通过 kubectl describe 命令查看对应的 Events 信息,可以发现 cloneset-controller 是直接创建的 Pod,这个和原生的 Deployment 就有一些区别了,Deployment 是通过 ReplicaSet 去创建的 Pod,所以从这里也可以看出来 CloneSet 是直接管理 Pod 的,3 个副本的 Pod 此时也创建成功了:

 kubectl get pods -l app=cs
NAME READY STATUS RESTARTS AGE
cs-demo-jfx5s 1/1 Running 0 58s
cs-demo-kg9p2 1/1 Running 0 58s
cs-demo-n72fr 1/1 Running 0 58s

CloneSet 虽然在使用上和 Deployment 比较类似,但还是有非常多比 Deployment 更高级的功能,下面我们来详细介绍下。

扩缩容

流式扩容

CloneSet 在扩容的时候可以通过 ScaleStrategy.MaxUnavailable 来限制扩容的步长,这样可以对服务应用的影响最小,可以设置一个绝对值或百分比,如果不设置该值,则表示不限制。

比如我们在上面的资源清单中添加如下所示数据:

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
metadata:
name: cs-demo
spec:
minReadySeconds: 60
scaleStrategy:
maxUnavailable: 1
replicas: 5
......

上面我们配置 scaleStrategy.maxUnavailable 为 1,结合 minReadySeconds 参数,表示在扩容时,只有当上一个扩容出的 Pod 已经 Ready 超过一分钟后,CloneSet 才会执行创建下一个 Pod,比如这里我们扩容成 5 个副本,更新上面对象后查看 CloneSet 的事件:

 kubectl describe cloneset cs-demo
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 4m25s cloneset-controller succeed to create pod cs-demo-jfx5s
Normal SuccessfulCreate 4m25s cloneset-controller succeed to create pod cs-demo-kg9p2
Normal SuccessfulCreate 4m25s cloneset-controller succeed to create pod cs-demo-n72fr
Warning ScaleUpLimited 66s cloneset-controller scaleUp is limited because of scaleStrategy.maxUnavailable, limit: 1
Normal SuccessfulCreate 66s cloneset-controller succeed to create pod cs-demo-x8ndf
Warning ScaleUpLimited 64s (x6 over 66s) cloneset-controller scaleUp is limited because of scaleStrategy.maxUnavailable, limit: 0
Normal SuccessfulCreate 5s cloneset-controller succeed to create pod cs-demo-2sfzz

可以看到第一时间扩容了一个 Pod,由于我们配置了 minReadySeconds: 60,也就是新扩容的 Pod 创建成功超过 1 分钟后才会扩容另外一个 Pod,上面的 Events 信息也能表现出来,查看 Pod 的 AGE 也能看出来扩容的 2 个 Pod 之间间隔了 1 分钟左右:

 kubectl get pods -l app=cs
NAME READY STATUS RESTARTS AGE
cs-demo-2sfzz 1/1 Running 0 22s
cs-demo-jfx5s 1/1 Running 0 4m42s
cs-demo-kg9p2 1/1 Running 0 4m42s
cs-demo-n72fr 1/1 Running 0 4m42s
cs-demo-x8ndf 1/1 Running 0 83s

当 CloneSet 被缩容时,我们还可以指定一些 Pod 来删除,这对于 StatefulSet 或者 Deployment 来说是无法实现的, StatefulSet 是根据序号来删除 Pod,而 Deployment/ReplicaSet 目前只能根据控制器里定义的排序来删除。而 CloneSet 允许用户在缩小 replicas 数量的同时,指定想要删除的 Pod 名字,如下所示:

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
metadata:
name: cs-demo
spec:
minReadySeconds: 60
scaleStrategy:
maxUnavailable: 1
podsToDelete:
- cs-demo-n72fr
replicas: 4
......

更新上面的资源对象后,会将应用缩到 4 个 Pod,如果在 podsToDelete 列表中指定了 Pod 名字,则控制器会优先删除这些 Pod,对于已经被删除的 Pod,控制器会自动从 podsToDelete 列表中清理掉。比如我们更新上面的资源对象后 cs-demo-n72fr 这个 Pod 会被移除,其余会保留下来:

 kubectl get pods -l app=cs
NAME READY STATUS RESTARTS AGE
cs-demo-2sfzz 1/1 Running 0 61s
cs-demo-jfx5s 1/1 Running 0 5m21s
cs-demo-kg9p2 1/1 Running 0 5m21s
cs-demo-x8ndf 1/1 Running 0 2m2s

如果你只把 Pod 名字加到 podsToDelete,但没有修改 replicas 数量,那么控制器会先把指定的 Pod 删掉,然后再扩一个新的 Pod,另一种直接删除 Pod 的方式是在要删除的 Pod 上打 apps.kruise.io/specified-delete: true 标签。

相比于手动直接删除 Pod,使用 podsToDelete 或 apps.kruise.io/specified-delete: true 方式会有 CloneSet 的 maxUnavailable/maxSurge 来保护删除, 并且会触发 PreparingDelete 生命周期的钩子。

PVC 模板

一个比较奇特的特性,CloneSet 允许用户配置 PVC 模板 volumeClaimTemplates,用来给每个 Pod 生成独享的 PVC,这是 Deployment 所不支持的,因为往往有状态的应用才需要单独设置 PVC,在使用 CloneSet 的 PVC 模板的时候需要注意下面的这些事项:

以下是一个带有 PVC 模板的例子:

# cloneset-pvc.yaml
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
metadata:
labels:
app: sample
name: sample-data
spec:
replicas: 3
selector:
matchLabels:
app: sample
template:
metadata:
labels:
app: sample
spec:
containers:
- name: nginx
image: nginx:alpine
volumeMounts:
- name: data-vol
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: data-vol
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi

比如应用上面的资源对象后会自动创建 3 个 Pod 和 3 个 PVC,每个 Pod 都会挂载一个 PVC:

 kubectl get pods -l app=sample
NAME READY STATUS RESTARTS AGE
sample-data-t4vq6 0/1 Pending 0 2m13s
sample-data-vcjnl 0/1 Pending 0 2m13s
sample-data-znwjd 0/1 Pending 0 2m13s
kubectl get pvc -l app=sample
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-vol-sample-data-t4vq6 Pending 2m46s
data-vol-sample-data-vcjnl Pending 2m46s
data-vol-sample-data-znwjd Pending 2m46s

升级

CloneSet 一共提供了 3 种升级方式:

这里有一个重要概念:原地升级,这也是 OpenKruise 提供的核心功能之一,当我们要升级一个 Pod 中镜像的时候,下图展示了重建升级和原地升级的区别:

原地升级

重建升级时我们需要删除旧 Pod、创建新 Pod:

但是对于原地升级,我们仍然复用同一个 Pod 对象,只是修改它里面的字段:

所以显然如果能用原地升级方式来升级我们的工作负载,对在线应用的影响是最小的。上面我们提到 CloneSet 升级类型支持 InPlaceIfPossible,这意味着 Kruise 会尽量对 Pod 采取原地升级,如果不能则退化到重建升级,以下的改动会被允许执行原地升级:

否则,其他字段的改动,比如 spec.template.spec.containers[x].env​ 或 spec.template.spec.containers[x].resources,都是会回退为重建升级。

比如我们将上面的应用升级方式设置为 InPlaceIfPossible​,只需要在资源清单中添加 spec.updateStrategy.type: InPlaceIfPossible 即可:

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
metadata:
name: cs-demo
spec:
updateStrategy:
type: InPlaceIfPossible
......
# image: nginx:1.7.9

更新后可以发现 Pod 的状态并没有发生什么大的变化,名称、IP 都一样,唯一变化的是镜像 tag:

 kubectl get pods -l app=cs
NAME READY STATUS RESTARTS AGE
cs-demo-2sfzz 1/1 Running 1 (18s ago) 36m
cs-demo-jfx5s 1/1 Running 0 40m
cs-demo-kg9p2 1/1 Running 0 40m
cs-demo-x8ndf 1/1 Running 0 37m
kubectl describe cloneset cs-demo
Name: cs-demo
Namespace: default
Labels:
Annotations:
API Version: apps.kruise.io/v1alpha1
Kind: CloneSet
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
# ......
Normal SuccessfulUpdatePodInPlace 6m58s cloneset-controller successfully update pod cs-demo-2sfzz in-place(revision cs-demo-7cb9c88699)
Normal SuccessfulUpdatePodInPlace 5m46s cloneset-controller successfully update pod cs-demo-x8ndf in-place(revision cs-demo-7cb9c88699)
Normal SuccessfulUpdatePodInPlace 4m43s cloneset-controller successfully update pod cs-demo-kg9p2 in-place(revision cs-demo-7cb9c88699)
Normal SuccessfulUpdatePodInPlace 3m40s cloneset-controller successfully update pod cs-demo-jfx5s in-place(revision cs-demo-7cb9c88699)
kubectl describe pod cs-demo-2sfzz
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 44m default-scheduler Successfully assigned default/cs-demo-2sfzz to node2
Normal Pulled 44m kubelet Container image "nginx:alpine" already present on machine
Normal Killing 8m8s kubelet Container nginx definition changed, will be restarted
Normal Pulling 8m8s kubelet Pulling image "nginx:1.7.9"
Normal Created 7m58s (x2 over 44m) kubelet Created container nginx
Normal Started 7m58s (x2 over 44m) kubelet Started container nginx
Normal Pulled 7m58s kubelet Successfully pulled image "nginx:1.7.9" in 9.720841233s (9.720847295s including waiting)

这就是原地升级的效果,原地升级整体工作流程如下图所示:

原地升级流程

如果你在安装或升级 Kruise 的时候启用了 PreDownloadImageForInPlaceUpdate 这个 feature-gate,CloneSet 控制器会自动在所有旧版本 pod 所在节点上预热你正在灰度发布的新版本镜像,这对于应用发布加速很有帮助。

默认情况下 CloneSet 每个新镜像预热时的并发度都是 1,也就是一个个节点拉镜像,如果需要调整,你可以在 CloneSet 通过 apps.kruise.io/image-predownload-parallelism 这个 annotation 来设置并发度。

另外从 Kruise v1.1.0 开始,还可以使用 apps.kruise.io/image-predownload-min-updated-ready-pods 来控制在少量新版本 Pod 已经升级成功之后再执行镜像预热。它的值可能是绝对值数字或是百分比。

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
metadata:
annotations:
apps.kruise.io/image-predownload-parallelism: "5"
apps.kruise.io/image-predownload-min-updated-ready-pods: "2"

注意,为了避免大部分不必要的镜像拉取,目前只针对 replicas > 3 的 CloneSet 做自动预热。

此外 CloneSet 还支持分批进行灰度,在 updateStrategy​ 属性中可以配置 partition 参数,该参数可以用来保留旧版本 Pod 的数量或百分比,默认为 0:

比如,我们将上面示例中的的 image 更新为 nginx:latest​ 并且设置 partitinotallow=2,更新后,过一会查看可以发现只升级了 2 个 Pod:

 kubectl get pods -l app=cs -L controller-revision-hash
NAME READY STATUS RESTARTS AGE CONTROLLER-REVISION-HASH
cs-demo-2sfzz 1/1 Running 1 (11m ago) 47m cs-demo-7cb9c88699
cs-demo-jfx5s 1/1 Running 2 (99s ago) 52m cs-demo-7c4d79f5bc
cs-demo-kg9p2 1/1 Running 2 (27s ago) 52m cs-demo-7c4d79f5bc
cs-demo-x8ndf 1/1 Running 1 (10m ago) 48m cs-demo-7cb9c88699
kubectl get pods -o custom-columns='DATA:metadata.name,CONTAINERS:spec.containers[*].name,IMAGES:spec.containers[*].image' -l app=cs
DATA CONTAINERS IMAGES
cs-demo-2sfzz nginx nginx:1.7.9
cs-demo-jfx5s nginx nginx:latest
cs-demo-kg9p2 nginx nginx:latest
cs-demo-x8ndf nginx nginx:1.7.9

此外 CloneSet 还支持一些更高级的用法,比如可以定义优先级策略来控制 Pod 发布的优先级规则,还可以定义策略来将一类 Pod 打散到整个发布过程中,也可以暂停 Pod 发布等操作。

生命周期钩子

每个 CloneSet 管理的 Pod 会有明确所处的状态,在 Pod label 中的 lifecycle.apps.kruise.io/state 标记:

而生命周期钩子,则是通过在上述状态流转中卡点,来实现原地升级前后、删除前的自定义操作(比如开关流量、告警等)。CloneSet 的 lifecycle 下面主要支持 preDelete 和 inPlaceUpdate 两个属性。

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:

# 通过 finalizer 定义 hook
lifecycle:
preDelete: # PreDelete 是 Pod 被删除之前的 hook
finalizersHandler:
- example.io/unready-blocker
inPlaceUpdate: # InPlaceUpdate 是 Pod 更新之前和更新后的 hook
finalizersHandler:
- example.io/unready-blocker

# 或者也可以通过 label 定义
lifecycle:
inPlaceUpdate:
labelsHandler:
example.io/block-unready: "true"

升级/删除 Pod 前将其置为 NotReady

lifecycle:
preDelete:
markPodNotReady: true
finalizersHandler:
- example.io/unready-blocker
inPlaceUpdate:
markPodNotReady: true
finalizersHandler:
- example.io/unready-blocker

我们可以利用这一特性,在容器真正被停止之前将 Pod 上的流量先行排除,防止流量损失。

流转示意图

生命周期示意图

关于从 PreparingDelete 回到 Normal 状态,从设计上是支持的(通过撤销指定删除),但我们一般不建议这种用法。由于 PreparingDelete 状态的 Pod 不会被升级,当回到 Normal 状态后可能立即再进入发布阶段,对于用户处理 hook 是一个难题。

用户 controller 逻辑示例

按上述例子,可以定义:

在 CloneSet template 模板里带上这个字段:

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:
template:
metadata:
annotations:
example.io/initialing: "true"
finalizers:
- example.io/unready-blocker
# ...
lifecycle:
preDelete:
finalizersHandler:
- example.io/unready-blocker
inPlaceUpdate:
finalizersHandler:
- example.io/unready-blocker

而后用户 controller 的逻辑如下:

使用场景

因为各种各样的历史原因和客观因素,有些用户可能无法将自己公司的整套体系架构 Kubernetes 化,比如有些用户暂时无法使用 Kubernetes 本身提供的 Service 服务发现机制,而是使用了独立于 Kubernetes 之外的另外一套服务注册和发现体系。在这种架构下,如果用户对服务进行 Kubernetes 化改造,可能会遇到诸多问题。例如,每当 Kubernetes 成功创建出一个 Pod,都需要自行将该 Pod 注册到服务发现中心,以便能够对内对外提供服务;相应的,想要下线一个 Pod,也通常先要将其在服务发现中心删除,才能将 Pod 优雅下线,否则就可能导致流量损失。但是在原生的 Kubernetes 体系中, Pod 的生命周期由 Workload 管理(例如 Deployment),当这些 Workload 的 Replicas 字段发生变化后,相应的 Controller 会立即添加或删除掉 Pod,用户很难定制化地去管理 Pod 的生命周期。

面对这类问题,一般来说有两种解决思路:一是约束 Kubernetes 的弹性能力,例如规定只能由特定的链路对 Workload 进行扩缩容,以保证在删除 Pod 前先把 Pod IP 在服务注册中心摘除,但这样一来会制约 Kubernetes 本身的弹性能力, 并且也增加了链路管控的难度和风险。 二是在根本上改造现有的服务发现体系,显然这是一个更加漫长和高风险的事情。

CloneSet生命周期改造

那么有没有一种既能够充分利用 Kubernetes 弹性能力,又避免对既有服务发现体系进行改造,快速弥补两个系统之间的间隙的方法呢?

OpenKruise CloneSet 就提供了这样一组高度可定制化的扩展能力来专门应对此类场景,让用户能够对 Pod 生命周期做更精细化、定制化的管理。CloneSet 在 Pod 生命周期中几个重要的时间节点预留了 Hook,使得用户可以在这些时间节点插入一些定制化的扩展动作。比如,在 Pod 升级前,将 Pod IP 在服务发现中心删除,升级完成后再将 Pod IP 注册到服务发现中心,或者做一些特殊的嗅探和监控动作。

我们假设现在有这样一个场景:

并且对具体的需求做如下合理假设:

基于以上假设,其实我们就可以利用 CloneSet LifeCycle 来编写一个简单的 Operator 实现用户定义的 Pod 生命周期管理机制。

前面我们提到了 CloneSet LifeCycle 将 Pod 的生命周期定义为了 5 种状态,5 种状态之间的转换逻辑由一个状态机所控制。我们可以只选择自己所关心的一种或多种,编写一个独立的 Operator 来实现这些状态的转换,控制 Pod 的生命周期,并在所关心的时间节点插入自己的定制化逻辑。

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
metadata:
namespace: demo
n
本文标题:K8s增强版工作负载OpenKruise之CloneSet
当前路径:http://cdbrznjsb.com/article/cdcsddj.html

其他资讯

让你的专属顾问为你服务