CloneSet 控制器提供了高效管理无状态应用的能力,它可以对标原生的 Deployment,但 CloneSet 提供了很多增强功能。
按照 Kruise 的命名规范,CloneSet 是一个直接管理 Pod 的 Set 类型 workload。 一个简单的 CloneSet yaml 文件如下:
- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- metadata:
- labels:
- app: sample
- name: sample
- spec:
- replicas: 5
- selector:
- matchLabels:
- app: sample
- template:
- metadata:
- labels:
- app: sample
- spec:
- containers:
- - name: nginx
- image: nginx:alpine
CloneSet 允许用户配置 PVC 模板 volumeClaimTemplates,用来给每个 Pod 生成独享的 PVC,这是 Deployment 所不支持的。 如果用户没有指定这个模板,CloneSet 会创建不带 PVC 的 Pod。
一些注意点:
apps.kruise.io/cloneset-instance-id: xxx 的 label。关联的 Pod 和 PVC 会有相同的 instance-id,且它们的名字后缀都是这个 instance-id。以下是一个带有 PVC 模板的例子:
- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- metadata:
- labels:
- app: sample
- name: sample-data
- spec:
- replicas: 2
- selector:
- matchLabels:
- app: sample
- template:
- metadata:
- labels:
- app: sample
- spec:
- containers:
- - name: nginx
- image: nginx
- volumeMounts:
- - name: data-vol
- mountPath: /usr/share/nginx/html
- volumeClaimTemplates:
- - metadata:
- name: data-vol
- spec:
- storageClassName: "nfs-storage"
- accessModes: [ "ReadWriteOnce" ]
- resources:
- requests:
- storage: 10Mi
这样就实现了动态创建PVC
- [root@k8s-master][15:45:22][OK] ~/open-kruise
- #kubectl get clone
- NAME DESIRED UPDATED UPDATED_READY READY TOTAL AGE
- sample-data 2 2 2 2 2 16s
- [root@k8s-master][15:45:25][OK] ~/open-kruise
- #kubectl get pod
- NAME READY STATUS RESTARTS AGE
- sample-data-gfrkl 1/1 Running 0 21s
- sample-data-pp4zt 1/1 Running 0 21s
- [root@k8s-master][15:45:30][OK] ~/open-kruise
- #kubectl get pvc,pv
- NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
- persistentvolumeclaim/data-vol-sample-data-gfrkl Bound pvc-eaca0822-9621-4cf7-97e9-ebbf8d70f34b 10Mi RWO nfs-storage 25s
- persistentvolumeclaim/data-vol-sample-data-pp4zt Bound pvc-dfb31f15-465a-4533-9802-bc4722d66ac4 10Mi RWO nfs-storage 25s
-
- NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
- persistentvolume/pvc-dfb31f15-465a-4533-9802-bc4722d66ac4 10Mi RWO Delete Bound web/data-vol-sample-data-pp4zt nfs-storage 25s
- persistentvolume/pvc-eaca0822-9621-4cf7-97e9-ebbf8d70f34b 10Mi RWO Delete Bound web/data-vol-sample-data-gfrkl nfs-storage 25s
当一个 CloneSet 被缩容时,有时候用户需要指定一些 Pod 来删除。这对于 StatefulSet 或者 Deployment 来说是无法实现的,因为 StatefulSet 要根据序号来删除 Pod,而 Deployment/ReplicaSet 目前只能根据控制器里定义的排序来删除。
CloneSet 允许用户在缩小 replicas 数量的同时,指定想要删除的 Pod 名字。参考下面这个例子:
- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- spec:
- # ...
- replicas: 2
- scaleStrategy:
- podsToDelete:
- - sample-data-gfrkl
指定容器缩容例子:
- #查看当前活跃的容器
- [root@k8s-master][15:49:30][OK] ~/open-kruise
- #kubectl get po
- NAME READY STATUS RESTARTS AGE
- sample-data-89vp6 1/1 Running 0 3m28s
- sample-data-xcqn2 1/1 Running 0 35s
-
- #修改clone配置
- 添加如下:
- replicas: 1 #将副本数由2缩容到1
- scaleStrategy:
- podsToDelete:
- - sample-data-xcqn2 #指定容器名进行缩容
-
- #再次查看
- [root@k8s-master][16:04:22][OK] /etc/kubernetes/manifests
- #kubectl get po
- NAME READY STATUS RESTARTS AGE
- sample-data-89vp6 1/1 Running 0 4m
当控制器收到上面这个 CloneSet 更新之后,会确保 replicas 数量为 4。如果 podsToDelete 列表里写了一些 Pod 名字,控制器会优先删除这些 Pod。 对于已经被删除的 Pod,控制器会自动从 podsToDelete 列表中清理掉。
如果你只把 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 生命周期 hook (见下文)。
FEATURE STATE: Kruise v0.9.0
controller.kubernetes.io/pod-deletion-cost 是从 Kubernetes 1.21 版本后加入的 annotation,Deployment/ReplicaSet 在缩容时会参考这个 cost 数值来排序。 CloneSet 从 Kruise v0.9.0 版本后也同样支持了这个功能。
用户可以把这个 annotation 配置到 pod 上,值的范围在 [-2147483647, 2147483647]。 它表示这个 pod 相较于同个 CloneSet 下其他 pod 的 "删除代价",代价越小的 pod 删除优先级相对越高。 没有设置这个 annotation 的 pod 默认 deletion cost 是 0。
FEATURE STATE: Kruise v0.10.0
目前,CloneSet 支持 按同节点打散 和 按 pod topolocy spread constraints 打散。
如果在 CloneSet template 中存在 Pod Topology Spread Constraints 规则定义,则 controller 在这个 CloneSet 缩容的时候会根据 spread constraints 规则来所打散并选择要删除的 pod。 否则,controller 默认情况下是按同节点打散来选择要缩容的 pod。
默认情况下,CloneSet 在 Pod label 中设置的 controller-revision-hash 值为 ControllerRevision 的完整名字,比如
- apiVersion: v1
- kind: Pod
- metadata:
- labels:
- controller-revision-hash: demo-cloneset-956df7994
它是通过 CloneSet 名字和 ControllerRevision hash 值拼接而成。 通常 hash 值长度为 8~10 个字符,而 Kubernetes 中的 label 值不能超过 63 个字符。 因此 CloneSet 的名字一般是不能超过 52 个字符的。
因此 CloneSetShortHash 这个新的 feature-gate 被引入。 如果它被打开,CloneSet 会将 controller-revision-hash 的值只设置为 hash 值,比如 956df7994,因此 CloneSet 名字则不会有任何限制了。
不用担心,即使打开了 CloneSetShortHash,CloneSet 仍然会识别和管理过去存量的 revision label 为完整格式的 Pod。
从 Kruise v1.1.0 版本开始,CloneSet 还会给 Pod 中加入另一个 pod-template-hash 标签,它永远是短 hash 的形式。
CloneSet 扩容时可以指定 ScaleStrategy.MaxUnavailable 来限制扩容的步长,以达到服务应用影响最小化的目的。 它可以设置为一个绝对值或者百分比,如果不填,则 Kruise 会设置为默认值为 nil,即表示不设限制。
该字段可以配合 Spec.MinReadySeconds 字段使用, 例如:
- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- spec:
- # ...
- minReadySeconds: 60
- scaleStrategy:
- maxUnavailable: 1
上述配置能达到的效果是:在扩容时,只有当上一个扩容出的 Pod 已经 Ready 超过一分钟后,CloneSet 才会执行创建下一个 Pod 的操作。
CloneSet 提供了 3 种升级方式,默认为 ReCreate:
ReCreate: 控制器会删除旧 Pod 和它的 PVC,然后用新版本重新创建出来。InPlaceIfPossible: 控制器会优先尝试原地升级 Pod,如果不行再采用重建升级。具体参考下方阅读文档。InPlaceOnly: 控制器只允许采用原地升级。因此,用户只能修改上一条中的限制字段,如果尝试修改其他字段会被 Kruise 拒绝。我们还在原地升级中提供了 graceful period 选项,作为优雅原地升级的策略。用户如果配置了 gracePeriodSeconds 这个字段,控制器在原地升级的过程中会先把 Pod status 改为 not-ready,然后等一段时间(gracePeriodSeconds),最后再去修改 Pod spec 中的镜像版本。 这样,就为 endpoints-controller 这些控制器留出了充足的时间来将 Pod 从 endpoints 端点列表中去除。
spec.template 中定义了当前 CloneSet 中最新的 Pod 模板。 控制器会为每次更新过的 spec.template 计算一个 revision hash 值,比如针对开头的 CloneSet 例子, 控制器会为 template 计算出 revision hash 为 sample-744d4796cc 并上报到 CloneSet status 中。
- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- metadata:
- generation: 1
- # ...
- spec:
- replicas: 5
- # ...
- status:
- observedGeneration: 1
- readyReplicas: 5
- replicas: 5
- currentRevision: sample-d4d4fb5bd
- updateRevision: sample-d4d4fb5bd
- updatedReadyReplicas: 5
- updatedReplicas: 5
- # ...
这里是对 CloneSet status 中的字段说明:
status.replicas: Pod 总数status.readyReplicas: ready Pod 数量status.availableReplicas: ready and available Pod 数量 (满足 minReadySeconds, 且 lifecycle state 为 Normal)status.currentRevision: 最近一次全量 Pod 推平版本的 revision hash 值status.updateRevision: 最新版本的 revision hash 值status.updatedReplicas: 最新版本的 Pod 数量status.updatedReadyReplicas: 最新版本的 ready Pod 数量 FEATURE STATE: Kruise v1.2.0status.expectedUpdatedReplicas: 需要升级到最新版本 Pod 的数量(包含已经升级的数量),该字段根据用户当前设置的 .spec.updateStrategy.partition 字段计算得到。Partition 的语义是 保留旧版本 Pod 的数量或百分比,默认为 0。这里的 partition 不表示任何 order 序号。
如果在发布过程中设置了 partition:
(replicas - partition) 数量的 Pod 更新到最新版本。(replicas * (100% - partition)) 数量的 Pod 更新到最新版本。FEATURE STATE: Kruise v1.2.0
partition 是百分比, 并且满足 partition < 100% && replicas > 1 , CloneSet 会保证 至少有一个 Pod 会被升级到最新版本。.status.updatedReplicas >= .status.ExpectedUpdatedReplicas 条件,来判断在当前 partition 字段的限制下,CloneSet 是否已经完成了预期数量 Pod 的版本升级。比如,我们将 CloneSet 例子的 image 更新为 nginx:mainline 并且设置 partition=3。过了一会,查到的 CloneSet 如下:
- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- metadata:
- # ...
- generation: 2
- spec:
- replicas: 5
- template:
- metadata:
- labels:
- app: sample
- spec:
- containers:
- - image: nginx:mainline
- imagePullPolicy: Always
- name: nginx
- updateStrategy:
- partition: 3
- # ...
- status:
- observedGeneration: 2
- readyReplicas: 5
- replicas: 5
- currentRevision: sample-d4d4fb5bd
- updateRevision: sample-56dfb978d4
- updatedReadyReplicas: 2
- updatedReplicas: 2
注意 status.updateRevision 已经更新为 sample-56dfb978d4 新的值。 由于我们设置了 partition=3,控制器只升级了 2 个 Pod。
- $ kubectl get pod -L controller-revision-hash
- NAME READY STATUS RESTARTS AGE CONTROLLER-REVISION-HASH
- sample-chvnr 1/1 Running 0 6m46s sample-d4d4fb5bd
- sample-j6c4s 1/1 Running 0 6m46s sample-d4d4fb5bd
- sample-ns85c 1/1 Running 0 6m46s sample-d4d4fb5bd
- sample-jnjdp 1/1 Running 0 10s sample-56dfb978d4
- sample-qqglp 1/1 Running 0 18s sample-56dfb978d4
默认情况下,partition 只控制 Pod 更新到 status.updateRevision 新版本。 也就是说以上面这个 CloneSet 来看,当 partition 5 -> 3 时,CloneSet 会升级 2 个 Pod 到 status.updateRevision 版本。 而当把 partition 3 -> 5 修改回去时,CloneSet 不会做任何事情。
但是如果你启用了 CloneSetPartitionRollback 这个 feature-gate, 上面这个场景下 CloneSet 会把 2 个 status.updateRevision 版本的 Pod 重新回滚为 status.currentRevision 版本。
MaxUnavailable 是 CloneSet 限制下属最多不可用的 Pod 数量。 它可以设置为一个绝对值或者百分比,如果不填 Kruise 会设置为默认值 20%。
- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- spec:
- # ...
- updateStrategy:
- maxUnavailable: 20%
从 Kruise v0.9.0 版本开始,maxUnavailable 不仅会保护发布,也会对 Pod 指定删除生效。
也就是说用户通过 podsToDelete 或 apps.kruise.io/specified-delete: true 来指定一个 Pod 期望删除, CloneSet 只会在当前不可用 Pod 数量(相对于 replicas 总数)小于 maxUnavailable 的时候才执行删除。
MaxSurge 是 CloneSet 控制最多能扩出来超过 replicas 的 Pod 数量。 它可以设置为一个绝对值或者百分比,如果不填 Kruise 会设置为默认值 0。
如果发布的时候设置了 maxSurge,控制器会先多扩出来 maxSurge 数量的 Pod(此时 Pod 总数为 (replicas+maxSurge)),然后再开始发布存量的 Pod。 然后,当新版本 Pod 数量已经满足 partition 要求之后,控制器会再把多余的 maxSurge 数量的 Pod 删除掉,保证最终的 Pod 数量符合 replicas。
要说明的是,maxSurge 不允许配合 InPlaceOnly 更新模式使用。 另外,如果是与 InPlaceIfPossible 策略配合使用,控制器会先扩出来 maxSurge 数量的 Pod,再对存量 Pod 做原地升级。
- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- spec:
- # ...
- updateStrategy:
- maxSurge: 3
从 Kruise v0.9.0 版本开始,maxSurge 不仅会保护发布,也会对 Pod 指定删除生效。
也就是说用户通过 podsToDelete 或 apps.kruise.io/specified-delete: true 来指定一个 Pod 期望删除, CloneSet 有可能会先创建一个新 Pod、等待它 ready 之后、再删除旧 Pod。这取决于当时的 maxUnavailable 和实际不可用 Pod 数量。
比如:
maxUnavailable=2, maxSurge=1 且有一个 pod-a 处于不可用状态, 如果你对另一个 pod-b 打标 apps.kruise.io/specified-delete: true 或将它的名字加入 podsToDelete, 那么 CloneSet 会立即删除它,然后创建一个新 Pod。maxUnavailable=1, maxSurge=1 且有一个 pod-a 处于不可用状态, 如果你对另一个 pod-b 打标 apps.kruise.io/specified-delete: true 或将它的名字加入 podsToDelete, 那么 CloneSet 会先新建一个 Pod、等待它 ready,最后再删除 pod-b。maxUnavailable=1, maxSurge=1 且有一个 pod-a 处于不可用状态, 如果你对这个 pod-a 打标 apps.kruise.io/specified-delete: true 或将它的名字加入 podsToDelete, 那么 CloneSet 会立即删除它,然后创建一个新 Pod。当控制器选择 Pod 做升级时,默认是有一套根据 Pod phase/conditions 的排序逻辑: unscheduled < scheduled, pending < unknown < running, not-ready < ready。 在此之外,CloneSet 也提供了增强的 priority(优先级) 和 scatter(打散) 策略来允许用户自定义发布顺序。
这个策略定义了控制器计算 Pod 发布优先级的规则,所有需要更新的 Pod 都会通过这个优先级规则计算后排序。 目前 priority 可以通过 weight(权重) 和 order(序号) 两种方式来指定。
weight: Pod 优先级是由所有 weights 列表中的 term 来计算 match selector 得出。如下:- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- spec:
- # ...
- updateStrategy:
- priorityStrategy:
- weightPriority:
- - weight: 50
- matchSelector:
- matchLabels:
- test-key: foo
- - weight: 30
- matchSelector:
- matchLabels:
- test-key: bar
order: Pod 优先级是由 orderKey 的 value 决定,这里要求对应的 value 的结尾能解析为 int 值。比如 value "5" 的优先级是 5,value "sts-10" 的优先级是 10。- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- spec:
- # ...
- updateStrategy:
- priorityStrategy:
- orderPriority:
- - orderedKey: some-label-key
这个策略定义了如何将一类 Pod 打散到整个发布过程中。 比如,针对一个 replica=10 的 CloneSet,我们在 3 个 Pod 中添加了 foo=bar 标签、并设置对应的 scatter 策略,那么在发布的时候这 3 个 Pod 会排在第 1、6、10 个发布。
- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- spec:
- # ...
- updateStrategy:
- scatterStrategy:
- - key: foo
- value: bar
注意:
priority 和 scatter 策略可以一起设置,但我们强烈推荐同时只用其中一个。scatter 策略,我们强烈建议只配置一个 term (key-value)。否则,实际的打散发布顺序可能会不太好理解。最后要说明的是,使用上述发布顺序策略都要求对特定一些 Pod 打标,这是在 CloneSet 中没有提供的。
用户可以通过设置 paused 为 true 暂停发布,不过控制器还是会做 replicas 数量管理:
- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- spec:
- # ...
- updateStrategy:
- paused: true
如果你在安装或升级 Kruise 的时候启用了 PreDownloadImageForInPlaceUpdate feature-gate, CloneSet 控制器会自动在所有旧版本 pod 所在 node 节点上预热你正在灰度发布的新版本镜像。 这对于应用发布加速很有帮助。
默认情况下 CloneSet 每个新镜像预热时的并发度都是 1,也就是一个个节点拉镜像。 如果需要调整,你可以通过 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: "10"
- apps.kruise.io/image-predownload-min-updated-ready-pods: "3"
注意,为了避免大部分不必要的镜像拉取,目前只针对 replicas > 3 的 CloneSet 做自动预热。
每个 CloneSet 管理的 Pod 会有明确所处的状态,在 Pod label 中的 lifecycle.apps.kruise.io/state 标记:
而生命周期钩子,则是通过在上述状态流转中卡点,来实现原地升级前后、删除前的自定义操作(比如开关流量、告警等)。
- 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"`
- /*********************************************************************/
- }
示例:
- apiVersion: apps.kruise.io/v1alpha1
- kind: CloneSet
- spec:
-
- # 通过 finalizer 定义 hook
- lifecycle:
- preDelete:
- finalizersHandler:
- - example.io/unready-blocker
- inPlaceUpdate:
- finalizersHandler:
- - example.io/unready-blocker
-
- # 或者也可以通过 label 定义
- lifecycle:
- inPlaceUpdate:
- labelsHandler:
- example.io/block-unready: "true"
FEATURE STATE: Kruise v1.2.0
- lifecycle:
- preDelete:
- markPodNotReady: true
- finalizersHandler:
- - example.io/unready-blocker
- inPlaceUpdate:
- markPodNotReady: true
- finalizersHandler:
- - example.io/unready-blocker
preDelete.markPodNotReady=true:
PreparingDelete 状态时,将 KruisePodReady 这个 Pod Condition 设置为 False, Pod 将变为 NotReady。inPlaceUpdate.markPodNotReady=true:
PreparingUpdate 状态时,将 KruisePodReady 这个 Pod Condition 设置为 False, Pod 将变为 NotReady。KruisePodReady 这个 Pod Condition 设置回 True。用户可以利用这一特性,在容器真正被停止之前将 Pod 上的流量先行排除,防止流量损失。
注意: 该特性仅在 Pod 被注入 KruisePodReady 这个 ReadinessGate 时生效。

PreparingDelete。等用户 controller 完成任务去掉 label/finalizer、Pod 不符合 preDelete 条件后,kruise 才执行 Pod 删除PreparingDelete 状态的 Pod 处于删除阶段,不会被升级PreparingUpdateUpdating 并开始升级UpdatedNormal 并判断为升级成功关于从 PreparingDelete 回到 Normal 状态,从设计上是支持的(通过撤销指定删除),但我们一般不建议这种用法。由于 PreparingDelete 状态的 Pod 不会被升级,当回到 Normal 状态后可能立即再进入发布阶段,对于用户处理 hook 是一个难题。
按上述例子,可以定义:
example.io/unready-blocker finalizer 作为 hookexample.io/initialing annotation 作为初始化标记在 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 的逻辑如下:
Normal 状态的 Pod,如果 annotation 中有 example.io/initialing: true 并且 Pod status 中的 ready condition 为 True,则接入流量、去除这个 annotationPreparingDelete 和 PreparingUpdate 状态的 Pod,切走流量,并去除 example.io/unready-blocker finalizerUpdated 状态的 Pod,接入流量,并打上 example.io/unready-blocker finalizer