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 处于删除阶段,不会被升级PreparingUpdate
Updating
并开始升级Updated
Normal
并判断为升级成功关于从 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