如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?

作者:王思宇,阿里云技术专家,OpenKruise 社区负责人

通常情况下,人们只能使用普通旧数据作为 Kubernetes 中最小的操作单元。一些公司在他们的集群中入侵了 Kubelet 的代码,以便他们可以对容器做更多的事情。然而,为运行时扩展操作确实是一种错误的方法,因为它不利于开源和社区的合作。现在,云原生计算基金会沙箱项目之一 OpenKruise 提供了高级功能,可以在每个原始 Kubernetes 集群中操作容器运行时。在本次演讲中,我们将介绍 OpenKruise 中一些功能的用法,以及它如何与 Kubelet 和 CRI 合作。

本次分享主要分为以下几个部分,首先我们介绍在 Kubernetes 中,针对于对容器 runtime 的操作限制有哪些,也就是说我们在 Kubernetes 中,它的机制限制了我们哪些操作,其实是对 controller runtime 是做不到的;第二点是 OpenKruise 是怎样拓展对 controller runtime 的这些操作;第三点是我们简单做一个 demo,我们如何通过 OpenKruise 来实现这些操作的;第四点是简单介绍一下我们后续的一些规划。

Kubernetes 中针对容器运行时的操作有哪些限制?

Kubernetes 中的容器运行时

如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?_第1张图片

如上图所示,这是一个 Kubernetes 的基本结构,它的结构在每个节点( Node)上,其实是 Kubelet 在 API server 里面收到它的。比如 Pod 的变化,当 Kubelet 收到一个 Pod 创建之后,通过 CRI(Container Runtime Interface) , CNI 以及类似的公共接口(例如 CSI)来调用底层真正的接口实现者去完成操作。对于容器运行时来说,是通过 CRI 接口调用底层真正的 Runtime 运行时来完成对容器的创建和启动镜像拉取这些操作。

其中 CRI 是 Kubernetes1.5 之后加入的一个新功能,由协议缓冲区和 gRPC API 组成,提供了一个明确定义的抽象层,它的目的是对于 Kubelet 能屏蔽底下 Runtime 实现的细节而只显示所需的接口。
https://github.com/kubernetes...

在 Kubernetes1.5 之前,Kubelet 与 Docker 是相耦合的,Kubelet 其实是引入了 Docker 的 client,由它们直接对 Docker 操作。有了 CRI ,对于 Kubelet 来说就不用关心底层真正的 Runtime 实现是什么,而只需要调用这层接口,接口背后的实现可能是 Containerd-d ,可能是 CRI-O,也可能是 Docker。

CRI 的职责是对容器运行时以及对镜像做相关的管理,包括对容器的启停操作,对 Sandbox 容器的操作,容器 States 的数据采集,以及镜像的拉取和查询等操作。因此,CRI 提供了比较完善的容器接口,如下图。

如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?_第2张图片

Kubernetes 中容器运行时的操作限制

Kubernetes API 并没有提供对容器运行时的接口操作,它唯一提供的是对 v1 版本的 Pod 操作(Pod API CRUD,Pod Subresources API)。除了 Pod 创建和更新之外,唯一能跟 Runtime 做比较相对应操作的是 Exec subresource 和 Log subresource。

Kubernetes 的 API 层面限制了用户只能创建或删除 Pod ,除此之外,里面的容器只能做 Exec, Log 这样的操作。在 Kubernetes 接口层面,用户无法进行比如拉取镜像、重启容器等操作。
如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?_第3张图片

那有没有可能去拓展这个 API 呢?

我们发现 Kubelet 目前没有提供任何 hook 解决 plugin 的这个操作,来让外层能去动态拓展 Kubelet 所做的事情。( Kubelet 的接口是不提供这样的插件机制的)那是否可以加入一个与 Kubelet 类似的新组件,可以连接到 CRI API,来拓展 Kubernetes 容器进行时的操作呢?

我们同样会调用 CRI 这一层,比如它可以拉镜像,可以重启容器,它的上层也可以接收一个对于 Kubernetes API 上定义的一个 CRD 资源,这个 CRD 资源定义了让用户能够声明对 CRI 接口做一些操作。比如它可以定义指定用户去拉镜像,去重启容器,可以做更多的事情。

这种方式是我们能想到的,对于这个 Kubernetes 容器运行时 operations 的拓展思路。

如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?_第4张图片

OpenKruise 是什么?

OpenKruise 概念

Openkruise 是 Kubernetes 的一个拓展套件,它弥补了 Kubernetes 很多功能不足,例如对于应用工作负载(应用部署发布相关功能)的不足,对于 container runtime 操作的不足等。它可以配合原生 Kubernetes 使用,并为管理应用容器、sidecar、镜像分发等方面提供更加强大和高效的能力。

2020 年 11 月,OpenKruise 作为 Sandbox 项目加入 CNCF。
如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?_第5张图片

Openkruise 本身并不是一个 PasS 平台。但 PasS 平台可以通过利用 Openkruise 提供的拓展能力更好的管理,运维云原生的应用。感兴趣的朋友可以通过以下网址了解更多 OpenKruise 相关信息。

Github:
https://github.com/openkruise...

WebSite:
https://openkruise.io

OpenKruise 的功能

OpenKruise 是基于 CRD 的拓展,其功能大致可分为五部分:
(1)应用工作负载:包括针对无状态应用,有状态应用的灰度发布、流量控制、它的原地升级等相关功能;
(2)Sidecar 容器管理:提供更多增强的独立定义以及独立部署;
(3)应用多分区管理(Multi-domain management):一个应用如果要部署在多个分区,会进行打散和分片的管理。
(4)应用可用性防护:保护云原生应用在 Kubernetes 上运行时的高可用性;
(5)拓展增强操作:通过这种方式来实现对 container runtime(运行时)增强的操作能力。其中拓展增强操作是本文主要介绍的功能,后续我们会详细展开。

如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?_第6张图片
OpenKruise 功能图

OpenKruise 的架构

如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?_第7张图片

如图所示,OpenKruise 主要分成中心端(Kruise-manager)和节点端(Node)两个组件。中心端的 Kruise-manager 包含 controllers 和 webhooks ,通过 Kruise-manager 中心端的角色和每个节点上 kruise-daemon 功能结合,可以完成很多 Kubernetes 本身不提供的能力。Kruise-daemon 是用来避免对 Kubelet 做改造,通过拓展的方式对 CRI runtime 进行操作。

Runtime 的拓展功能

Runtime 有三个核心拓展功能。

原地升级功能

原地升级是一种可以避免删除、新建 Pod 的升级镜像能力。
如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?_第8张图片

如上图所示:第一部分并不是直接通过 kruise-daemon 拓展,而是利用 Kubelet 的原生机制,叫做原地升级。

如何理解原地升级?我们举一个简单的例子:比如原来有一个 pod-a ,此时的 pod-a 是通过 deployment 的或者 Openkruise 的 cloneset 扩容出来的。如果我们想要升级 app容器的镜像版本,比如从 Fv1 升级到 Fv2,正常情况下大家使用 development 部署是采用 Recreatte Update 也就是重建 Pod 升级,重建完成后我们会看到 Pod 的名字,Pod UID, (镜像也会升级为 V2) 很大程度上都会发生改变。

再看后面两者,前者 Pod 的名字和 UID 一定会发生变化,因为它已经不是同一个 Pod 的对象了。相对于我们这次介绍的原地升级,Pod 的对象其实还是原来的对象,Pod 的名字,Pod UID 都不变。其次,Pod 所在节点的 IP 也都不变,唯一变化的是镜像从 v1 级到了 v2,由于 Pod 节点没有发生任何变化,Pod 对象就不需要经过调度器重新调度,IP 分配,volume 分配,挂载这些耗时也都消除掉了,因此一个很明显好处就是节省了调度的耗时。

大家都知道,当应用镜像从 v1 升到 v2 的过程中,可能只是最顶上的 layers 发生了变化,底下绝大部分的这个 base 镜像,公共 layers 是没有发生变化的。

当我们在同一个节点上面做原地升级的时候,可以复用原有 v1 镜像的大部分 layers ,只用下载小部分的 layers 镜像。

在升级 app 容器的过程中,Pod 中的其他容器。如 sidecar 容器,是一直正常运行的,没有受到影响。反过来说,当我们升级 sidecar 容器时,容器也是正常运行的。这样可以很大程度上避免在升级一个旁路(比如运维)容器的过程中,也要对业务能力造成影响。

1.1 优势

• 节省操作耗时,包括:Pod 调度、IP 分配、volume 分配、挂载等;
• 复用大部分镜像层;
• 当一个容器进行升级时,不会对 Pod 中的其它容器造成影响;

1.2 工作原理

原地升级的原理可以简单理解为 Kubelet 在创建每个容器时,会为容器计算一个 hash 值,当上层修改了比如 app 容器的 image 之后,Kubelet 就认为容器的 hash 值发生了变化。当 Kubelet 发现 Pod spec 中 app 容器的 hash 值和实际的,如 container d 对应的 hash 值不一致时,就会把旧的 app 容器停掉,用新的镜像再重建新的 app 容器,从而实现容器的原地升级的能力。
如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?_第9张图片

### 容器重启功能
如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?_第10张图片

容器重启的功能是很多业务,包括运维平台都很依赖的功能。大家可能会问,在 Kubernetes 中,一个 Pod 既然是无状态的,那么想重启时就直接删除 Pod,再新建一个 Pod 不就可以了吗?

这是当然可以的,但对于业务来说可能还存在很多 debug 场景,并不只是重建一个新的 Pod 就可以,而是要从原地把容器进行重启,相当于把里面的业务进程重启。比如想保留一些 volume 中的数据,一些网络、堆栈信息等,这些场景都导致业务需要有 Kubernetes Pod 的容器原地重启能力。

Kubernetes 原生是不具备容器重启能力的,对于 Kubernetes 来说,如果想要重启容器,只能手动进入容器,把容器里的应用进程杀掉,这时当容器退出,Kubernetes 会再把它拉起。当然这种方式其实都比较 hack 的这么一种方式, Openkruise 所提供的容器重启能力对于 API 来说,只需要创建一个 CR。

CR 里写的东西很明确,name spacesl 只需定义跟 pod 在同一个 name spacesl 里,其中 name 是自定义的名字,通过指定需要重启的 Pod 是哪个,需要重启 pod 出来哪些 containers,当定义了这些信息之后,提交 CR,当 kruise 收到 CR ,kruise - manager 会先经过 webhooks,对它注入一些信息,接下来 kruise-daemon 拿到 CR 会根据 CR 中定义的信息(比如会找到对应 Pod 的容器)执行 preStop hook,再通过 CRI 接口,通过 EXTC 执行 preStop,当 preStop 执行完成之后再调用这个 CRI 的 stop 。

其实这个停止方式和本身 Kubelet 在删除 Pod 时对容器的停止方式是一致的。当 kruise-daemon 对旧容器,比如对零号的 app(app_0)容器停掉之后,Kubelet 感知到 app 容器停掉了,接着就会新建一个 F1 容器并把它拉起,通过这种方式来实现优雅的容器原地重启能力。

代码示例:

apiVersion: apps.kruise.io/v1alpha1
kind: ContainerRecreateRequest
metadata:
namespace: pod-namespace
name: xxx
spec:
podName: pod-name
containers:
- name: app
strategy:
# ...
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 1800
status:
containerRecreateStates:
- name: app
phase: Succeeded
phase: Completed
# ..

镜像预热功能

提前在节点上包括新建的 Node 上预热,就可以大幅度减少后续 Pod 扩容的耗时。
如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?_第11张图片

从上图我们可以看到,对于上层用户来说 Openkruise 提供了一个 CRD 叫 ImagePullJob,用户可以定义需要预热哪个镜像,也可以选择性的配置 selector(selector 可以是节点的标签选择器也可以根据 Pod 进行选择),以上都可以在 Pod 所在节点上进行预热。

当用户建立 ImagePullJob 后,对于 kruise 内部逻辑来说,kruise 会把 ImagePullJob 拆分到每个 node 对应 node image 的 CR 上,当同步上去后,节点上的 kruise-daemon 会拿到这个节点所对应node image 的 CR ,在节点上预热 CR 中定义的多个镜像。

换句话说,每个节点所对应的 node image 中的镜像列表,其实就表示了上层所有 ImagePullJob 指定在这个节点上要拉取镜像全集。kruise-daemon 底层拿到 node image 后,相当于也是调用 CRI 的 Pod image 接口来完成镜像的预热。

代码示例:

apiVersion: apps.kruise.io/v1alpha1
kind: ImagePullJob
metadata:
name: test-job
spec:
image: nginx:latest
parallelism: 10
selector:
# ...
podSelector:
# ...
completionPolicy:
# ...

未来项目规划

2021 年 12 月,OpenKruise 完成了首个正式版本 v1.0 的发布,使云原生应用自动化达到新的高峰。OpenKruise 从 2019 年发布 0.1 版本到现在已经有 2 年多的时间,已有超过 70 位贡献者参与贡献,star 数量已经超过 3000。2022,我们将推进 OpenKruise 成为 CNCF Incubation 项目,推动云原生应用自动化领域进一步成熟。

使用用户:
• 阿里巴巴集团, 蚂蚁集团, 斗鱼TV, 申通, Boss 直聘
• 杭银消费, 万翼科技, 多点, Bringg, 佐疆科技
• Lyft, 携程, 享住智慧, VIPKID, 掌门 1 对 1
• 小红书, 比心, 永辉科技中心, 跟谁学, 哈啰出行
• Spectro Cloud, 艾佳生活, Arkane Systems, 滴普科技, 火花思维
• OPPO, 苏宁, 欢聚时代, 汇量科技, 深圳凤凰木网络有限公司
• 小米, 网易,美团金融, Shopee, LinkedIn

你可能感兴趣的:(如何基于 OpenKruise 打破原生 Kubernetes 中的容器运行时操作局限?)