Skip to main content

· 阅读需要 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: 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 应用管理、交付扩展能力,能够面向更加规模化、复杂化、极致性能的场景。

· 阅读需要 1 分钟
Fei Guo

Ironically, probably every cloud user knew (or should realized that) failures in Cloud resources are inevitable. Hence, high availability is probably one of the most desirable features that Cloud Provider offers for cloud users. For example, in AWS, each geographic region has multiple isolated locations known as Availability Zones (AZs). AWS provides various AZ-aware solutions to allow the compute or storage resources of the user applications to be distributed across multiple AZs in order to tolerate AZ failure, which indeed happened in the past.

In Kubernetes, the concept of AZ is not realized by an API object. Instead, an AZ is usually represented by a group of hosts that have the same location label. Although hosts within the same AZ can be identified by labels, the capability of distributing Pods across AZs was missing in Kubernetes default scheduler. Hence it was difficult to use single StatefulSet or Deployment to perform AZ-aware Pods deployment. Fortunately, in Kubernetes 1.16, a new feature called "Pod Topology Spread Constraints" was introduced. Users now can add new constraints in the Pod Spec, and scheduler will enforce the constraints so that Pods can be distributed across failure domains such as AZs, regions or nodes, in a uniform fashion.

In Kruise, UnitedDeploymemt provides an alternative to achieve high availability in a cluster that consists of multiple fault domains - that is, managing multiple homogeneous workloads, and each workload is dedicated to a single Subset. Pod distribution across AZs is determined by the replica number of each workload. Since each Subset is associated with a workload, UnitedDeployment can support finer-grained rollout and deployment strategies. In addition, UnitedDeploymemt can be further extended to support multiple clusters! Let us reveal how UnitedDeployment is designed.

Using Subsets to describe domain topology

UnitedDeploymemt uses Subset to represent a failure domain. Subset API primarily specifies the nodes that forms the domain and the number of replicas, or the percentage of total replicas, run in this domain. UnitedDeployment manages subset workloads against a specific domain topology, described by a Subset array.

type Topology struct {
// Contains the details of each subset.
Subsets []Subset
}

type Subset struct {
// Indicates the name of this subset, which will be used to generate
// subset workload name prefix in the format '<deployment-name>-<subset-name>-'.
Name string

// Indicates the node select strategy to form the subset.
NodeSelector corev1.NodeSelector

// Indicates the number of the subset replicas or percentage of it on the
// UnitedDeployment replicas.
Replicas *intstr.IntOrString
}

The specification of the subset workload is saved in Spec.Template. UnitedDeployment only supports StatefulSet subset workload as of now. An interesting part of Subset design is that now user can specify customized Pod distribution across AZs, which is not necessarily a uniform distribution in some cases. For example, if the AZ utilization or capacity are not homogeneous, evenly distributing Pods may lead to Pod deployment failure due to lack of resources. If users have prior knowledge about AZ resource capacity/usage, UnitedDeployment can help to apply an optimal Pod distribution to ensure overall cluster utilization remains balanced. Of course, if not specified, a uniform Pod distribution will be applied to maximize availability.

Customized subset rollout Partitions

User can update all the UnitedDeployment subset workloads by providing a new version of subset workload template. Note that UnitedDeployment does not control the entire rollout process of all subset workloads, which is typically done by another rollout controller built on top of it. Since the replica number in each Subset can be different, it will be much more convenient to allow user to specify the individual rollout Partition of each subset workload instead of using one Partition to rule all, so that they can be upgraded in the same pace. UnitedDeployment provides ManualUpdate strategy to customize per subset rollout Partition.

type UnitedDeploymentUpdateStrategy struct {
// Type of UnitedDeployment update.
Type UpdateStrategyType
// Indicates the partition of each subset.
ManualUpdate *ManualUpdate
}

type ManualUpdate struct {
// Indicates number of subset partition.
Partitions map[string]int32
}

multi-cluster controller

This makes it fairly easy to coordinate multiple subsets rollout. For example, as illustrated in Figure 1, assuming UnitedDeployment manages three subsets and their replica numbers are 4, 2, 2 respectively, a rollout controller can realize a canary release plan of upgrading 50% of Pods in each subset at a time by setting subset partitions to 2, 1, 1 respectively. The same cannot be easily achieved by using a single workload controller like StatefulSet or Deployment.

Multi-Cluster application management (In future)

UnitedDeployment can be extended to support multi-cluster workload management. The idea is that Subsets may not only reside in one cluster, but also spread over multiple clusters. More specifically, domain topology specification will associate a ClusterRegistryQuerySpec, which describes the clusters that UnitedDeployment may distribute Pods to. Each cluster is represented by a custom resource managed by a ClusterRegistry controller using Kubernetes cluster registry APIs.

type Topology struct {
// ClusterRegistryQuerySpec is used to find the all the clusters that
// the workload may be deployed to.
ClusterRegistry *ClusterRegistryQuerySpec
// Contains the details of each subset including the target cluster name and
// the node selector in target cluster.
Subsets []Subset
}

type ClusterRegistryQuerySpec struct {
// Namespaces that the cluster objects reside.
// If not specified, default namespace is used.
Namespaces []string
// Selector is the label matcher to find all qualified clusters.
Selector map[string]string
// Describe the kind and APIversion of the cluster object.
ClusterType metav1.TypeMeta
}

type Subset struct {
Name string

// The name of target cluster. The controller will validate that
// the TargetCluster exits based on Topology.ClusterRegistry.
TargetCluster *TargetCluster

// Indicate the node select strategy in the Subset.TargetCluster.
// If Subset.TargetCluster is not set, node selector strategy refers to
// current cluster.
NodeSelector corev1.NodeSelector

Replicas *intstr.IntOrString
}

type TargetCluster struct {
// Namespace of the target cluster CRD
Namespace string
// Target cluster name
Name string
}

A new TargetCluster field is added to the Subset API. If it presents, the NodeSelector indicates the node selection logic in the target cluster. Now UnitedDeployment controller can distribute application Pods to multiple clusters by instantiating a StatefulSet workload in each target cluster with a specific replica number (or a percentage of total replica), as illustrated in Figure 2.

multi-cluster	controller

At a first glance, UnitedDeployment looks more like a federation controller following the design pattern of Kubefed, but it isn't. The fundamental difference is that Kubefed focuses on propagating arbitrary object types to remote clusters instead of managing an application across clusters. In this example, had a Kubefed style controller been used, each StatefulSet workload in individual cluster would have a replica of 100. UnitedDeployment focuses more on providing the ability of managing multiple workloads in multiple clusters on behalf of one application, which is absent in Kubernetes community to the best of our knowledge.

Summary

This blog post introduces UnitedDeployment, a new controller which helps managing application spread over multiple domains (in arbitrary clusters). It not only allows evenly distributing Pods over AZs, which arguably can be more efficiently done using the new Pod Topology Spread Constraint APIs though, but also enables flexible workload deployment/rollout and supports multi-cluster use cases in the future.

· 阅读需要 1 分钟
Fei Guo

The concept of controller in Kubernete is one of the most important reasons that make it successful. Controller is the core mechanism that supports Kubernetes APIs to ensure the system reaches the desired state. By leveraging CRDs/controllers and operators, it is fairly easy for other systems to integrate with Kubernetes.

Controller runtime library and the corresponding controller tool KubeBuilder are widely used by many developers to build their customized Kubernetes controllers. In Kruise project, we also use Kubebuilder to generate scaffolding codes that implement the "reconciling" logic. In this blog post, I will share some learnings from Kruise controller development, particularly, about concurrent reconciling.

Some people may already notice that controller runtime supports concurrent reconciling. Check for the options (source) used to create new controller:

type Options struct {
// MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1.
MaxConcurrentReconciles int

// Reconciler reconciles an object
Reconciler reconcile.Reconciler
}

Concurrent reconciling is quite useful when the states of the controller's watched objects change so frequently that a large amount of reconcile requests are sent to and queued in the reconcile queue. Multiple reconcile loops do help drain the reconcile queue much more quickly compared to the default single reconcile loop case. Although this is a great feature for performance, without digging into the code, an immediate concern that a developer may raise is that will this introduce consistency issue? i.e., is it possible that two reconcile loops handle the same object at the same time?

The answer is NO, as you may expect. The "magic" is enforced by the workqueue implementation in Kubernetes client-go, which is used by controller runtime reconcile queue. The workqueue algorithm (source) is demonstrated in Figure 1.

workqueue

Basically, the workqueue uses a queue and two sets to coordinate the process of handling multiple reconciling requests against the same object. Figure 1(a) presents the initial state of handling four reconcile requests, two of which target the same object A. When a request arrives, the target object is first added to the dirty set or dropped if it presents in dirty set, and then pushed to the queue only if it is not presented in processing set. Figure 1(b) shows the case of adding three requests consecutively. When a reconciling loop is ready to serve a request, it gets the target object from the front of the queue. The object is also added to the processing set and removed from the dirty set (Figure 1(c)). Now if a request of the processing object arrives, the object is only added to the dirty set, not to the queue (Figure 1(d)). This guarantees that an object is only handled by one reconciling loop. When reconciling is done, the object is removed from the processing set. If the object is also shown in the dirty set, it is added back to the back of the queue (Figure 1(e)).

The above algorithm has following implications:

  • It avoids concurrent reconciling for the same object.
  • The object processing order can be different from arriving order even if there is only one reconciling thread. This usually would not be a problem since the controller still reconciles to the final cluster state. However, the out of order reconciling may cause a significant delay for a request. workqueue-starve.... For example, as illustrated in Figure 2, assuming there is only one reconciling thread and two requests targeting the same object A arrive, one of them will be processed and object A will be added to the dirty set (Figure 2(b)). If the reconciling takes a long time and during which a large number of new reconciling requests arrive, the queue will be filled up by the new requests (Figure 2(c)). When reconciling is done, object A will be added to the back of the queue (Figure 2(d)). It would not be handled until all the requests coming after had been handled, which can cause a noticeable long delay. The workaround is actually simple - USE CONCURRENT RECONCILES. Since the cost of an idle go routine is fairly small, the overhead of having multiple reconcile threads is low even if the controller is idle. It seems that the MaxConcurrentReconciles value should be overwritten to a value larger than the default 1 (CloneSet uses 10 for example).
  • Last but not the least, reconcile requests can be dropped (if the target exists in dirty set). This means that we cannot assume that the controller can track all the object state change events. Recalling a presentation given by Tim Hockin, Kubernetes controller is level triggered, not edge triggered. It reconciles for state, not for events.

Thanks for reading the post, hope it helps.

· 阅读需要 1 分钟
Fei Guo
Siyu Wang

Kubernetes 目前并没有为一个应用应该使用哪个控制器提供明确的指引,这尤其不利于用户理解应用和 workload 的关系。 比如说,用户通常知道什么时候应该用 Job/CronJob 或者 DaemonSet,这些 workload 的概念是非常明确的 -- 前者是为了任务类型的应用部署、后者则是面向需要分发到每个 node 上的长期运行 Pod。

但是另一些 workload 比如 DeploymentStatefulSet 之间的界限是比较模糊的。一个通过 Deployment 部署的应用也可以通过 StatefulSet 部署,StatefulSet 对 Pod 的 OrderedReady 策略并非是强制的。而且,随着 Kubernetes 社区中越来越多的自定义 controllers/operators 变的成熟,用户就越难以为自己的应用找到一个最合适的 workload 来管理,尤其是一些控制器的功能上都存在重合部分。

Kruise 尝试在两个方面来缓解这个问题:

  • 在 Kruise 中谨慎设计新的控制器,避免不必要的功能重复给用户来带困扰
  • 为所有提供出来的 workload 控制器创建一个分类机制,方便用户更容易理解它们的使用场景。我们下面会详细描述一下,首先是 controller 命名上的规范:

Controller 命名惯例

一个易于理解的 controller 名字对于用户选用是非常有帮助的。经过对内外部不少 Kubernetes 用户的咨询,我们决定在 Kruise 中实行以下的命名惯例(这些惯例与目前上游的 controller 命名并不冲突):

  • Set 后缀:这类 controller 会直接操作和管理 Pod,比如 CloneSet, ReplicaSet, SidecarSet 等。它们提供了 Pod 维度的多种部署、发布策略。
  • Deployment 后缀:这类 controller 不会直接地操作 Pod,它们通过操作一个或多个 Set 类型的 workload 来间接管理 Pod,比如 Deployment 管理 ReplicaSet 来提供一些额外的滚动策略,以及 UnitedDeployment 支持管理多个 StatefulSet/AdvancedStatefulSet 来将应用部署到不同的可用区。
  • Job 后缀:这类 controller 主要管理短期执行的任务,比如 BroadcastJob 支持将任务类型的 Pod 分发到集群中所有 Node 上。

Set, DeploymentJob 都是被 Kubernetes 社区广泛接受的概念,在 Kruise 中给他们定义了明确的扩展规范。

我们能否对有相同后缀的 controller 做进一步区分呢?通常来说前缀前面的名字应该是让人能一目了然的,不过也有一些情况下很难一语描述 controller 自身的行为。可以看一下 StatefulSet 来源的这个 issue,社区用了四个月的时间才决定用 StatefulSet 这个名字代替过去的 PetSet,尽管新名字也让人看起来比较困惑。

这个例子说明了有时候一个精心计划的名字也不一定有助于标识这个 controller。因此,Kruise 并不打算解决这个问题,而是通过以下的标准来帮助对 Set 类型的 controller 分类。

固定 Pod 名字

StatefulSet 的一个独有的特性是支持一致的 Pod 网络和存储标识,这在本质上是通过固定 Pod 名字来实现的。Pod 名字可以用于标识网络和存储,因为它是 DNS record 的一部分,并且可以作为 PVC 的名字。既然 StatefulSet 下的 Pod 都是通过同一个模板创建出来的,为什么需要这个特性呢?一个常见的例子就是用于管理分布式一致性服务,比如 etcd 或 Zookeeper。这类应用需要知道集群构成的所有成员,并且在重建、发布后都需要保持原有的网络标识和磁盘数据。而像 ReplicaSet, DaemonSet 这类的控制器是面向无状态的,它们并不会新建 Pod 时并不会复用过去的 Pod 名字。

为了支持有状态,控制器的实现上会比较固定。StatefulSet 依赖于给每个 Pod 名字中加入一个序号,在扩缩容和滚动升级的时候都需要按照这个序号的顺序来执行。但这样一来,StatefulSet 也就无法做到另一些增强功能,比如:

  • 当缩小 replicas 时选择特定的 Pod 来删除,这个功能在跨多个可用区部署的时候会用到。
  • 把一个存量的 Pod 接管到另一个 workload 下面(比如 StatefulSet

我们发现很多云原生应用并不需要这个有状态的特性来固定 Pod 名字,而 StatefulSet 又很难在其他方面做扩展。为了解决这个问题,Kruise 发布了一个新的控制器 CloneSet 来管理无状态应用,CloneSet 提供了对 PVC 模板的支持,并且为应用部署提供了丰富的可选策略。以下表中比较了 Advanced StatefulSet 和 CloneSet 一些方面的能力:

FeaturesAdvanced StatefulSetCloneSet
PVCYesYes
Pod nameOrderedRandom
Inplace upgradeYesYes
Max unavailableYesYes
Selective deletionNoYes
Selective upgradeNoYes
Change Pod ownershipNoYes

目前对于 Kruise 用户的建议是,如果你的应用需要固定的 Pod 名字(网络和存储标识),你可以使用 Advanced StatefulSet,否则 CloneSet 应该是 Set 类型控制器的首选。

总结

Kruise 会为各种 workload 选择明确的名字,本文目标是能为 Kruise 用户提供选择正确 controller 部署应用的指引。 希望对你有帮助!