Skip to main content

· 阅读需要 1 分钟
Mingshan Zhao

OpenKruise( https://github.com/openkruise/kruise )是阿里云开源的云原生应用自动化管理套件,也是当前托管在 Cloud Native Computing Foundation (CNCF) 下的孵化项目。它来自阿里巴巴多年来容器化、云原生的技术沉淀,是阿里内部生产环境大规模应用的基于 Kubernetes 之上的标准扩展组件,也是紧贴上游社区标准、适应互联网规模化场景的技术理念与最佳实践。

OpenKruise 在 2023.3.31 发布了最新的 v1.4 版本(ChangeLog),新增 Job Sidecar Terminator 重磅功能,本文以下对新版本做整体的概览介绍。

1. 重要更新

  • 为了方便大家使用 Kruise 增强能力,默认打开了一些稳定的能力,如下:ResourcesDeletionProtection, WorkloadSpread, PodUnavailableBudgetDeleteGate, InPlaceUpdateEnvFromMetadata, StatefulSetAutoDeletePVC, PodProbeMarkerGate。上述能力大部分是需要特别配置才会生效的,所以默认打开一般不会对存量集群造成影响,如果有一些特性不想使用,可以在升级时关闭。
  • Kruise-Manager leader 选举方式从 configmaps 迁移为 configmapsleases,为后面迁移到 leases 方式做准备,另外,这是官方提供的平滑升级的方式,不会对存量的集群造成影响。

2. Sidecar容器管理能力:Job Sidecar Terminator

在 Kubernetes 中对于 Job 类型 Workload,人们通常希望当主容器完成任务并退出后,Pod 进入已完成状态。然而,当这些 Pod 拥有 Long-Running Sidecar 容器时,由于 Sidecar 容器在主容器退出后无法自行退出,导致 Pod 一直无法进入已完成状态。面对这个问题,社区的常见解决方案一般都需要对 Main 和 Sidecar 进行改造,两者通过 Volume 共享来实现 Main 容器退出之后,Sidecar 容器完成退出的效果。

社区的解决方案可以解决这个问题,但是需要对容器进行改造,尤其对于社区通用的 Sidecar 容器,改造和维护的成本太高了。

为此,我们在 Kruise 中加入了一个名为 SidecarTerminator 的控制器,专门用于在此类场景下,监听主容器的完成状态,并选择合适的时机终止掉 Pod 中的 sidecar 容器,并且无需对 Main 和 Sidecar 容器进行侵入式改造。

运行在普通节点的 Pod

对于运行于普通节点的 Pod(常规Kubelet),使用该特性非常简单,用户只需要在要在目标 sidecar 容器中添加一个特殊的 env 对其进行标识,控制器会在恰当的时机利用 Kruise Daemon 提供的 CRR 的能力,将这些 sidecar 容器终止:

kind: Job
spec:
template:
spec:
containers:
- name: sidecar
env:
- name: KRUISE_TERMINATE_SIDECAR_WHEN_JOB_EXIT
value: "true"
- name: main
...

运行在虚拟节点的 Pod

对于一些提供 Serverless 容器的平台,例如 ECI 或者 Fargate, 其 Pods 只能运行于 Virtual-Kubelet 之类的虚拟节点。 然而,Kruise Daemon 无法部署和工作在这些虚拟节点之上,导致无法使用 CRR 能力将容器终止。 但幸运地是,我们可以借助原生 Kubernetes 提供的 Pod 原地升级机制来达到同样的目的:只需要构造一个特殊镜像,这个镜像的唯一作用就是当被拉起后,会快速地主动退出,这样一来,只需要在退出 sidecar 时,将原本的 sidecar 镜像替换为快速退出镜像,即可达到退出 sidecar 的目的。

步骤一: 准备一个快速退出镜像

  • 该镜像只需要具备非常简单的逻辑:当其被拉起后,直接退出,且退出码为 0。
  • 该镜像需要兼容原 sidecar 镜像的 commands 和 args,以防容器被拉起时报错。

步骤二: 配置你的 sidecar 容器

kind: Job
spec:
template:
spec:
containers:
- name: sidecar
env:
- name: KRUISE_TERMINATE_SIDECAR_WHEN_JOB_EXIT_WITH_IMAGE
value: "example/quick-exit:v1.0.0"
- name: main
...

使用你自己准备的快速退出镜像来替换上述 "example/quick-exit:v1.0.0".

注意事项

  • sidecar 容器必须能够响应 SIGTERM 信号,并且当收到此信号时,entrypoint 进程需要退出(即 sidecar 容器需要退出),并且退出码应当为 0。
  • 该特性适用于任意 Job 类型 Workload 所管理的 Pod,只要他们的 RestartPolicy 为 Never/OnFailure 即可。
  • 具有环境变量 KRUISE_TERMINATE_SIDECAR_WHEN_JOB_EXIT 的容器将被视为 sidecar 容器,其他容器将被视为主容器,当所有主容器完成后,sidecar 容器才会被终止:
    • 在 Never 重启策略下,主容器一旦退出,将被视为"已完成"。
    • 在 OnFailure 重启策略下,主容器退出代码必须为0,才会被视为"已完成"。
  • 且运行在普通节点方式下,KRUISE_TERMINATE_SIDECAR_WHEN_JOB_EXIT 的优先级高于KRUISE_TERMINATE_SIDECAR_WHEN_JOB_EXIT_WITH_IMAGE

3. 增强版本的工作负载

CloneSet 优化性能 :新增 FeatureGate CloneSetEventHandlerOptimization

当前,无论是 Pod 的状态变化还是 Metadata 变化,Pod Update 事件都会触发 CloneSet reconcile 逻辑。CloneSet Reconcile 默认配置了三个 worker,对于集群规模较小的场景,这种情况并不会造成问题。

但对于集群规模较大或 Pod Update 事件较多的情况,这些无效的 reconcile 将会阻塞真正的 CloneSet reconcile,进而导致 CloneSet 的滚动升级等变更延迟。为了解决这个问题,可以打开 feature-gate CloneSetEventHandlerOptimization 来减少一些不必要的 reconcile 入队。

CloneSet 新增 disablePVCReuse 字段

如果一个 Pod 被外部直接调用删除或驱逐时,这个 Pod 关联的 PVCs 还都存在;并且 CloneSet controller 发现数量不足重新扩容时,新扩出来的 Pod 会复用原 Pod 的 instance-id 并关联原来的 PVCs。

然而,如果 Pod 所在的 Node 出现异常,复用可能会导致新 Pod 启动失败,详情参考 issue 1099。为了解决这个问题,您可以设置字段 DisablePVCReuse=true,当 Pod 被驱逐或者删除后,与 Pod 相关的 PVCs 将被自动删除,不再被复用。

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:
...
replicas: 4
scaleStrategy:
disablePVCReuse: true

CloneSet 增加 PreNormal 生命周期钩子

CloneSet 已经支持了PreparingUpdate、PreparingDelete 两种生命周期钩子,用于应用的优雅下线,详情参考社区文档。为了支持优雅上线的场景,本次新增加了 PreNormal 状态,具体如下:

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:
# define with finalizer
lifecycle:
preNormal:
finalizersHandler:
- example.io/unready-blocker

# or define with label
# lifecycle:
# preNormal:
# labelsHandler:
# example.io/block-unready: "true"

当 CloneSet 创建一个 Pod(包括正常扩容和重建升级)时:

  • 如果 Pod 满足了 PreNormal hook 的定义,才会被认为是 Available,并且才会进入 Normal 状态 这对于一些 Pod 创建时的后置检查很有用,比如你可以检查 Pod 是否已经挂载到 SLB 后端,从而避免滚动升级时,旧实例销毁后,新实例挂载失败导致的流量损失。

4. 高级的应用运维能力

容器重启新增 forceRecreate 字段

当创建 CRR 资源时,如果容器正在启动过程中,CRR 将不会再重启容器。如果您想要强制重启容器,可以使用以下字段开启:

apiVersion: apps.kruise.io/v1alpha1
kind: ContainerRecreateRequest
spec:
...
strategy:
forceRecreate: true

镜像预热支持 Attach metadata into cri interface

当 Kubelet 创建 Pod 时,Kubelet 将会 attach metadata 到 container runtime cri 接口。镜像仓库可以根据这些 metadata 信息来确定拉镜像的来源业务,如果发生了仓库过载、压力过大的情况,可以对具体的业务进行降级处理。OpenKruise 镜像预热同样支持类似的能力,如下:

apiVersion: apps.kruise.io/v1alpha1
kind: ImagePullJob
spec:
...
image: nginx:1.9.1
sandboxConfig:
annotations:
io.kubernetes.image.metrics.tags: "cluster=cn-shanghai"
labels:
io.kubernetes.image.app: "foo"

社区参与

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

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

· 阅读需要 1 分钟
Mingshan Zhao

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

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

版本解析

在版本v1.3中,OpenKruise提供了新的CRD资源 PodProbeMarker,改善了大规模集群的一些性能问题,Advanced DaemonSet支持镜像预热, 以及 CloneSet、WorkloadSpread、Advanced CronJob、SidecarSet一些新的特性。

1. 新增 CRD 和 Controller:PodProbeMarker

Kubernetes提供了三种默认的Pod生命周期管理:

  • Readiness Probe 用来判断业务容器是否已经准备好响应用户请求,如果检查失败,会将该Pod从Service Endpoints中剔除。
  • Liveness Probe 用来判断容器的健康状态,如果检查失败,kubelet将会重启该容器。
  • Startup Probe 用来判断容器是否启动完成,如果定义了该Probe,那么Readiness Probe与Liveness Probe将会在它成功之后再执行。

所以Kubernetes中提供的Probe能力都已经限定了特定的语义以及相关的行为。除此之外,其实还是存在自定义Probe语义以及相关行为的需求,例如:

  • GameServer定义 Idle Probe 用来判断该Pod当前是否存在游戏对局,如果没有,从成本优化的角度,可以将该Pod缩容掉。
  • K8S Operator定义 main-secondary Probe 来判断当前Pod的角色(main or secondary),升级的时候,可以优先升级 secondary,进而达到升级过程只有一次选主的行为,降低升级过程中服务抖动时间。

OpenKruise提供了自定义Probe的能力,并将结果返回到Pod Status中,用户可以根据该结果决定后续的行为。

PodProbeMarker配置如下:

apiVersion: apps.kruise.io/v1alpha1
kind: PodProbeMarker
metadata:
name: game-server-probe
namespace: ns
spec:
selector:
matchLabels:
app: game-server
probes:
- name: Idle
containerName: game-server
probe:
exec:
command:
- /home/game/idle.sh
initialDelaySeconds: 10
timeoutSeconds: 3
periodSeconds: 10
successThreshold: 1
failureThreshold: 3
markerPolicy:
- state: Succeeded
labels:
gameserver-idle: 'true'
annotations:
controller.kubernetes.io/pod-deletion-cost: '-10'
- state: Failed
labels:
gameserver-idle: 'false'
annotations:
controller.kubernetes.io/pod-deletion-cost: '10'
podConditionType: game.io/idle

PodProbeMarker结果可以通过Pod对象查看:

apiVersion: v1
kind: Pod
metadata:
labels:
app: game-server
gameserver-idle: 'true'
annotations:
controller.kubernetes.io/pod-deletion-cost: '-10'
name: game-server-58cb9f5688-7sbd8
namespace: ns
spec:
...
status:
conditions:
# podConditionType
- type: game.io/idle
# Probe State 'Succeeded' indicates 'True', and 'Failed' indicates 'False'
status: "True"
lastProbeTime: "2022-09-09T07:13:04Z"
lastTransitionTime: "2022-09-09T07:13:04Z"
# If the probe fails to execute, the message is stderr
message: ""

2. 性能优化:大规模集群性能显著提升

  • #1026 引入了延迟入队机制,大幅优化了在大规模应用集群下 kruise-manager 拉起时的 CloneSet 控制器工作队列堆积问题,在理想情况下初始化时间减少了 80% 以上。
  • #1027 优化 PodUnavailableBudget 控制器 Event Handler 逻辑,减少无关 Pod 入队数量。
  • #1011 通过缓存机制,优化了大规模集群下 Advanced DaemonSet 重复模拟 Pod 调度计算的 CPU、Memory 消耗。
  • #1015, #1068 大幅降低了大规模集群下的运行时内存消耗。弥补了 v1.1 版本中 Disable DeepCopy 的一些疏漏点,减少 expressions 类型 label selector 的转换消耗。

3. SidecarSet 支持注入特定的历史版本

SidecarSet 通过 ControllerRevision 记录了关于 containersvolumesinitContainersimagePullSecretspatchPodMetadata 等字段的历史版本,并允许用户在 Pod 创建时选择特定的历史版本进行注入。 基于这一特性,用户可以规避在 SidecarSet 灰度发布时,因Deployment 等 Workload 扩容、升级等操作带来的 SidecarSet 发布风险。如果不选择注入版本,SidecarSet 将对重建 Pod 默认全都注入最新版本 Sidecar。

SidecarSet 相关 ControllerRevision 资源被放置在了与 Kruise-Manager 相同的命名空间中,用户可以使用 kubectl get controllerrevisions -n kruise-system -l kruise.io/sidecarset-name=your-sidecarset-name 来查看。 此外,用户还可以通过 SidecarSet 的 status.latestRevision 字段看到当前版本对应的 ControllerRevision 名称,以方便自行记录。

通过 ControllerRevision 名称指定注入的 Sidecar 版本

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: sidecarset
spec:
...
injectionStrategy:
revision:
revisionName: specific-controllerRevision-name

通过自定义版本标识指定注入的 Sidecar 版本

用户可以通过在发版时,同时给 SidecarSet 打上 apps.kruise.io/sidecarset-custom-version=your-version-id 来标记每一个历史版本,SidecarSet 会将这个 label 向下带入到对应的 ControllerRevision 对象,以便用户进行筛选,并且允许用户在选择注入历史版本时,使用该 your-version-id 来进行描述。

假设用户只想灰度 10% 的 Pods 到 version-2,并且对于新创建的 Pod 希望都注入更加稳定的 version-1 版本来控制灰度风险:

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: sidecarset
labels:
apps.kruise.io/sidecarset-custom-version: version-2
spec:
...
updateStrategy:
partition: 90%
injectionStrategy:
revision:
customVersion: version-1

4. SidecarSet 支持注入 Pod Annotations

SidecarSet支持注入Pod Annotations,配置如下:

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
spec:
containers:
...
patchPodMetadata:
- annotations:
oom-score: '{"log-agent": 1}'
custom.example.com/sidecar-configuration: '{"command": "/home/admin/bin/start.sh", "log-level": "3"}'
patchPolicy: MergePatchJson
- annotations:
apps.kruise.io/container-launch-priority: Ordered
patchPolicy: Overwrite | Retain

patchPolicy为注入的策略,如下:

  • Retain: 默认策略,如果Pod中存在 annotation[key]=value ,则保留Pod原有的value。只有当 Pod中不存在 annotation[key] 时,才注入 annotations[key]=value。
  • Overwrite: 与 Retain 对应,当 Pod 中存在 annotation[key]=value,将被强制覆盖为 value2。
  • MergePatchJson: 与 Overwrite 对应,annotations value为 json 字符串。如果 Pod 不存在该 annotations[key],则直接注入。如果存在,则进行 json value合并。 例如:Pod中存在 annotations[oom-score]='{"main": 2}',注入后将 value json合并为 annotations[oom-score]='{"log-agent": 1, "main": 2}'。

注意: patchPolicy为Overwrite和MergePatchJson时,SidecarSet原地升级 Sidecar Container时,能够同步更新该 annotations。但是,如果只修改annotations则不能生效,只能搭配Sidecar容器镜像一起原地升级。 patchPolicy为Retain时,SidecarSet原地升级 Sidecar Container时,将不会同步更新该 annotations。

上述配置后,sidecarSet在注入sidecar container时,会注入Pod annotations,如下:

apiVersion: v1
kind: Pod
metadata:
annotations:
apps.kruise.io/container-launch-priority: Ordered
oom-score: '{"log-agent": 1, "main": 2}'
custom.example.com/sidecar-configuration: '{"command": "/home/admin/bin/start.sh", "log-level": "3"}'
name: test-pod
spec:
containers:
...

注意: SidecarSet从安全的考虑不应该注入或修改除 sidecar container 之外的 Pod 字段,所以如果想要使用该能力,首先需要配置 SidecarSet_PatchPodMetadata_WhiteList 白名单 或通过 Feature-gate SidecarSetPatchPodMetadataDefaultsAllowed=true 关闭白名单校验。

5. Advanced DaemonSet 支持镜像预热

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

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

apiVersion: apps.kruise.io/v1alpha1
kind: DaemonSet
metadata:
annotations:
apps.kruise.io/image-predownload-parallelism: "10"

6. CloneSet 扩缩容与 PreparingDelete

默认情况下,CloneSet 将处于 PreparingDelete 状态的 Pod 视为正常,意味着这些 Pod 仍然被计算在 replicas 数量中。

在这种情况下:

  • 如果你将 replicasN 改为 N-1,当一个要删除的 Pod 还在 PreparingDelete 状态中时,你重新将 replicas 改为 N,CloneSet 会将这个 Pod 重新置为 Normal 状态。
  • 如果你将 replicasN 改为 N-1 的同时在 podsToDelete 中设置了一个 Pod,当这个 Pod 还在 PreparingDelete 状态中时,你重新将 replicas 改为 N,CloneSet 会等到这个 Pod 真正进入 terminating 之后再扩容一个 Pod 出来。
  • 如果你在不改变 replicas 的时候指定删除一个 Pod,当这个 Pod 还在 PreparingDelete 状态中时,CloneSet 会等到这个 Pod 真正进入 terminating 之后再扩容一个 Pod 出来。

从 Kruise v1.3.0 版本开始,你可以在 CloneSet 中设置一个 apps.kruise.io/cloneset-scaling-exclude-preparing-delete: "true" 标签,它标志着这个 CloneSet 不会将 PreparingDelete 状态的 Pod 计算在 replicas 数量中。

在这种情况下:

  • 如果你将 replicasN 改为 N-1,当一个要删除的 Pod 还在 PreparingDelete 状态中时,你重新将 replicas 改为 N,CloneSet 会将这个 Pod 重新置为 Normal 状态。
  • 如果你将 replicasN 改为 N-1 的同时在 podsToDelete 中设置了一个 Pod,当这个 Pod 还在 PreparingDelete 状态中时,你重新将 replicas 改为 N,CloneSet 会立即创建一个新 Pod。
  • 如果你在不改变 replicas 的时候指定删除一个 Pod,当这个 Pod 还在 PreparingDelete 状态中时,CloneSet 会立即创建一个新 Pod。

7. Advanced CronJob Time zones

默认情况下,所有 AdvancedCronJob schedule 调度时,都是基于 kruise-controller-manager 容器本地的时区所计算的。

不过,在 v1.3.0 版本中我们引入了 spec.timeZone 字段,你可以将它设置为任意合法时区的名字。例如,设置 spec.timeZone: "Asia/Shanghai" 则 Kruise 会根据国内的时区计算 schedule 任务触发时间。

Go 标准库中内置了时区数据库,作为在容器的系统环境中没有外置数据库时的 fallback 选择。

8. 其他改动

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

社区参与

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

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

· 阅读需要 1 分钟
Siyu Wang

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

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

版本解析

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

1. 新增 CRD 和 Controller: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

“固定 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=8partition=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 页面,来查看更多的改动以及它们的作者与提交记录。

社区参与

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

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

· 阅读需要 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)

· 阅读需要 1 分钟
Siyu Wang

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

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

openkruise-features|center|450x400

总得来看,目前 OpenKruise 提供的能力分为几个领域:

  • 应用工作负载:面向无状态、有状态、daemon 等多种类型应用的高级部署发布策略,例如原地升级、灰度流式发布等。
  • Sidecar 容器管理:支持独立定义 sidecar 容器,完成动态注入、独立原地升级、热升级等功能。
  • 增强运维能力:包括容器原地重启、镜像预拉取、容器启动顺序保障等。
  • 应用分区管理:管理应用在多个分区(可用区、不同机型等)上的部署比例、顺序、优先级等。
  • 应用安全防护:帮助应用在 Kubernetes 之上获得更高的安全性保障与可用性防护。

版本解析

在 v1.0 大版本中,OpenKruise 带来了多种新的特性,同时也对不少已有功能做了增强与优化。

首先要说的是,从 v1.0 开始 OpenKruise 将 CRD/WehhookConfiguration 等资源配置的版本从 v1beta1 升级到 v1,因此可以支持 Kubernetes v1.22 及以上版本的集群,但同时也要求 Kubernetes 的版本不能低于 v1.16

以下对 v1.0 的部分功能做简要介绍,详细的 ChangeLog 列表请查看 OpenKruise Github 上的 release 说明以及官网文档。

1. 支持环境变量原地升级

Author: @FillZpp

OpenKruise 从早期版本开始就支持了 “原地升级” 功能,主要应用于 CloneSet 与 Advanced StatefulSet 两种工作负载上。简单来说,原地升级使得应用在升级的过程中,不需要删除、新建 Pod 对象,而是通过对 Pod 中容器配置的修改来达到升级的目的。

inplace-update-comparation|center|450x400

如上图所示,原地升级过程中只修改了 Pod 中的字段,因此:

  1. 可以避免如 调度分配 IP分配、挂载盘 等额外的操作和代价。
  2. 更快的镜像拉取,因为可以复用已有旧镜像的大部分 layer 层,只需要拉取新镜像变化的一些 layer。
  3. 当一个容器在原地升级时,Pod 的网络、挂载盘、以及 Pod 中的其他容器不会受到影响,仍然维持运行。

然而,OpenKruise 过去只能对 Pod 中 image 字段的更新做原地升级,对于其他字段仍然只能采用与 Deployment 相似的重建升级。一直以来,我们收到很多用户反馈,希望支持对 env 等更多字段的原地升级 -- 由于受到 kube-apiserver 的限制,这是很难做到的。

经过我们的不懈努力,OpenKruise 终于在 v1.0 版本中,支持了通过 Downward API 的方式支持了 env 环境变量的原地升级。例如对以下CloneSet YAML,用户将配置定义在 annotation 中并关联到对应 env 中。后续在修改配置时,只需要更新 annotation value 中的值,Kruise 就会对 Pod 中所有 env 里引用了这个 annotation 的容器触发原地重建,从而生效这个新的 value 配置。

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

与此同时,我们在这个版本中也去除了过去对镜像原地升级的imageID限制,即支持相同imageID的两个镜像替换升级。

具体使用方式请参考文档

2. 配置跨命名空间分发

Author: @veophi

在对 Secret、ConfigMap 等 namespace-scoped 资源进行跨 namespace 分发及同步的场景中,原生 kubernetes 目前只支持用户 one-by-one 地进行手动分发与同步,十分地不方便。

典型的案例有:

  • 当用户需要使用 SidecarSet 的 imagePullSecrets 能力时,要先重复地在相关 namespaces 中创建对应的 Secret,并且需要确保这些 Secret 配置的正确性和一致性。
  • 当用户想要采用 ConfigMap 来配置一些通用的环境变量时,往往需要在多个 namespaces 做 ConfigMap 的下发,并且后续的修改往往也要求多 namespaces 之间保持同步。

因此,面对这些需要跨 namespaces 进行资源分发和多次同步的场景,我们期望一种更便捷的分发和同步工具来自动化地去做这件事,为此我们设计并实现了一个新的CRD --- ResourceDistribution

ResourceDistribution 目前支持 SecretConfigMap 两类资源的分发和同步。

apiVersion: apps.kruise.io/v1alpha1
kind: ResourceDistribution
metadata:
name: sample
spec:
resource:
apiVersion: v1
kind: ConfigMap
metadata:
name: game-demo
data:
...
targets:
namespaceLabelSelector:
...
# or includedNamespaces, excludedNamespaces

如上述 YAML 所示,ResourceDistribution是一类 cluster-scoped 的 CRD,其主要由 resourcetargets 两个字段构成,其中 resource 字段用于描述用户所要分发的资源,targets 字段用于描述用户所要分发的目标命名空间。

具体使用方式请参考文档

3. 容器启动顺序控制

Author: @Concurrensee

对于 Kubernetes 的一个 Pod,其中的多个容器可能存在依赖关系,比如 容器B 中应用进程的运行依赖于 容器A 中的应用。因此,多个容器之间存在顺序关系的需求:

  • 容器A 先启动,启动成功后才可以启动 容器B
  • 容器B 先退出,退出完成后才可以停止 容器A

通常来说 Pod 容器的启动和退出顺序是由 Kubelet 管理的。Kubernetes 曾经有一个 KEP 计划在 container 中增加一个 type 字段来标识不同类型容器的启停优先级。但是由于 sig-node 考虑到对现有代码架构的改动太大,目前这个 KEP 已经被拒绝了。

因此,OpenKruise 在 v1.0 中提供了名为 Container Launch Priority 的功能,用于控制一个 Pod 中多个容器的强制启动顺序:

  1. 对于任意一个 Pod 对象,只需要在 annotations 中定义 apps.kruise.io/container-launch-priority: Ordered,则 Kruise 会按照 Pod 中 containers 容器列表的顺序来保证其中容器的串行启动。
  2. 如果要自定义 containers 中多个容器的启动顺序,则在容器 env 中添加 KRUISE_CONTAINER_PRIORITY 环境变量,value 值是范围在 [-2147483647, 2147483647] 的整数。一个容器的 priority 值越大,会保证越先启动。

具体使用方式请参考文档

4. kubectl-kruise 命令行工具

Author: @hantmac

过去 OpenKruise 是通过 kruise-api、client-java 等仓库提供了 Go、Java 等语言的 Kruise API 定义以及客户端封装,可供用户在自己的应用程序中引入使用。但仍然有不少用户在测试环境下需要灵活地用命令行操作 workload 资源。

然而原生 kubectl 工具提供的 rolloutset image 等命令只能适用于原生的 workload 类型,如 Deployment、StatefulSet,并不能识别 OpenKruise 中扩展的 workload 类型。

因此,OpenKruise 最新提供了 kubectl-kruise 命令行工具,它是 kubectl 的标准插件,提供了许多适用于 OpenKruise workload 的功能。

# rollout undo cloneset
$ kubectl kruise rollout undo cloneset/nginx

# rollout status advanced statefulset
$ kubectl kruise rollout status statefulsets.apps.kruise.io/sts-demo

# set image of a cloneset
$ kubectl kruise set image cloneset/nginx busybox=busybox nginx=nginx:1.9.1

具体使用方式请参考文档

5. 其余部分功能改进与优化

CloneSet:

  • 通过 scaleStrategy.maxUnavailable 策略支持流式扩容
  • Stable revision 判断逻辑变化,当所有 Pod 版本与 updateRevision 一致时则标记为 currentRevision

WorkloadSpread:

  • 支持接管存量 Pod 到匹配的 subset 分组中
  • 优化 webhook 在 Pod 注入时的更新与重试逻辑

Advanced DaemonSet:

  • 支持对 Daemon Pod 做原地升级
  • 引入 progressive annotation 来选择是否按 partition 限制 Pod 创建

SidecarSet:

  • 解决 SidecarSet 过滤屏蔽 inactive Pod
  • transferenv 中新增 SourceContainerNameFromEnvNames 字段,来解决 container name 不一致与大量 env 情况下的冗余问题

PodUnavailableBudget:

  • 新增 “跳过保护” 标识
  • PodUnavailableBudget controller 关注 workload 工作负载的 replicas 变化

NodeImage:

  • 加入 --nodeimage-creation-delay 参数,并默认等待新增 Node ready 一段时间后同步创建 NodeImage

UnitedDeployment:

  • 解决 NodeSelectorTerms 为 nil 情况下 Pod NodeSelectorTerms 长度为 0 的问题

Other optimization:

  • kruise-daemon 采用 protobuf 协议操作 Pod 资源
  • 暴露 cache resync 为命令行参数,并在 chart 中设置默认值为 0
  • 解决 certs 更新时的 http checker 刷新问题
  • 去除对 forked controller-tools 的依赖,改为使用原生 controller-tools 配合 markers 注解

社区参与

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

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

· 阅读需要 1 分钟
GuangLei Cao

背景

Workload分布在不同zone,不同的硬件类型,甚至是不同的集群和云厂商已经是一个非常普遍的需求。过去一般只能将一个应用拆分为多个 workload(比如 Deployment)来部署,由SRE团队手工管理或者对PaaS 层深度定制,来支持对一个应用多个 workload 的精细化管理。

进一步来说,在应用部署的场景下有着多种多样的拓扑打散以及弹性的诉求。其中最常见就是按某种或多种拓扑维度打散,比如:

  • 应用部署需要按 node 维度打散,避免堆叠(提高容灾能力)。
  • 应用部署需要按 AZ(available zone)维度打散(提高容灾能力)。
  • 按 zone 打散时,需要指定在不同 zone 中部署的比例数。

随着云原生在国内外的迅速普及落地,应用对于弹性的需求也越来越多。各公有云厂商陆续推出了Serverless容器服务来支撑弹性部署场景,如阿里云的弹性容器服务ECI,AWS的Fragate容器服务等。以ECI为例,ECI可以通过Virtual Kubelet对接Kubernetes系统,给予Pod一定的配置就可以调度到virtual-node背后的ECI集群。总结一些常见的弹性诉求,比如:

  • 应用优先部署到自有集群,资源不足时再部署到弹性集群。缩容时,优先从弹性节点缩容以节省成本。
  • 用户自己规划基础节点池和弹性节点池。应用部署时需要固定数量或比例的 Pod 部署在基础节点池,其余的都扩到弹性节点池。

针对这些需求,OpenKruise在 v0.10.0 版本中新增了 WorkloadSpread 特性。目前它支持配合 Deployment、ReplicaSet、CloneSet 这些 workload ,来管理它们下属 Pod 的分区部署与弹性伸缩。下文会深入介绍WorkloadSpread的应用场景和实现原理,帮助用户更好的了解该特性。


WorkloadSpread 介绍

详细细节可以参考官方文档

简而言之,WorkloadSpread能够将workload所属的Pod按一定规则分布到不同类型的Node节点上,能够同时满足上述的打散与弹性场景。


现有方案对比

简单对比一些社区已有的方案。

Pod Topology Spread Constrains

Pod topology spread constraints 是Kubernetes社区提供的方案,可以定义按 topology key 的水平打散。用户在定义完后,调度器会依据配置选择符合分布条件的node。

由于PodTopologySpread更多的是均匀打散,无法支持自定义的分区数量以及比例配置,且缩容时会破坏分布。WorkloadSpread可以自定义各个分区的数量,并且管理着缩容的顺序。因此在一些场景下可以避免PodTopologySpread的不足。

UnitedDeployment

UnitedDeployment 是Kruise社区提供的方案,通过创建和管理多个 workload 管理多个区域下的 Pod。

UnitedDeployment非常好的支持了打散与弹性的需求,不过它是一个全新的workload,用户的使用和迁移成本会比较高。而WorkloadSpread是一种轻量化的方案,只需要简单的配置并关联到workload即可。


应用场景

下面我会列举一些WorkloadSpread的应用场景,给出对应的配置,帮助大家快速了解WorkloadSpread的能力。

1. 基础节点池至多部署100个副本,剩余的部署到弹性节点池。

case-1

subsets:
- name: subset-normal
maxReplicas: 100
requiredNodeSelectorTerm:
matchExpressions:
- key: app.deploy/zone
operator: In
values:
- normal
- name: subset-elastic #副本数量不限
requiredNodeSelectorTerm:
matchExpressions:
- key: app.deploy/zone
operator: In
values:
- elastic

当workload少于100副本时,全部部署到normal节点池,超过100个部署到elastic节点池。缩容时会优先删除elastic节点上的Pod。

由于WorkloadSpread不侵入workload,只是限制住了workload的分布,我们还可以通过结合HPA根据资源负载动态调整副本数,这样当业务高峰时会自动调度到elastic节点上去,业务低峰时会优先释放elastic节点池上的资源。

2. 优先部署到基础节点池,资源不足再部署到弹性资源池。

case-2

scheduleStrategy:
type: Adaptive
adaptive:
rescheduleCriticalSeconds: 30
disableSimulationSchedule: false
subsets:
- name: subset-normal #副本数量不限
requiredNodeSelectorTerm:
matchExpressions:
- key: app.deploy/zone
operator: In
values:
- normal
- name: subset-elastic #副本数量不限
requiredNodeSelectorTerm:
matchExpressions:
- key: app.deploy/zone
operator: In
values:
- elastic

两个subset都没有副本数量限制,且启用Adptive调度策略的模拟调度和Reschedule能力。部署效果是优先部署到normal节点池,normal资源不足时,webhook会通过模拟调度选择elastic节点。当normal节点池中的Pod处于pending状态超过30s阈值, WorkloadSpread controller会删除该Pod以触发重建,新的Pod会被调度到elastic节点池。缩容时还是优先缩容elastic节点上的Pod,为用户节省成本。

3. 打散到3个zone,比例分别为1:1:3

case-3

subsets:
- name: subset-a
maxReplicas: 20%
requiredNodeSelectorTerm:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-a
- name: subset-b
maxReplicas: 20%
requiredNodeSelectorTerm:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-b
- name: subset-c
maxReplicas: 60%
requiredNodeSelectorTerm:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-c

按照不同zone的实际情况,将workload按照1:1:3的比例打散。WorkloadSpread会确保workload扩缩容时按照定义的比例分布。

4. workload在不同CPU Arch上配置不同的资源配额

workload分布的Node可能有不同的硬件配置,CPU架构等,这就可能需要为不同的subset分别制定Pod配置。这些配置可以是label和annotation等元数据也可以是Pod内部容器的资源配额,环境变量等。

case-4

subsets:
- name: subset-x86-arch
# maxReplicas...
# requiredNodeSelectorTerm...
patch:
metadata:
labels:
resource.cpu/arch: x86
spec:
containers:
- name: main
resources:
limits:
cpu: "500m"
memory: "800Mi"
- name: subset-arm-arch
# maxReplicas...
# requiredNodeSelectorTerm...
patch:
metadata:
labels:
resource.cpu/arch: arm
spec:
containers:
- name: main
resources:
limits:
cpu: "300m"
memory: "600Mi"

从上面的样例中我们为两个subset的Pod分别patch了不同的label, container resources,方便我们对Pod做更精细化的管理。当workload的Pod分布在不同的CPU架构的节点上,配置不同的资源配额以更好的利用硬件资源。


实现原理

WorkloadSpread是一个纯旁路的弹性/拓扑管控方案。用户只需要针对自己的 Deployment/CloneSet/Job 对象创建对应的 WorkloadSpread 即可,无需对 workload 做改动,也不会对用户使用 workload 造成额外成本。

arch

1. subset优先级与副本数量控制

WorkloadSpread 中定义了多个 subset,每个subset代表一个逻辑域。用户可以自由的根据节点配置,硬件类型,zone等来划分subset。特别的,我们规定了subset的优先级:

  1. 按定义从前往后的顺序,优先级从高到低。
  2. 优先级越高,越先扩容;优先级越低,越先缩容。

2. 如何控制缩容优先级?

理论上,WorkloadSpread 这种旁路方案是无法干涉到 workload 控制器里的缩容顺序逻辑的。

不过,这个问题在近期得以解决—— 经过一代代用户的不懈努力(反馈),k8s 从 1.21 版本开始为 ReplicaSet(Deployment)支持了通过设置 controller.kubernetes.io/pod-deletion-cost 这个 annotation 来指定 Pod 的 “删除代价”:deletion-cost 越高的 Pod,删除的优先级越低。

而 Kruise 从 v0.9.0 版本开始,就在 CloneSet 中支持了 deletion-cost 特性。

因此,WorkloadSpread controller通过调整各个 subset 下属 Pod 的 deletion-cost,来控制workload的缩容顺序。

举个例子:对于以下 WorkloadSpread,以及它关联的 CloneSet 有 10 个副本:

  subsets:
- name: subset-a
maxReplicas: 8
- name: subset-b # 副本数量不限

则 deletion-cost 数值以及删除顺序为:

  • 2 个在 subset-b上的 Pod,deletion-cost 为 100(优先缩容)
  • 8 个在 subset-a上的 Pod,deletion-cost 为 200(最后缩容)

然后,如果用户修改了 WorkloadSpread 为:

  subsets:
- name: subset-a
maxReplicas: 5 # 8-3,
- name: subset-b

则 workloadspread controller 会将其中 3 个在 susbet-a 上 Pod 的 deletion-cost值由 200 改为 -100,则:

  • 3 个在 subset-a 上的 Pod,deletion-cost 为 -100(优先缩容)
  • 2 个在 subset-b 上的 Pod,deletion-cost 为 100(其次缩容)
  • 5 个在 subset-a 上的 Pod,deletion-cost 为 200(最后缩容)

这样就能够优先缩容那些超过subset副本限制的Pod了,当然总体还是按照subset定义的顺序从后向前缩容。

3. 数量控制

如何确保 webhook 严格按照 subset 优先级顺序、maxReplicas 数量来注入Pod 规则是 WorkloadSpread 实现层面的重点难题。

3.1 解决并发一致性问题

在 workloadspread的status 中有对应每个 subset 的 status,其中 missingReplicas 字段表示了这个 subset 需要的 Pod 数量,-1 表示没有数量限制(subset 没有配置 maxReplicas)。

spec:
subsets:
- name: subset-a
maxReplicas: 1
- name: subset-b
# ...
status:
subsetStatuses:
- name: subset-a
missingReplicas: 1
- name: subset-b
missingReplicas: -1
# ...

当 webhook 收到 Pod create请求时:

  1. 根据 subsetStatuses 顺序依次找 missingReplicas 大于 0 或为 -1 的 suitable subset。
  2. 找到suitable subset后,如果 missingReplicas 大于 0,则先减 1 并尝试更新 workloadspread status。
  3. 如果更新成功,则将该 subset定义的规则注入到 pod 中。
  4. 如果更新失败,则重新 get 这个 workloadspread以获取最新的status,并回到步骤 1(有一定重试次数限制)。

同样,当 webhook 收到 Pod delete/eviction 请求时,则将 missingReplicas 加 1 并更新。

毫无疑问,我们在使用乐观锁来解决更新冲突。但是仅使用乐观锁是不合适的,因为workload在创建Pod时会并行创建大量的Pod,apiserver会在一瞬间发送很多Pod create请求到webhook,并行处理会产生非常多的冲突。大家都知道,冲突太多就不适合使用乐观锁了,因为它解决冲突的重试成本非常高。为此我们还加入了workloadspread级别的互斥锁,将并行处理限制为串行处理。加入互斥锁还有新的问题,即当前groutine获取锁后,极有可能从infromer中拿的workloadspread不是最新的,还是会冲突。所以groutine在更新完workloadspread之后,先将最新的workloadspread对象缓存起来再释放锁,这样新的groutine获取锁后就可以直接从缓存中拿到最新的workloadspread。当然,多个webhook的情况下还是需要结合乐观锁机制来解决冲突。

3.2 解决数据一致性问题

那么,missingReplicas 数值是否交由 webhook 控制即可呢?答案是不行,因为:

  1. webhook 收到的 Pod create 请求,最终不一定真的能成功(比如 Pod 不合法,或在后续 quota 等校验环节失败了)。
  2. webhook 收到的 Pod delete/eviction 请求,最终也不一定真的能成功(比如后续被 PDB、PUB 等拦截了)。
  3. K8s 里总有种种的可能性,导致 Pod 没有经过 webhook 就结束或没了(比如 phase 进入 Succeeded/Failed,或是 etcd 数据丢了等等)。
  4. 同时,这也不符合面向终态的设计理念。

因此,workloadspread status 是由 webhook 与 controller 协作来控制的:

  • webhook 在 Pod create/delete/eviction 请求链路拦截,修改 missingReplicas 数值。
  • 同时 controller 的 reconcile 中也会拿到当前 workload 下的所有 Pod,根据 subset 分类,并将 missingReplicas 更新为当前实际缺少的数量。
  • 从上面的分析中,controller从informer中获取Pod很可能存在延时,所以我们还在status中增加了creatingPods map, webook注入的时候会记录key为pod.name, value为时间戳的一条entry到map,controller再结合map维护真实的missingReplicas。同理还有一个deletingPods map来记录Pod的delete/eviction事件。

4. 自适应调度能力

在 WorkloadSpread 中支持配置 scheduleStrategy。默认情况下,type 为 Fixed,即固定按照各个 subset 的前后顺序、maxReplicas 限制来将 Pod 调度到对应的 subset 中。 但真实的场景下,很多时候 subset 分区或拓扑的资源,不一定能完全满足 maxReplicas 数量。用户需要按照实际的资源情况,来为 Pod 选择有资源的分区扩容。这就需要用 Adaptive 这种自适应的调度分配。

WorkloadSpread 提供的 Adaptive 能力,逻辑上分为两种:

  1. SimulationSchedule:在 Kruise webhook 中根据 informer 里已有的 nodes/pods 数据,组装出调度账本,对 Pod 进行模拟调度。即通过 nodeSelector/affinity、tolerations、以及基本的 resources 资源,做一次简单的过滤。(对于 vk 这种节点不太适用)
  2. Reschedule:在将 Pod 调度到一个 subset 后,如果调度失败超过 rescheduleCriticalSeconds 时间,则将该 subset 暂时标记为 unschedulable,并删除 Pod 触发重建。默认情况下,unschedulable 会保留 5min,即在 5min 内的 Pod 创建会跳过这个 subset。

小结

WorkloadSpread通过结合一些kubernetes现有的特性以一种旁路的形式赋予workload弹性部署与多域部署的能力。我们希望用户通过使用WorkloadSpread降低workload部署复杂度,利用其弹性伸缩能力切实降低成本。 目前阿里云内部正在积极的落地,落地过程中的调整会及时反馈社区。未来WorkloadSpread还有一些新能力计划,比如让WorkloadSpread支持workload的存量Pod接管,支持批量的workload约束,甚至是跨过workload层级使用label来匹配Pod。其中一些能力需要实际考量社区用户的需求场景。希望大家多多参与到Kruise社区,多提issue和pr,帮助用户解决更多云原生部署方面的难题,构建一个更好的社区。


参考文献

· 阅读需要 1 分钟
Siyu Wang

本文将带你一览 v0.10.0 的新变化,其中新增的 WorkloadSpread、PodUnavailableBudget 等大颗粒特性后续还将有专文详细介绍其设计实现原理。

WorkloadSpread:旁路的应用弹性拓扑管理能力

在应用部署运维的场景下,有着多种多样的拓扑打散以及弹性的诉求。其中最常见、最基本的,就是按某种或几种拓扑水平打散,比如:

  • 应用部署需要按 node 维度打散,避免堆叠(提高容灾能力)
  • 应用部署需要按 AZ(available zone)维度打散(提高容灾能力)

这些基本的诉求,通过 Kubernetes 原生提供的 pod affinity、topology spread constraints 等能力目前都能够满足了。但在实际的生产场景下,还有着太多更加复杂的分区与弹性需求,以下举一些实际的例子:

  • 按 zone 打散时,需要指定在不同 zone 中部署的比例数,比如某个应用在 zone a、b、c 中部署的 Pod 数量比例为 1 : 1 : 2 等(由于一些现实的原因比如该应用在多个 zone 中的流量不均衡等)
  • 存在多个 zone 或不同机型的拓扑,应用扩容时,优先部署到某个 zone 或机型上,当资源不足时再部署到另一个 zone 或机型上(往后以此类推);应用缩容时,要按反向顺序,优先缩容后面 zone 或机型上的 Pod(往前以此类推)
  • 存在多个基础的节点池和弹性的节点池,应用部署时需要固定数量或比例的 Pod 部署在基础节点池,其余的都扩到弹性节点池

对于这些例子,过去一般只能将一个应用拆分为多个 Workload(比如 Deployment)来部署,才能解决应用在不同拓扑下采用不同比例数量、扩缩容优先级、资源感知、弹性选择等场景的基本问题,但还是需要 PaaS 层深度定制化,来支持对一个应用多个 Workload 的精细化管理。

针对这些问题,在 Kruise v0.10.0 版本中新增了 WorkloadSpread 资源,目前它支持配合 Deployment、ReplicaSet、CloneSet 这些 Workload 类型,来管理它们下属 Pod 的分区与弹性拓扑。 以下是一个简化的例子:

apiVersion: apps.kruise.io/v1alpha1
kind: WorkloadSpread
metadata:
name: workloadspread-demo
spec:
targetRef:
apiVersion: apps/v1 | apps.kruise.io/v1alpha1
kind: Deployment | CloneSet
name: workload-xxx
subsets:
- name: subset-a
requiredNodeSelectorTerm:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-a
maxReplicas: 10 | 30%
- name: subset-b
requiredNodeSelectorTerm:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-b

创建这个 WorkloadSpread 可以通过 targetRef 关联到一个 Workload 对象上,然后这个 Workload 在扩容 pod 的过程中,Pod 会被 Kruise 按上述策略注入对应的拓扑规则。这是一种旁路的注入和管理方式,本身不会干涉 Workload 对 Pod 的扩缩容、发布管理。

注意:WorkloadSpread 对 Pod 的缩容的优先级控制是通过 Pod Deletion Cost 来实现的:

  • 如果 Workload 类型是 CloneSet,则已经支持了这个 feature,可以实现缩容优先级
  • 如果 Workload 类型是 Deployment/ReplicaSet,则要求 Kubernetes version >= 1.21,且在 1.21 中要在 kube-controller-manager 上开启 PodDeletionCost 这个 feature-gate

使用 WorkloadSpread 功能,需要在 安装/升级 Kruise v0.10.0 的时候打开 WorkloadSpread 这个 feature-gate。

PodUnavailableBudget:应用可用性防护

在诸多 Voluntary Disruption 场景中 Kubernetes 原生提供的 Pod Disruption Budget(PDB) 通过限制同时中断的 Pod 数量,来保证应用的高可用性。

但还有很多场景中,即便有 PDB 防护依然将会导致业务中断、服务降级,比如:

  • 应用 owner 通过 Deployment 正在进行版本升级,与此同时集群管理员由于机器资源利用率过低正在进行 node 缩容
  • 中间件团队利用 SidecarSet 正在原地升级集群中的sidecar版本(例如:ServiceMesh envoy),同时HPA正在对同一批应用进行缩容
  • 应用 owner 和中间件团队利用 CloneSet、SidecarSet 原地升级的能力,正在对同一批 Pod 进行升级

这其实很好理解 -- PDB 只能防控通过 Eviction API 来触发的 Pod 驱逐(例如 kubectl drain驱逐node上面的所有Pod),但是对于 Pod 删除、原地升级 等很多操作是无法防护的。

在 Kruise v0.10.0 版本中新增的 PodUnavailableBudget(PUB)功能,则是对原生 PDB 的强化扩展。它包含了 PDB 自身的能力,并在此基础上增加了对更多 Voluntary Disruption 操作的防护,包括但不限于 Pod 删除、原地升级 等。

apiVersion: apps.kruise.io/v1alpha1
kind: PodUnavailableBudget
metadata:
name: web-server-pub
namespace: web
spec:
targetRef:
apiVersion: apps/v1 | apps.kruise.io/v1alpha1
kind: Deployment | CloneSet | StatefulSet | ...
name: web-server
# selector 与 targetRef 二选一配置
# selector:
# matchLabels:
# app: web-server
# 保证的最大不可用数量
maxUnavailable: 60%
# 保证的最小可用数量
# minAvailable: 40%

使用 PodUnavailableBudget 功能,需要在 安装/升级 Kruise v0.10.0 的时候打开feature-gate(两个可以选择打开一个,也可以都打开):

  • PodUnavailableBudgetDeleteGate:拦截防护 Pod 删除、驱逐 等操作
  • PodUnavailableBudgetUpdateGate:拦截防护 Pod 原地升级 等更新操作

CloneSet 支持按拓扑规则缩容

在 CloneSet 缩容(调小 replicas 数量)的时候,选择哪些 Pod 删除是有一套固定算法排序的:

  1. 未调度 < 已调度
  2. PodPending < PodUnknown < PodRunning
  3. Not ready < ready
  4. 较小 pod-deletion cost < 较大 pod-deletion cost
  5. 较大打散权重 < 较小
  6. 处于 Ready 时间较短 < 较长
  7. 容器重启次数较多 < 较少
  8. 创建时间较短 < 较长

其中,“4” 是在 Kruise v0.9.0 中开始提供的特性,用于支持用户指定删除顺序(WorkloadSpread 就是利用这个功能实现缩容优先级);而 “5” 则是当前 v0.10.0 提供的特性,即在缩容的时候会参考应用的拓扑打散来排序

  • 如果应用配置了 topology spread constraints,则 CloneSet 缩容时会按照其中的 topology 维度打散来选择 Pod 删除(比如尽量打平多个 zone 上部署 Pod 的数量)
  • 如果应用没有配置 topology spread constraints,则默认情况下 CloneSet 缩容时会按照 node 节点维度打散来选择 Pod 删除(尽量减少同 node 上的堆叠数量)

Advanced StatefulSet 支持流式扩容

为了避免在一个新 Advanced StatefulSet 创建后有大量失败的 pod 被创建出来,从 Kruise v0.10.0 版本开始引入了在 scale strategy 中的 maxUnavailable 策略:

apiVersion: apps.kruise.io/v1beta1
kind: StatefulSet
spec:
# ...
replicas: 100
scaleStrategy:
maxUnavailable: 10% # percentage or absolute number

当这个字段被设置之后,Advanced StatefulSet 会保证创建 pod 之后不可用 pod 数量不超过这个限制值。 比如说,上面这个 StatefulSet 一开始只会一次性创建 10 个 pod。在此之后,每当一个 pod 变为 running、ready 状态后,才会再创建一个新 pod 出来。

注意:这个功能只允许在 podManagementPolicy 是 Parallel 的 StatefulSet 中使用。

More

更多版本变化,请参考 release pageChangeLog

· 阅读需要 1 分钟
Mingshan Zhao

OpenKruise 是阿里云开源的云原生应用自动化管理套件,也是当前托管在 Cloud Native Computing Foundation (CNCF) 下的Sandbox项目。它来自阿里巴巴多年来容器化、云原生的技术沉淀,是阿里内部生产环境大规模应用的基于Kubernetes之上的标准扩展组件,也是紧贴上游社区标准、适应互联网规模化场景的技术理念与最佳实践。

OpenKruise在2021.5.20发布了最新的v0.9.0版本,其中sidecarSet基于上一个版本扩展了特别针对Service Mesh场景的支持。

背景 - 如何独立升级Mesh容器

SidecarSet 是 Kruise 提供的独立管理 sidecar 容器的 workload。用户通过 SidecarSet 能够便利的完成对Sidecar容器的自动注入和独立升级。

默认情况下,sidecar 的独立升级顺序是先停止旧版本的容器,然后再创建新版本的容器。这种方式尤其适合不影响Pod服务可用性的sidecar容器,例如日志收集 agent,但是对于很多代理或运行时的 sidecar 容器,如 Istio Envoy,这种升级方法就有问题了。Envoy 作为 Pod 中的一个Proxy容器代理了所有的流量,这种场景下如果直接重启升级,Pod服务的可用性必然会受到影响,因此需要考虑应用自身的发布和容量情况,无法完全独立于应用做sidecar的发布。

how update mesh sidecar

阿里巴巴集团内部拥有上万的Pod都是基于Service Mesh来实现相互间的通信,由于mesh容器升级会导致业务pod的不可用,因而mesh容器的升级将会极大阻碍Service Mesh的迭代。针对这种场景,我们同集团内部的Service Mesh团队一起合作实现了mesh容器的热升级能力。本文将重点介绍在实现mesh容器热升级能力的过程中SidecarSet是扮演了怎样的重要角色。

SidecarSet助力Mesh容器无损热升级

Mesh容器不能像日志采集类容器直接原地升级,其原因在于:mesh容器必须要不间断地对外提供服务,而独立升级方式会导致mesh服务存在一段不可用时间。虽然社区中已有一些知名的mesh服务如Envoy、Mosn等默认能够提供平滑升级的能力,但是这些升级方式无法与云原生进行恰当地结合,且kubernetes本身也缺乏对此类sidecar容器的升级方案。

OpenKruise SidecarSet为此类mesh容器提供了sidecar热升级机制,能够通过云原生的方式助力Mesh容器实现无损热升级。

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: hotupgrade-sidecarset
spec:
selector:
matchLabels:
app: hotupgrade
containers:
- name: sidecar
image: openkruise/hotupgrade-sample:sidecarv1
imagePullPolicy: Always
lifecycle:
postStart:
exec:
command:
- /bin/sh
- /migrate.sh
upgradeStrategy:
upgradeType: HotUpgrade
hotUpgradeEmptyImage: openkruise/hotupgrade-sample:empty
  • upgradeType: HotUpgrade代表该sidecar容器的类型是hot upgrade,即热升级方案
  • hotUpgradeEmptyImage: 当热升级sidecar容器时,业务须要提供一个empty容器用于热升级过程中的容器切换。Empty容器同sidecar容器具有相同的配置(镜像地址除外),例如command, lifecycle, probe等。

SidecarSet热升级机制主要包含注入热升级Sidecar容器和Mesh容器平滑升级两个过程。

注入热升级Sidecar容器

针对热升级类型的Sidecar容器,在Pod创建时SidecarSet Webhook将会注入两个容器:

  • {sidecar.name}-1: 如下图所示 envoy-1,这个容器代表正在实际工作的sidecar容器,例如:envoy:1.16.0
  • {sidecar.name}-2: 如下图所示 envoy-2,这个容器是业务提供的hotUpgradeEmptyImage容器,例如:empty:1.0

inject sidecar

上述Empty容器在Mesh容器运行过程中,并没有做任何实际的工作。

Mesh容器平滑升级

热升级流程主要分为一下三个步骤:

  1. Upgrade: 将Empty容器替换为最新版本的sidecar容器,例如:envoy-2.Image = envoy:1.17.0
  2. Migration: 执行sidecar容器的PostStartHook脚本,完成mesh服务的平滑升级
  3. Reset: mesh服务平滑升级后,将老版本sidecar容器替换为Empty容器,例如:envoy-1.Image = empty:1.0

update sidecar

仅需上述三个步骤即可完成热升级中的全部流程,若对Pod执行多次热升级,则重复执行上述三个步骤即可。

Migration核心逻辑

SidecarSet热升级机制不仅完成了mesh容器的切换,并且提供了新老版本的协调机制(PostStartHook),但是至此还只是万里长征的第一步,Mesh容器同时还需要提供PostSartHook脚本来完成mesh服务自身的平滑升级(上述Migration过程),如:Envoy热重启、Mosn无损重启。

mesh容器一般都是通过监听固定端口来对外提供服务,此类mesh容器的migration过程可以概括为:通过UDS传递ListenFD和停止Accpet、开始排水。针对不支持热重启的mesh容器可以参考此过程完成改造,逻辑图如下:

migration

Migration Demo

不同mesh容器对外提供的服务以及内部实现逻辑各有差异,进而具体的Migration也有所不同,上述逻辑只是对其中一些要点做了一些总结,希望能对有需要的各位有所裨益,同时在github上面我们也提供了一个热升级Migration Demo以供参考,下面将对其中的一些关键代码进行介绍。

  1. 协商机制 Mesh容器启动逻辑首先就需要判断第一次启动还是热升级平滑迁移过程,为了减少mesh容器沟通成本,Kruise在两个sidecar容器中注入了两个环境变量SIDECARSET_VERSION和SIDECARSET_VERSION_ALT,通过判断两个环境变量的值来判断是否是热升级过程以及当前sidecar容器是新版本还是老版本。
// return two parameters:
// 1. (bool) indicates whether it is hot upgrade process
// 2. (bool ) when isHotUpgrading=true, the current sidecar is newer or older
func isHotUpgradeProcess() (bool, bool) {
// Version of the current sidecar container
version := os.Getenv("SIDECARSET_VERSION")
// Version of the peer sidecar container
versionAlt := os.Getenv("SIDECARSET_VERSION_ALT")
// If the version of the peer sidecar container is "0", hot upgrade is not underway
if versionAlt == "0" {
return false, false
}
// Hot upgrade is underway
versionInt, _ := strconv.Atoi(version)
versionAltInt, _ := strconv.Atoi(versionAlt)
// version is of int type and monotonically increases, which means the version value of the new-version container will be greater
return true, versionInt > versionAltInt
}
  1. ListenFD迁移 通过Unix Domain Socket实现ListenFD在不同容器间的迁移,此步同样也是热升级中非常关键的一步,代码示例如下:
  // For code conciseness, all failures will not be captured

/* The old sidecar migrates ListenFD to the new sidecar through Unix Domain Socket */
// tcpLn *net.TCPListener
f, _ := tcpLn.File()
fdnum := f.Fd()
data := syscall.UnixRights(int(fdnum))
// Establish a connection with the new sidecar container through Unix Domain Socket
raddr, _ := net.ResolveUnixAddr("unix", "/dev/shm/migrate.sock")
uds, _ := net.DialUnix("unix", nil, raddr)
// Use UDS to send ListenFD to the new sidecar container
uds.WriteMsgUnix(nil, data, nil)
// Stop receiving new requests and start the drainage phase, for example, http2 GOAWAY
tcpLn.Close()

/* The new sidecar receives ListenFD and starts to provide external services */
// Listen to UDS
addr, _ := net.ResolveUnixAddr("unix", "/dev/shm/migrate.sock")
unixLn, _ := net.ListenUnix("unix", addr)
conn, _ := unixLn.AcceptUnix()
buf := make([]byte, 32)
oob := make([]byte, 32)
// Receive ListenFD
_, oobn, _, _, _ := conn.ReadMsgUnix(buf, oob)
scms, _ := syscall.ParseSocketControlMessage(oob[:oobn])
if len(scms) > 0 {
// Parse FD and convert to *net.TCPListener
fds, _ := syscall.ParseUnixRights(&(scms[0]))
f := os.NewFile(uintptr(fds[0]), "")
ln, _ := net.FileListener(f)
tcpLn, _ := ln.(*net.TCPListener)
// Start to provide external services based on the received Listener. The http service is used as an example
http.Serve(tcpLn, serveMux)
}

已知Mesh容器热升级案例

阿里云服务网格(Alibaba Cloud Service Mesh,简称ASM)提供了一个全托管式的服务网格平台,兼容社区Istio开源服务网格。当前,基于OpenKruise SidecarSet的热升级能力,ASM实现了数据平面Sidecar热升级能力(Beta),用户可以在应用无感的情况下完成服务网格的数据平面版本升级,正式版也将于近期上线。除热升级能力外,ASM还支持配置诊断、操作审计、访问日志、监控、服务注册接入等能力,全方位提升服务网格使用体验,欢迎您前往试用。

总结

云原生中mesh容器的热升级一直都是迫切却又棘手的问题,本文中的方案也只是阿里巴巴集团在此问题上的一次探索,在反馈社区的同时也希望能够抛砖引玉,引发各位对此中场景的思考。同时,我们也欢迎更多的同学参与到 OpenKruise 社区来,共同建设一个场景更加丰富、完善的 K8s 应用管理、交付扩展能力,能够面向更加规模化、复杂化、极致性能的场景。

· 阅读需要 1 分钟
Siyu Wang

OpenKruise 在 2021.5.20 发布了最新的 v0.9.0 版本,新增了 Pod 容器重启、资源级联删除防护等重磅功能,本文以下对新版本做整体的概览介绍。

Pod 容器重启/重建

“重启” 是一个很朴素的需求,即使日常运维的诉求,也是技术领域较为常见的 “恢复手段”。而在原生的 Kubernetes 中,并没有提供任何对容器粒度的操作能力,Pod 作为最小操作单元也只有创建、删除两种操作方式。

有的同学可能会问,在云原生时代,为什么用户还要关注容器重启这种运维操作呢?在理想的 serverless 模式下,业务只需要关心服务自身就好吧?

这来自于云原生架构和过去传统基础基础设施的差异性。在传统的物理机、虚拟机时代,一台机器上往往会部署和运行多个应用的实例,并且机器和应用的生命周期是不同的;在这种情况下,应用实例的重启可能仅仅是一条 systemctl 或 supervisor 之类的指令,而无需将整个机器重启。然而,在容器与云原生模式下,应用的生命周期是和 Pod 容器绑定的;即常规情况下,一个容器只运行一个应用进程,一个 Pod 也只提供一个应用实例的服务。

基于上述的限制,目前原生 Kubernetes 之下是没有 API 来为上层业务提供容器(应用)重启能力的。而 Kruise v0.9.0 版本提供了一种单 Pod 维度的容器重启能力,兼容 1.16 及以上版本的标准 Kubernetes 集群。在安装或升级 Kruise 之后,只需要创建 ContainerRecreateRequest(简称 CRR) 对象来指定重启,最简单的 YAML 如下:

apiVersion: apps.kruise.io/v1alpha1
kind: ContainerRecreateRequest
metadata:
namespace: pod-namespace
name: xxx
spec:
podName: pod-name
containers:
- name: app
- name: sidecar

其中,namespace 需要与要操作的 Pod 在同一个命名空间,name 可自选。spec 中 podName 是 Pod 名字,containers 列表则可以指定 Pod 中一个或多个容器名来执行重启。

除了上述必选字段外,CRR 还提供了多种可选的重启策略:

spec:
# ...
strategy:
failurePolicy: Fail
orderedRecreate: false
terminationGracePeriodSeconds: 30
unreadyGracePeriodSeconds: 3
minStartedSeconds: 10
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 1800
  • failurePolicy: Fail 或 Ignore,默认 Fail;表示一旦有某个容器停止或重建失败,CRR 立即结束
  • orderedRecreate: 默认 false;true 表示列表有多个容器时,等前一个容器重建完成了,再开始重建下一个
  • terminationGracePeriodSeconds: 等待容器优雅退出的时间,不填默认用 Pod 中定义的时间
  • unreadyGracePeriodSeconds: 在重建之前先把 Pod 设为 not ready,并等待这段时间后再开始执行重建
    • 注:该功能依赖于 KruisePodReadinessGate 这个 feature-gate 要打开,后者会在每个 Pod 创建的时候注入一个 readinessGate。 否则,默认只会给 Kruise workload 创建的 Pod 注入 readinessGate,也就是说只有这些 Pod 才能在 CRR 重建时使用 unreadyGracePeriodSeconds
  • minStartedSeconds: 重建后新容器至少保持运行这段时间,才认为该容器重建成功
  • activeDeadlineSeconds: 如果 CRR 执行超过这个时间,则直接标记为结束(未完成的容器标记为失败)
  • ttlSecondsAfterFinished: CRR 结束后,过了这段时间自动被删除掉

实现原理:当用户创建了 CRR 后,经过了 kruise-manager 中心端的初步处理,会被 Pod 所在节点上的 kruise-daemon 收到并开始执行。执行的过程如下:

  1. 如果 Pod 容器定义了 preStop,kruise-daemon 会先走 CRI 运行时 exec 到容器中执行 preStop
  2. 如果没有 preStop 或执行完成,kruise-daemon 调用 CRI 接口将容器停止
  3. kubelet 感知到容器退出,则会新建一个 “序号” 递增的新容器,并开始启动(以及执行 postStart)
  4. kruise-daemon 感知到新容器启动成功,上报 CRR 重启完成

ContainerRecreateRequest

上述的容器 “序号” 其实就对应了 Pod status 中 kubelet 上报的 restartCount。因此,在容器重启后会看到 Pod 的 restartCount 增加。另外,因为容器发生了重建,之前临时写到旧容器 rootfs 中的文件会丢失,但是 volume mount 挂载卷中的数据仍然存在。

级联删除防护

Kubernetes 的面向终态自动化是一把 “双刃剑”,它既为应用带来了声明式的部署能力,同时也潜在地会将一些误操作行为被终态化放大。例如它的 “级联删除” 机制,即正常情况(非 orphan 删除)下一旦父类资源被删除,则所有子类资源都会被关联删除:

  1. 删除一个 CRD,其所有对应的 CR 都被清理掉
  2. 删除一个 namespace,这个命名空间下包括 Pod 在内所有资源都被一起删除
  3. 删除一个 workload(Deployment/StatefulSet/...),则下属所有 Pod 被删除

类似这种 “级联删除” 带来的故障,我们已经听到不少社区 K8s 用户和开发者带来的抱怨。对于任何一家企业来说,其生产环境发生这种规模误删除都是不可承受之痛。

因此,在 Kruise v0.9.0 版本中,我们建立了防级联删除能力,期望能为更多的用户带来稳定性保障。在当前版本中如果需要使用该功能,则在安装或升级 Kruise 的时候需要显式打开 ResourcesDeletionProtection 这个 feature-gate。

对于需要防护删除的资源对象,用户可以给其打上 policy.kruise.io/delete-protection 标签,value 可以有两种:

  • Always: 表示这个对象禁止被删除,除非上述 label 被去掉
  • Cascading:这个对象如果还有可用的下属资源,则禁止被删除

目前支持的资源类型、以及 cascading 级联关系如下:

KindGroupVersionCascading judgement
Namespacecorev1whether there is active Pods in this namespace
CustomResourceDefinitionapiextensions.k8s.iov1beta1, v1whether there is existing CRs of this CRD
Deploymentappsv1whether the replicas is 0
StatefulSetappsv1whether the replicas is 0
ReplicaSetappsv1whether the replicas is 0
CloneSetapps.kruise.iov1alpha1whether the replicas is 0
StatefulSetapps.kruise.iov1alpha1, v1beta1whether the replicas is 0
UnitedDeploymentapps.kruise.iov1alpha1whether the replicas is 0

CloneSet 新增功能

删除优先级

controller.kubernetes.io/pod-deletion-cost 是从 Kubernetes 1.21 版本后加入的 annotation,ReplicaSet 在缩容时会参考这个 cost 数值来排序。 CloneSet 从 Kruise v0.9.0 版本后也同样支持了这个功能。

用户可以把这个 annotation 配置到 pod 上,它的 value 数值是 int 类型,表示这个 pod 相较于同个 CloneSet 下其他 pod 的 "删除代价",代价越小的 pod 删除优先级相对越高。 没有设置这个 annotation 的 pod 默认 deletion cost 是 0。

注意这个删除顺序并不是强制保证的,因为真实的 pod 的删除类似于下述顺序:

  1. 未调度 < 已调度
  2. PodPending < PodUnknown < PodRunning
  3. Not ready < ready
  4. 较小 pod-deletion cost < 较大 pod-deletion cost
  5. 处于 Ready 时间较短 < 较长
  6. 容器重启次数较多 < 较少
  7. 创建时间较短 < 较长

配合原地升级的镜像预热

当使用 CloneSet 做应用原地升级时,只会升级容器镜像、而 Pod 不会发生重建。这就保证了 Pod 升级前后所在 node 不会发生变化,从而在原地升级的过程中,如果 CloneSet 提前在所有 Pod 节点上先把新版本镜像拉取好,则在后续的发布批次中 Pod 原地升级速度会得到大幅度提高。

在当前版本中如果需要使用该功能,则在安装或升级 Kruise 的时候需要显式打开 PreDownloadImageForInPlaceUpdate 这个 feature-gate。打开后,当用户更新了 CloneSet template 中的镜像、且发布策略支持原地升级,则 CloneSet 会自动为这个新镜像创建 ImagePullJob 对象(OpenKruise 提供的批量镜像预热功能),来提前在 Pod 所在节点上预热新镜像。

默认情况下 CloneSet 给 ImagePullJob 配置的并发度是 1,也就是一个个节点拉镜像。 如果需要调整,你可以在 CloneSet annotation 上设置其镜像预热时的并发度:

apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
metadata:
annotations:
apps.kruise.io/image-predownload-parallelism: "5"

先扩再缩的 Pod 置换方式

在过去版本中,CloneSet 的 maxUnavailablemaxSurge 策略只对应用发布过程生效。而从 Kruise v0.9.0 版本开始,这两个策略同样会对 Pod 指定删除生效。

也就是说,当用户通过 podsToDeleteapps.kruise.io/specified-delete: true 方式(具体见官网文档)来指定一个或多个 Pod 期望删除时,CloneSet 只会在当前不可用 Pod 数量(相对于 replicas 总数)小于 maxUnavailable 的时候才执行删除。同时,如果用户配置了 maxSurge 策略,则 CloneSet 有可能会先创建一个新 Pod、等待新 Pod ready、再删除指定的旧 Pod。

具体采用什么样的置换方式,取决于当时的 maxUnavailable 和实际不可用 Pod 数量。比如:

  • 对于一个 CloneSet maxUnavailable=2, maxSurge=1 且有一个 pod-a 处于不可用状态, 如果你对另一个 pod-b 指定删除, 那么 CloneSet 会立即删除它,然后创建一个新 Pod。
  • 对于一个 CloneSet maxUnavailable=1, maxSurge=1 且有一个 pod-a 处于不可用状态, 如果你对另一个 pod-b 指定删除, 那么 CloneSet 会先新建一个 Pod、等待它 ready,最后再删除 pod-b。
  • 对于一个 CloneSet maxUnavailable=1, maxSurge=1 且有一个 pod-a 处于不可用状态, 如果你对这个 pod-a 指定删除, 那么 CloneSet 会立即删除它,然后创建一个新 Pod。
  • ...

基于 partition 终态的高效回滚

在原生的 workload 中,Deployment 自身发布不支持灰度发布,StatefulSet 有 partition 语义来允许用户控制灰度升级的数量;而 Kruise workload 如 CloneSet、Advanced StatefulSet,也都提供了 partition 来支持灰度分批。

对于 CloneSet,Partition 的语义是 保留旧版本 Pod 的数量或百分比。比如说一个 100 个副本的 CloneSet,在升级镜像时将 partition 数值阶段性改为 80 -> 60 -> 40 -> 20 -> 0,则完成了分 5 批次发布。

但过去,不管是 Deployment、StatefulSet 还是 CloneSet,在发布的过程中如果想要回滚,都必须将 template 信息(镜像)重新改回老版本。后两者在灰度的过程中,将 partition 调小会触发旧版本升级为新版本,但再次 partition 调大则不会处理。

从 v0.9.0 版本开始,CloneSet 的 partition 支持了 “终态回滚” 功能。如果在安装或升级 Kruise 的时候打开了 CloneSetPartitionRollback 这个 feature-gate,则当用户将 partition 调大时,CloneSet 会将对应数量的新版本 Pod 重新回滚到老版本。

这样带来的好处是显而易见的:在灰度发布的过程中,只需要前后调节 partition 数值,就能灵活得控制新旧版本的比例数量。但需要注意的是,CloneSet 所依据的 “新旧版本” 对应的是其 status 中的 updateRevision 和 currentRevision:

  • updateRevision:对应当前 CloneSet 所定义的 template 版本
  • currentRevision:该 CloneSet 前一次全量发布成功的 template 版本

短 hash

默认情况下,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 个字符的,如果超过了,则无法成功创建出 Pod。

在 v0.9.0 版本引入了 CloneSetShortHash 新的 feature-gate。 如果它被打开,CloneSet 只会将 Pod 中的 controller-revision-hash 的值只设置为 hash 值,比如 956df7994,因此 CloneSet 名字的长度不会有任何限制了。(即使启用该功能,CloneSet 仍然会识别和管理过去存量的 revision label 为完整格式的 Pod。)

SidecarSet 新增功能

Sidecar 热升级

SidecarSet 是 Kruise 提供的独立管理 sidecar 容器的 workload。用户可以通过 SidecarSet,来在一定范围的 Pod 中注入和升级指定的 sidecar 容器。

默认情况下,sidecar 的独立原地升级是先停止旧版本的容器,然后创建新版本的容器。这种方式更加适合不影响Pod服务可用性的sidecar容器,比如说日志收集 agent,但是对于很多代理或运行时的 sidecar 容器,例如 Istio Envoy,这种升级方法就有问题了。Envoy 作为 Pod 中的一个代理容器,代理了所有的流量,如果直接重启升级,Pod 服务的可用性会受到影响。如果需要单独升级 envoy sidecar,就需要复杂的 grace 终止和协调机制。所以我们为这种 sidecar 容器的升级提供了一种新的解决方案,即热升级(hot upgrade)。

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
spec:
# ...
containers:
- name: nginx-sidecar
image: nginx:1.18
lifecycle:
postStart:
exec:
command:
- /bin/bash
- -c
- /usr/local/bin/nginx-agent migrate
upgradeStrategy:
upgradeType: HotUpgrade
hotUpgradeEmptyImage: empty:1.0.0
  • upgradeType: HotUpgrade代表该sidecar容器的类型是hot upgrade,将执行热升级方案hotUpgradeEmptyImage: 当热升级sidecar容器时,业务必须要提供一个empty容器用于热升级过程中的容器切换。empty容器同sidecar容器具有相同的配置(除了镜像地址),例如:command, lifecycle, probe等,但是它不做任何工作。
  • lifecycle.postStart: 状态迁移,该过程完成热升级过程中的状态迁移,该脚本需要由业务根据自身的特点自行实现,例如:nginx热升级需要完成Listen FD共享以及流量排水(reload)

更多

更多版本变化,请参考 release pageChangeLog

· 阅读需要 1 分钟
Mingshan Zhao

OpenKruise 是阿里云开源的云原生应用自动化管理套件,也是当前托管在 Cloud Native Computing Foundation (CNCF) 下的Sandbox项目。它来自阿里巴巴多年来容器化、云原生的技术沉淀,是阿里内部生产环境大规模应用的基于Kubernetes之上的标准扩展组件,也是紧贴上游社区标准、适应互联网规模化场景的技术理念与最佳实践。

OpenKruise在2021.3.4发布了最新的v0.8.0版本,其中增强了SidecarSet的能力,特别是对日志管理类Sidecar有了更加完善的支持。

背景

Sidecar是云原生中一种非常重要的容器设计模式,它将辅助能力从主容器中剥离出来成为单独的sidecar容器。在微服务架构中,通常也使用sidecar模式将微服务中的配置管理、服务发现、路由、熔断等通用能力从主程序中剥离出来,从而极大降低了微服务架构中的复杂性。随着Service Mesh的逐步风靡,sidecar模式也日益深入人心,在阿里巴巴集团内部也大量使用sidecar模式来管理诸如运维、安全、消息中间件等通用组件。

在Kubernetes集群中,Pod不仅可以实现主容器与sidecar容器的构建,同时提供了许多功能强大的workload(例如:deployment、statefulset)来对Pod进行管理、升级。但是随着kubernetes集群上的业务日益增多,sidecar容器的种类与规模也随之日益庞大,对线上sidecar容器的管理和升级成为了愈发繁杂的工作:

  1. 业务Pod里面包含了运维、安全、代理等多个sidecar容器,业务线同学不仅要完成自身主容器的配置,而且还需要熟悉这些sidecar容器的配置,这不仅增加了业务同学的工作量,同时也无形增加了sidecar容器配置的风险。
  2. sidecar容器的升级需要连同业务主容器一起重启(deployment、statefulset等workload基于Pod销毁、重建的模式,来实现Pod的滚动升级),推动和升级支撑着线上数百款业务的sidecar容器,必然存在着极大的业务阻力。
  3. 作为sidecar容器的提供者对线上诸多各种配置以及版本的sidecar容器没有直接有效的升级手段,这对sidecar容器的管理意味着极大的潜在风险。

阿里巴巴集团内部拥有着百万级的容器数量连同上面承载的上千个业务,因此,sidecar容器的管理与升级也就成为了亟待完善的主题。因此,我们总结了内部许多sidecar容器的通用化需求,并将其沉淀到OpenKruise上面,最终抽象为SidecarSet作为管理和升级种类繁多sidecar容器的利器。

OpenKruise SidecarSet

SidecarSet是OpenKruise中针对sidecar抽象出来的概念,负责注入和升级kubernetes集群中的sidecar容器,是OpenKruise的核心workload之一。它提供了非常丰富的功能,用户使用SidecarSet可以非常方便实现sidecar容器的管理。主要特性如下:

  1. 配置单独管理:为每一个sidecar容器配置单独的SidecarSet配置,方便管理
  2. 自动注入:在新建、扩容、重建pod的场景中,实现sidecar容器的自动注入
  3. 原地升级:支持不重建pod的方式完成sidecar容器的原地升级,不影响业务主容器,并包含丰富的灰度发布策略

注意:针对Pod中包含多个容器的模式,其中对外提供主要业务逻辑能力的容器称之为 主容器,其它一些如日志采集、安全、代理等辅助能力的容器称之为 Sidecar容器。例如:一个对外提供web能力的pod,nginx容器提供主要的web server能力即为 主容器,logtail容器负责采集、上报nginx日志即为 Sidecar容器。本文中的SidecarSet资源抽象也是为解决 Sidecar容器 的一些问题。

Sidecar logging architectures

应用日志可以让你了解应用内部的运行状况,日志对调试问题和监控集群活动非常有用。应用容器化后,最简单且最广泛采用的日志记录方式就是写入标准输出和标准错误。

但是,在当前分布式系统、大规模集群的时代下,上述方案还不足以达到生产环境的标准。首先,对于分布式系统而言,日志都是分散在单个容器里面,没有一个统一汇总的地方。其次,如果发生容器崩溃、Pod被驱逐等场景,会出现日志丢失的情况。因此,需要一种更加可靠,独立于容器生命周期的日志解决方案。

Sidecar logging architectures 是将logging agent放到一个独立的sidecar容器中,通过共享日志目录的方式,实现容器日志的采集,然后存储到日志平台的后端存储。

logsidecar

阿里巴巴以及蚂蚁集团内部同样也是基于这种架构实现了容器的日志采集,下面我将介绍OpenKruise SidecarSet如何助力 Sidecar日志架构在kubernetes集群中的大规模落地实践。

自动注入

OpenKruise SidecarSet基于kubernetes AdmissionWebhook机制实现了sidecar容器的自动注入,因此只要将sidecar配置到SidecarSet中,不管用户用 CloneSet、Deployment、StatefulSet 等任何方式部署,扩出来的 Pod 中都会注入定义好的 sidecar 容器。

inject sidecar

Sidecar容器的所有者只需要配置自身的SidecarSet,就可以在业务无感知的情况下完成sidecar容器的注入,这种方式极大的降低了sidecar容器使用的门槛,也方便了sidecar所有者的管理工作。为了满足sidecar注入的多种场景,SidecarSet除containers之外还扩展了如下字段:

# sidecarset.yaml
apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: test-sidecarset
spec:
# 通过selector选择pod
selector:
matchLabels:
app: web-server
# 指定 namespace 生效
namespace: ns-1
# container definition
containers:
- name: logtail
image: logtail:1.0.0
# 共享指定卷
volumeMounts:
- name: web-log
mountPath: /var/log/web
# 共享所有卷
shareVolumePolicy:
type: disabled
# 环境变量共享
transferEnv:
- sourceContainerName: web-server
# TZ代表时区,例如:web-server容器中存在环境变量 TZ=Asia/Shanghai
envName: TZ
volumes:
- name: web-log
emptyDir: {}
  • Pod选择器
    • 支持selector来选择要注入的Pod,如示例中将选择labels[app] = web-server的pod,将logtail容器注入进去,也可以在所有的pod中添加一个labels[inject/logtail] = true的方式,来实现全局性的sidecar注入。
    • namespace:sidecarSet默认是全局生效的,如果只想对某一个namespace生效,则配置该参数
  • 数据卷共享:
    • 共享指定卷:通过volumeMounts和volumes可以完成与主容器的特定卷的共享,如示例中通过共享web-log volume来达到日志采集的效果
    • 共享所有卷:通过 shareVolumePolicy = enabled | disabled 来控制是否挂载pod主容器的所有卷卷,常用于日志收集等 sidecar,配置为 enabled 后会把应用容器中所有挂载点注入 sidecar 同一路经下(sidecar中本身就有声明的数据卷和挂载点除外)
  • 环境变量共享 可以通过 transferEnv 从其它容器中获取环境变量,会把名为 sourceContainerName 容器中名为 envName 的环境变量拷贝到本sidecar容器,如示例中 日志sidecar容器共享了主容器的时区TZ,这在海外环境中尤其常见

注意:kubernetes社区对于已经创建的Pod不允许修改container数量,所以上述注入能力只能发生在Pod创建阶段,对于已经创建的Pod需要通过重建的方式来注入。

原地升级

SidecarSet不仅实现sidecar容器的注入,而且复用了OpenKruise中原地升级的特性,实现了在不重启Pod和主容器的前提下单独升级sidecar容器的能力。由于这种升级方式基本上能做到业务方无感知的程度,所以sidecar容器的升级已不再是上下交困的难题,从而极大解放了sidecar的所有者,提升了sidecar版本迭代的速度。

inplace sidecar

注意:kubernetes社区对于已经创建的Pod只允许修改 container.image 字段,因此对于sidecar容器的修改包含除 container.image 的其它字段,则需要通过Pod重建的方式,不能直接原地升级。

为了满足一些复杂的sidecar升级场景,SidecarSet除了原地升级以外,还提供了非常丰富的灰度发布策略。

灰度发布

灰度发布应该算是日常发布中最常见的一种手段,它能够比较平滑的完成sidecar容器的发布,尤其是在大规模集群的场景下,强烈建议使用这种方式。下面是 首批暂停,后续基于 最大不可用 滚动发布 的例子,假设一个有1000个pod需要发布:

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: sidecarset
spec:
# ...
updateStrategy:
type: RollingUpdate
partition: 980
maxUnavailable: 10%

上述配置首先发布(1000 - 980)= 20 个pod之后就会暂停发布,业务可以观察一段时间发现 sidecar 容器正常后,调整重新 update SidecarSet 配置:

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: sidecarset
spec:
# ...
updateStrategy:
type: RollingUpdate
maxUnavailable: 10%

这样调整后,对于余下的 980 个pod,将会按照最大不可用的数量(10% * 1000 = 100)的顺序进行发布,直到所有的pod都发布完成。

Partition 的语义是 保留旧版本 Pod 的数量或百分比,默认为 0。这里的 partition 不表示任何 order 序号。如果在发布过程中设置了 partition:

  • 如果是数字,控制器会将 (replicas - partition) 数量的 Pod 更新到最新版本。
  • 如果是百分比,控制器会将 (replicas * (100% - partition)) 数量的 Pod 更新到最新版本。

MaxUnavailable 是发布过程中保证的,同一时间下最大不可用的 Pod 数量,默认值为 1。用户可以将其设置为绝对值或百分比(百分比会被控制器按照selected pod做基数来计算出一个背后的绝对值)。

注意:maxUnavailable 和 partition 两个值是没有必然关联。举例:

  • 当 {matched pod}=100,partition=50,maxUnavailable=10,控制器会发布 50 个 Pod 到新版本,但是发布窗口为 10,即同一时间只会发布 10 个 Pod,每发布好一个 Pod 才会再找一个发布,直到 50 个发布完成。
  • 当 {matched pod}=100,partition=80,maxUnavailable=30,控制器会发布 20 个 Pod 到新版本,因为满足 maxUnavailable 数量,所以这 20 个 Pod 会同时发布。

金丝雀发布

对于有金丝雀发布需求的业务,可以通过strategy.selector来实现。方式:对于需要率先金丝雀灰度的pod打上固定的labels[canary.release] = true,再通过strategy.selector.matchLabels来选中该pod

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: sidecarset
spec:
# ...
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
canary.release: "true"
maxUnavailable: 10%

上述配置只会发布打上金丝雀labels的容器,在完成金丝雀验证之后,通过将 updateStrategy.selector 配置去掉,就会继续通过 最大不可用 来滚动发布。

打散发布

SidecarSet对于pod的升级顺序,默认按照如下规则:

  • 对升级的pod集合,保证多次升级的顺序一致
  • 选择优先顺序是(越小优先级越高): unscheduled < scheduled, pending < unknown < running, not-ready < ready, newer pods < older pods

除了上述默认发布顺序之外,scatter打散策略允许用户 自定义将符合某些标签的 Pod 打散 到整个发布过程中。比如,对于像 logtail 这种全局性的 sidecar container,一个集群当中很可能注入了几十个业务pod,因此可以使用基于 应用名 的方式来打散logtail的方式进行发布,实现 不同应用间打散灰度发布 的效果,并且这种方式可以同 最大不可用 一起使用。

apiVersion: apps.kruise.io/v1alpha1
kind: SidecarSet
metadata:
name: sidecarset
spec:
# ...
updateStrategy:
type: RollingUpdate
# 配置pod labels,假设所有的pod都包含labels[app_name]
scatterStrategy:
- key: app_name
value: nginx
- key: app_name
value: web-server
- key: app_name
value: api-gateway
maxUnavailable: 10%

注意:当前版本必须要列举所有的应用名称,我们将在下个版本支持 只配置label key 的智能打散方式。

总结

本次 OpenKruise v0.8.0 版本的升级,SidecarSet特性主要是完善了 日志管理类Sidecar 场景的能力,后续我们在持续深耕SidecarSet稳定性、性能的同时,也将覆盖更多的场景,比如下一个版本将会增加针对 Service Mesh场景 的支持。同时,我们也欢迎更多的同学参与到 OpenKruise 社区来,共同建设一个场景更加丰富、完善的 K8s 应用管理、交付扩展能力,能够面向更加规模化、复杂化、极致性能的场景。