|
1 | | -<div align="center"> |
| 1 | +低成本地让开源用户接入模块运维体系。 |
2 | 2 |
|
3 | | -[English](./README.md) | 简体中文 |
| 3 | +# 架构设计 |
4 | 4 |
|
5 | | -</div> |
| 5 | +## 整体思路 |
| 6 | +整体思想是用 mock 的方式利用 k8s 现有的的运维调度能力,完成低成本的模块运维体系接入。 |
6 | 7 |
|
7 | | -ModuleController 是 Koupleless 运维和调度系统,它是一个 Kubernetes Operator 应用,可以部署到您的 Kubernetes 集群中运行。 |
| 8 | +### 如何触发调度? |
| 9 | +把基座 pod mock 成一个 k8s 的 node 节点,把模块 mock 成 k8s 的一个 pod,由此 kube-scheduler 会触发一轮调度,并且给 pod 分配上一个合适的 node。<br />值的注意的是,mock 的 pod 只能被调度到 mock 的 node 上,否则会造成无法执行安装的异常。这是因为,正常的 node 节点上的 kubelet 只会执行 pod 的安装流程。只有 mock 的特殊 node 上才存在 virtual-kubelet,只有 virtual-kubelet 会识别到模块,并且发起模块安装。这个约束可以通过 k8s 原生的 taints 和 toleration 配合保证。 |
8 | 10 |
|
9 | | -查阅 ModuleController 用户文档,请参考[此处](https://koupleless.io/docs/tutorials/module-operation/module-online-and-offline/)。 |
| 11 | +### 如何触发运维? |
| 12 | +用户可以使用正常的 deployment 或者是其他开源社区自定义的运维 operator,只要保证其 pod template 的定义符合我们的 mock pod 的定义规范即可。 |
10 | 13 |
|
11 | | -查阅 ModuleController 共建文档和实现原理,请参考[此处](https://koupleless.io/docs/contribution-guidelines/module-controller/architecture/)。 |
| 14 | +### 如何执行模块安装? |
| 15 | +可以使用社区的 virtual-kubelet 框架,virtual-kubelet 定义了一套 kubelet 的交互生命周期,预留了一些具体的接口点,如 createPod 等。开发者通过实现这些预留的接口,便可接入 k8s 的正常的运维生命周期。 |
| 16 | + |
| 17 | +## 架构图 |
| 18 | +<br />通过架构图我们不难发现,仅仅通过 virtual-kubelet 这一个简单的组件,我们就可以直接利用 k8s 实现三层调度。值得注意的事情是,POD-B 的调度会对应一个 Kube ApiServer,我们不妨称其为 ApiServerA。而触发 mock pod 调度到 mock node 上会对应另外一个 ApiServer,我们不妨称其为 ApiServerB。ApiServerA 不一定必须等于 ApiServerB,可以是彼此独立的。在一些云托管的 k8s 场景下,由于云厂商对 ApiServer 的权限限制,用户可能必须独立部署一套 ApiServer。 |
| 19 | + |
| 20 | + |
| 21 | +# 详细设计 |
| 22 | +本章节要求用户对 k8s 的运维调度体系有一定的了解,k8s 基础的运维调度流程不做复述,只对重点实现细节进行讨论。 |
| 23 | + |
| 24 | +## VPod 定义规范 |
| 25 | +在 Koupleless 体系中,模块(模块组)是一个重要的抽象,其包含如下核心属性: |
| 26 | + |
| 27 | +- 模块名。 |
| 28 | +- 模块版本。 |
| 29 | +- 模块包地址。 |
| 30 | +- 模块运行状态等。 |
| 31 | + |
| 32 | +由于在 ModuleController V2 设计方案中,我们常使用 VPod(底层为 K8S 的 Pod)承载模块模型或模块组,因此,我们需要预先定义清楚相关模块配置到 Pod 属性的映射关系,本小结将探讨有关映射关系。<br />我们从元数据的映射开始讨论,其中,模块的元数据映射到 V1Pod 的 containers 字段下,由于 1 个 Pod 可以有 N 个 Containers,所以 V1Pod 模型天然地支持模块组的描述: |
| 33 | + |
| 34 | +```yaml |
| 35 | +containers: |
| 36 | + - name: ${MODULE_NAME} |
| 37 | + image: ${MODULE_URL} |
| 38 | + env: |
| 39 | + - name: MODULE_VERSION |
| 40 | + value: ${MODULE_VERSION} |
| 41 | + resource: |
| 42 | + requests: |
| 43 | + cpu: 800m |
| 44 | + mem: 1GI |
| 45 | +``` |
| 46 | +
|
| 47 | +相应的,模块的安装情况也可以塞在对应的 containerStatus 中,映射关系如下: |
| 48 | +
|
| 49 | +```yaml |
| 50 | + - containerID: arklet://{ip}:{module}:{version} |
| 51 | + image: {module url} |
| 52 | + name: moduleName |
| 53 | + ready: true |
| 54 | + started: true |
| 55 | + state: |
| 56 | + running: |
| 57 | + startedAt: "2024-04-25T03:53:09Z" |
| 58 | +``` |
| 59 | +
|
| 60 | +除此之外,为了方便的能通过 kubectl 通过简单的表达是把有关的 pod 筛选出来,我们还应该在 labels 里加上:<br />module.koupleless.io/${moduleName}:${version} 标签<br />模块或模块组的运行期整体状态的映射关系如下: |
| 61 | +
|
| 62 | +- 所有模块调度但未安装:pod.status.phase = 'Pending' |
| 63 | +- 所有模块调度成功但是有几个安装失败:pod.status.phase = 'Failed',并设置一个 condition,type 为 module.koupleless.io/installed,value 为 false。 |
| 64 | +- 所有模块调度成功并且所有都安装成功: pod.status.phase = 'Running',并设置一个 condition,type 为 module.koupleless.io/installed,value 为 true。 |
| 65 | +
|
| 66 | +上述介绍了模块的属性的配置,除此之外,为了和 k8s 的调度和生命周期体系融合,我们还需要配置一些高阶的运行期配置。<br />我们从调度开始介绍,为了保证 VPod 只会被调度到 VNode 上,我们需要添加对应的 Affinity 配置,如下所示: |
| 67 | +
|
| 68 | +```yaml |
| 69 | + affinity: |
| 70 | + nodeAffinity: |
| 71 | + requiredDuringSchedulingIgnoredDuringExecution: |
| 72 | + nodeSelectorTerms: |
| 73 | + - matchExpressions: |
| 74 | + - key: basement.koupleless.io/stack |
| 75 | + operator: In |
| 76 | + values: |
| 77 | + - java # 多语言环境下可能有其他技术栈 |
| 78 | + - key: basement.koupleless.io/version |
| 79 | + operator: In |
| 80 | + values: |
| 81 | + - ${compatiable_version} # 模块可能只能被调度到一些特殊版本的 node 上,如有这种限制,则必须有这个字段。 |
| 82 | +``` |
| 83 | +
|
| 84 | +除此之外,为了保证 VNode 只会被调度 VPod,所以 VNode 会有一些特殊的 Taints 标签,相应的,Pod 也必须添加上对应的 Tolerations,如下: |
| 85 | +
|
| 86 | +```yaml |
| 87 | + tolerations: |
| 88 | + - key: "schedule.koupleless.io/virtual-node" |
| 89 | + operator: "Equal" |
| 90 | + value: "true" |
| 91 | + effect: "NoExecute" |
| 92 | +``` |
| 93 | +
|
| 94 | +通过上述的 Affinity 和 Tolerations,我们可以保证 VPod 只会被调度到 VNode 上。<br />当然,我们还必须考虑这套模式和 k8s 原生流量的兼容性问题,我们可以通过 k8s 的 readinessGate 机制达到目的,添加如下配置: |
| 95 | +
|
| 96 | +```yaml |
| 97 | + readinessGates: |
| 98 | + - conditionType: "module.koupleless.io/ready" # virtual-kubelet 会根据健康检查状况跟新这个值 |
| 99 | +``` |
| 100 | +
|
| 101 | +通过这些关键的规范,我们不仅能用 k8s 的 pod 模型描述模块或模块组,还能和 k8s 的调度和流量体系结合起来,一个完整的可能的样例 yaml 如下: |
| 102 | +
|
| 103 | +```yaml |
| 104 | +apiVersion: v1 |
| 105 | +metadata: |
| 106 | + labels: |
| 107 | + module.koupleless.io/module0: 0.1.0 |
| 108 | + module.koupleless.io/module1: 0.1.0 |
| 109 | + name: custome-module-group-as-pod |
| 110 | +spec: |
| 111 | + affinity: |
| 112 | + nodeAffinity: |
| 113 | + requiredDuringSchedulingIgnoredDuringExecution: |
| 114 | + nodeSelectorTerms: |
| 115 | + - matchExpressions: |
| 116 | + - key: basement.koupleless.io/stack |
| 117 | + operator: In |
| 118 | + values: |
| 119 | + - java # 多语言环境下可能有其他技术栈 |
| 120 | + - key: basement.koupleless.io/version |
| 121 | + operator: In |
| 122 | + values: |
| 123 | + - version0 |
| 124 | + tolerations: |
| 125 | + - key: "schedule.koupleless.io/virtual-node" |
| 126 | + operator: "Equal" |
| 127 | + value: "true" |
| 128 | + effect: "NoExecute" |
| 129 | + readinessGates: |
| 130 | + - conditionType: "module.koupleless.io/ready" |
| 131 | + containers: |
| 132 | + - name: module0 |
| 133 | + image: http://module_url_0 |
| 134 | + env: |
| 135 | + - name: MODULE_VERSION |
| 136 | + value: 0.1.0 |
| 137 | + resource: |
| 138 | + requests: |
| 139 | + cpu: 800m |
| 140 | + mem: 1GI |
| 141 | + - name: module1 |
| 142 | + image: http://module_url_1 |
| 143 | + env: |
| 144 | + - name: MODULE_VERSION |
| 145 | + value: 0.1.0 |
| 146 | + resource: |
| 147 | + requests: |
| 148 | + cpu: 800m |
| 149 | + mem: 1GI |
| 150 | +status: |
| 151 | + phase: Running |
| 152 | + containerStatuses: |
| 153 | + - containerID: arklet://192.168.0.1:module0:0.1.0 |
| 154 | + image: http://module_url_1 |
| 155 | + name: module0 |
| 156 | + ready: true |
| 157 | + started: true |
| 158 | + state: |
| 159 | + running: |
| 160 | + startedAt: "2024-04-25T03:53:09Z" |
| 161 | + - containerID: arklet://192.168.0.1:module0:0.1.0 |
| 162 | + image: http://module_url_1 |
| 163 | + name: module1 |
| 164 | + ready: true |
| 165 | + started: true |
| 166 | + state: |
| 167 | + running: |
| 168 | + startedAt: "2024-04-25T03:53:09Z" |
| 169 | + conditions: |
| 170 | + - lastProbeTime: null |
| 171 | + lastTransitionTime: "2024-04-24T09:24:58Z" |
| 172 | + status: "True" |
| 173 | + type: basement.koupleless.io/installed |
| 174 | + - lastProbeTime: null |
| 175 | + lastTransitionTime: "2024-04-24T09:24:58Z" |
| 176 | + status: "True" |
| 177 | + type: basement.koupleless.io/ready |
| 178 | +``` |
| 179 | +
|
| 180 | +
|
| 181 | +## VNode 规范设计 |
| 182 | +第一小节我们完成了 VPod 的规范设计。接下来我们需要探讨 VNode 的细则设计。<br />首先,VNode 必须有特殊的 Taints,保证正常的 Pod 不可能被调度到对应的 VNode 上,对应配置如下: |
| 183 | +
|
| 184 | +```yaml |
| 185 | +taints: |
| 186 | + - effect: NoSchedule |
| 187 | + key: "schedule.koupleless.io/virtual-node" |
| 188 | + value: True |
| 189 | +``` |
| 190 | +
|
| 191 | +除此之外,我们还必须保证 VPod 只会被调度到 VNode 上。为此,Node 必须提供对应 labels,保证 Pod 能够配置相应的亲和性调度,对应配置如下: |
| 192 | +
|
| 193 | +```yaml |
| 194 | +labels: |
| 195 | + basement.koupleless.io/stack: java |
| 196 | + basement.koupleless.io/version: ${some_version} |
| 197 | +``` |
| 198 | +
|
| 199 | +除此之外,node 还需要上报一些资源属性,如 capacity: |
| 200 | +
|
| 201 | +```yaml |
| 202 | +capacity: |
| 203 | + pods: 1 # 一般来说,我们只希望一个模块被调度 1 个模块 |
| 204 | +``` |
| 205 | +
|
| 206 | +以及需要定期更新 allocatable 字段: |
| 207 | +
|
| 208 | +```yaml |
| 209 | +allocatable: |
| 210 | + pods: 1 |
| 211 | +``` |
| 212 | +
|
| 213 | +为了方便排障,即通过 vnode 直接找到对应的 pod,vnode 的命名规范如下:virtual-node-{stack}-{namspace}-{podname}<br />最后,vnode 的 ip 直接使用对应 pod 暴露的 ip。<br />一个可能的样例 VNode 如下: |
| 214 | +
|
| 215 | +```yaml |
| 216 | +apiVersion: v1 |
| 217 | +kind: Node |
| 218 | +metadata: |
| 219 | + labels: |
| 220 | + basement.koupleless.io/stack: java |
| 221 | + basement.koupleless.io/version: version0 |
| 222 | + creationTimestamp: "2023-07-25T13:00:00Z" |
| 223 | + name: virtual-node-java-example-pod-01 |
| 224 | +spec: |
| 225 | + taints: |
| 226 | + - effect: NoExecute |
| 227 | + key: "schedule.koupleless.io/virtual-node" |
| 228 | + value: True |
| 229 | +status: |
| 230 | + allocatble: |
| 231 | + pod: 1 |
| 232 | + capacity: |
| 233 | + pod: 1 |
| 234 | +``` |
| 235 | +
|
| 236 | +
|
| 237 | +## 自愈体系设计 |
| 238 | +
|
| 239 | +VNode 需要自愈能力,原因如下。在 JVM 体系中,模块的反复安装会导致 metaspace 的使用率逐渐上涨。最终,metaspace 的使用率会达到某个阈值,过了这个阈值后模块再也无法被安装,会触发 OOM。由此,VNode 需要有一定的自愈能力,去应对这个情况。由于 Java 目前无法有效地通过 API 去完全清理干净 metaspace 中的类,因此我们将选择更简单的做法,对基座 pod 做替换,整体流程如下: |
| 240 | +
|
| 241 | +- 基座打 "schedule.koupleless.io/metaspace-overload: True: NoExecute" 的驱逐标签。 |
| 242 | +- 等待 VPod 被驱逐到别的节点。 |
| 243 | +- 执行模块卸载逻辑。 |
| 244 | +- 从基座 Pod 所对应的 ApiServer 中(有可能不是 VPod 对应的 ApiServer),删除掉基座 Pod。 |
| 245 | +
|
| 246 | +如此,便可以保证 Node 的可用性。 |
| 247 | +
|
| 248 | +
|
| 249 | +# 实现 |
| 250 | +## 重要组件 |
| 251 | +### DaemonEndpoints |
| 252 | +用于 kubectl 的回掉,获取 metric、日志、pod 信息等。 |
| 253 | +
|
| 254 | +### nodeutil.Provider |
| 255 | +实现 virtual-kubelet 的核心逻辑,执行具体的 pod 运维的动作如:pod 安装、pod 卸载、pod 状态获取等。 |
| 256 | +
|
| 257 | +### 同步 node 节点信息 |
| 258 | +定期向 apiserver 上报 node 的信息,如 CPU、MEM 的使用量、POD 的承载量等。 |
| 259 | +
|
| 260 | +## 初始化流程 |
| 261 | +virtual-kubelet 组建的初始化流程如下,按照先后顺序依次介绍。 |
| 262 | +
|
| 263 | +### 初始化 K8S 证书 |
| 264 | +virtual-kubelet 和 k8s 交互依赖证书。 |
| 265 | +
|
| 266 | +### 初始化 APIServer |
| 267 | +初始化一个 golang 原生的 http 的 Mux 实例。<br />http 服务必须是加密的,因此还需要初始化对应: |
| 268 | +
|
| 269 | +- kubelet 的 ca:用于加密 kubelet 的信息。 |
| 270 | +- kubelet 的 key:用于解密服务端发送过来的信息。 |
| 271 | +- server.ca:用于加密对服务端的掉用。 |
| 272 | +
|
| 273 | +这些证书不可以是任意的自签证书,具体维护逻辑可以参考:<br />[https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/](https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/) |
| 274 | +
|
| 275 | +### 初始化 Node 信息 |
| 276 | +初始化一个 virtual-node 的信息,并且上报给 ApiServer,一些关键的配置有: |
| 277 | +
|
| 278 | +- node 的 labels |
| 279 | +- node 的 taints |
| 280 | +- node 的 capacity |
| 281 | +- node 的 ip 地址 |
| 282 | +- node 的 dameonEndpoint 配置 |
| 283 | +
|
| 284 | +### 初始化 tracing 采点逻辑 |
| 285 | +初始化一个符合 open-tracing 踩点的逻辑。 |
| 286 | +
|
| 287 | +### 启动本地的 Informer 循环 |
| 288 | +初始化 pod 和 node 的 Informer 循环。 |
| 289 | +
|
| 290 | +### 初始化 Controller 循环 |
| 291 | +初始化 pod 和 node 的 controller 循环。 |
| 292 | +
|
| 293 | +
|
| 294 | +## NodeController 循环 |
| 295 | +主要逻辑是: |
| 296 | +
|
| 297 | +1. 初始化 node 的基础信息,并在 apiserver 创建。 |
| 298 | +2. 通过 NotifyNodeStatus 方法更新 node 的状态。 |
| 299 | + |
| 300 | +## PodController 循环 |
| 301 | +有 3 个核心循环: |
| 302 | +
|
| 303 | +1. 基于 k8s 的 informer 机制,不断同步服务器的 pod 信息到本地,并且更新本地的状态 / 创建 pod 实例,最终会掉用 Provider.GetPod / Provider.UpdatePod |
| 304 | +2. 基于 k8s 的 informer 机制,不断同步服务器的待删除的(DeletionTimestamp 不为 null)pod 到本地,并在本地删除对应的 pod 实例,最终会掉用 Provider.DeletePod |
| 305 | +3. 不断同步本地的 pod status,如果不一样则更新服务端的 pod status,会掉用 Provider.GetPodStatus 方法。 |
| 306 | + |
| 307 | +## Provider 实现核心 |
| 308 | +
|
| 309 | +### 运行时信息映射 |
| 310 | +在 virtual-kubelet 的抽象中,vpod 是 vk 与 apiserver 交互的最小单位,模块是 vk 与 arklet 交互的最小单位。<br />在用户的视角,其提交的运维单位是 vpod,vpod 会被 vk 翻译成 n 个可能的模块,并下发给 arklet。而 vpod 和模块的对应关系,在 pod 的 spec 定义时已经通过 container 字段进行映射和描述清楚了。<br />在 vk 的视角,其需要不断的查询模块的信息,并翻译成 vpod 的状态,然后同步给 apiserver,最终更新对应的 status 字段。可是目前的问题是,模块是属于哪个 vpod的?这个信息应该记录在哪里?应该如何聚和翻译?<br />首先需要解决的问题是,如何确定模块是属于哪个 vpod 的?<br />一个可以预期的方案是,利用 containerStatus 的 containerId 字段设置成 bizIdenetity 字段,进行模块 -> vpod 的信息查找。目前 bizIdentity 的格式是 bizName:bizVersion,由系统自动生成。未来,我们希望这个 identity 可以由运维管道强制制定,格式为 vpod_{namespace}_{podName}_{moduleName}:{version}。<br />现阶段暂时使用 bizName:bizVersion 作为 bizIdenetity 的字段,不过这会带来一个问题,如果用户在 2 个 pod 上都声明了同样的 bizName+bizVersion,那么实际上在 arklet 安装的时候,会报错,因为其不支持同名同版本重复装载。不过一方面暂时用户没有这个用法,所以暂时不解决。<br />因此,vpod 到模块的关联关系,是通过 bizIdentity 这个信息去关联的。只是目前,bizIdentity 的值是 bizName:version。未来会采用 vpod_{namespace}_{podName}_{moduleName}:{version}。<br />那么接下来的问题是,如何翻译 pod 的状态?<br />pod 的状态依赖 container 的状态,而 container 的状态就是模块的状态,模块的状态可以通过 arklet 一口气查出来。我们只需要根据 bizIdentity 进行模块状态的按照关联 pod 的维度聚合,就可以获取一个完整的 contaienrStatus 状态了。其中,如果对比 vpod 的 spec 发现有模块未安装 / 安装失败,则更新为安装失败。如果都安装成功,则更新 pod 状态为安装成功。 |
| 311 | +
|
| 312 | +### 从接受 vpod 到安装 / 更新 / 卸载模块 |
| 313 | +当 vk 接受到一个 vpod 后,其首先需要通过翻译模块,解析出 vpod 需要安装的模块模型,然后进行创建或者更新的调和逻辑。<br />如果是进行模块创建逻辑,则 vk 会掉用 arklet 进行有关模块的安装流程。<br />如果是进行 pod 更新的流程,则 vk 需要先对比内存中老的 pod 对应的那些模块信息,并安装新增的和删除老的。<br />如果是进行 pod 的删除流程,则 vk 需要卸载对应的模块。<br />在这里,由于分布式容错或者网络延迟等因素,jvm 层面可能出现悬挂的模块,即 vk 发现这个模块不关联到任何的模块中。因此我们需要一个兜底的守护进程,定期的去查找到这些的悬挂模块,并进行删除操作。 |
| 314 | +
|
| 315 | +### Node 的信息初始化 / 状态上报 / 自愈流程 |
| 316 | +todo |
12 | 317 |
|
13 | | -注意:当前 ModuleController 在 K8S 1.24 版本测试过,没有在其它版本测试,但 ModuleController 没有依赖 K8S 过多特性,理论上可以支持 K8S 其它版本。 |
|
0 commit comments