Kubernetes 是一个容器编排平台,用于调度、部署和管理容器化应用。并且经过几年的发展,k8s 已经成为事实上的容器编排平台标准。集群是 k8s 架构的构建块(building block)。集群由多个工作节点(物理机或者虚拟机)组成,提供一个可供容器运行的资源池。一个集群拥有:
- 一个单独的 API 入口
- 一个集群范围的资源命名组织结构
- 一个调度域
- 一个服务路由域
- ……
每个集群都是独立的系统,可以部署在公司的自建机房,也可以部署在云厂商的一个可用区。k8s 负责管理集群的可用资源:调度器将容器调度到适当的机器、kubelet 负责 pod/ 容器的生命周期管理。这些都是大家很熟悉的概念了。但是 k8s 的作用范围是集群内,一个集群内的控制平面无法感知另一个集群的资源余量和服务状态。当多集群的应用场景出现时,我们怎么处理多个集群的调度呢。
为什么要有多集群调度?
通常来说,一个集群的大小需要预估业务的资源总量。当资源不够时,可以通过增加机器数量来进行集群扩容。但是集群规模也不是无限上升的。当节点 /pod 的数量越多,对控制平台的组件的压力就越大,进而影响集群整体稳定性。
- 单集群的容量限制:单集群的最大节点数不是一个确定值,其受到集群的部署方法和业务使用集群资源的方式的影响。在官方文档中的集群注意事项里提到单集群 5000 个节点上限,我们可以理解为推荐最大节点数。
- 多租户:因为容器没法做到完美的隔离,不同租户可以通过不同宿主机或者不同集群分割开。对企业内部也是如此,业务线就是租户。不同业务线对于资源的敏感程度也不同,企业也会将机器资源划分为不同集群给不同业务线用。
- 云爆发:云爆发是一种部署模式,通过公有云资源来满足应用高峰时段的资源需求。正常流量时期,业务应用部署在有限资源的集群里。当资源需求量增加,通过扩容到公有云来消减高峰压力。
- 高可用:单集群能够做到容器级别的容错,当容器异常或者无响应时,应用的副本能够较快地另一节点上重建。但是单集群无法应对网络故障或者数据中心故障导致的服务的不可用。跨地域的多集群架构能够达到跨机房、跨区域的容灾。
- 地域亲和性:尽管国内互联网一直在提速,但是处于带宽成本的考量,同一调用链的服务网络距离越近越好。服务的主调和被调部署在同一个地域内能够有效减少带宽成本;并且分而治之的方式让应用服务本区域的业务,也能有效缓解应用服务的压力。
当然,使用多集群调度肯定会增加整体架构的复杂度,集群之间的状态同步也会增加控制面的额外开销。所以,多集群的主要攻克的难点就是跨集群的信息同步和跨集群网络连通方式。跨集群网络连通方式一般的处理方式就是确保不同机房的网络相互可达,这也是最简单的方式。
而我比较干兴趣的是跨集群的信息同步。多集群的服务实例调度,需要保证在多集群的资源同步的实时,将 pod 调度不同的集群中不会 pod pending 的情况。控制平面的跨集群同步主要有两类方式:
- 定义专属的 API server:通过一套统一的中心化 API 来管理多集群以及机器资源。KubeFed 就是采用的这种方法,通过扩展 k8s API 对象来管理应用在跨集群的分布。
- 基于 Virtual Kubelet:Virtual Kubelet 本质上是允许我们冒充 Kubelet 的行为来管理 virtual node 的机制。这个 virtual node 的背后可以是任何物件,只要 virtual node 能够做到上报 node 状态、和 pod 的生命周期管理。Liqo 就是通过定制的 virtual kubelet 来实现多集群管理。
KubeFed[1]和Liqo[2]都是开源项目,两者的主要区别如下:
场景 |
Kubefed |
Liqo |
Pod 调度 |
静态调度、动态调度只支持 Deployment/ReplicaSet |
动态调度,但是还是会出现 pending 情况 |
控制平面 |
一套中心化管理组件 |
每个集群对等管理 |
集群发现 |
主动注册 |
动态发现和注册新集群 |
本文不会详细介绍这些项目在部署和网络管理的内容,重点分析两个项目(Kubefed、Liqo)的跨集群资源调度。
KubeFed
KubeFed 的目的是通过统一的 API 管理多个 k8s 集群,实现单一集群管理多个 k8s 集群的机制,并通过 CRD 的机制来扩展现有的 k8s 资源跨集群编排的能力。
KubeFed 将集群分为 host cluster,member cluster 两个角色:
- host cluster:KubeFed 的控制平面面,用户需要安装 KubeFed 的 operator 组件。
- member cluster:注册到 KubeFed API 上的集群,由 KubeFed 统一管理。通过命令行
kubefedctl join 的
方式手动注册到 host cluster。
被联邦管理的 k8s API 资源统称为 Federated Resources,例如 FederatedDeployments。开启跨集群调度也需要在 host cluster 上显式地对应用资源开启集群联邦管理,开启的方式是通过 kubefedctl federate
创建 FederatedTypeConfig。KubeFed 也支持联邦化 namespace,让 namespace 下面的资源都联邦化。通过 federation control plane 来下发资源到 member cluster。
kubefed-overview
KubeFed 概览
资源调度
那 KubeFed 怎么支持跨集群部署的呢?在开启资源的联邦化之后,Federated Resources 的 spec 分成 3 个基础配置 :
- template:原资源类型的 spec
- placement:定义期望部署的集群
- overrides:定义集群特定配置,基于原资源类型的 spec 进行修改,比如副本数的变化
用户在部署服务时,可以在 spec 下定义部署的集群(placement)和集群指定参数(overrides)。比如服务需要在 cluster2 集群上增加副本数,修改 deployment 的 replicas,需要在 overrides 上填写“/spec/replicas”的修改。可以想到为了通用,overrides 的配置有一定复杂性。
spec:
template:
metadata:
labels:
app: nginx
spec:
replicas: 3
...
placement:
clusters:
- name: cluster2
- name: cluster1
overrides:
- clusterName: cluster2
clusterOverrides:
- path: "/spec/replicas"
value: 5
...
Federated Resources 还支持通过另一个 CRD 来 ReplicaSchedulingPreference 配置资源类型级别的集群调度倾向。该方法通过指定资源类型在 member cluster 的最大 / 最小副本数实现了依据剩余可用资源的动态平衡。最终 controller 会将 preference 信息更新到 Federated Resources 的 overrides 上,从而重新分配实例。不过该功能还只支持 deployment/replicaset 资源。
可以看出在 KubeFed 的架构,还是区分了不同集群的配置和动态调度。
具体看看静态调度和动态调度。
静态调度
首先,FederatedTypeConfig 的 spec 里定义了 host cluster 中的 Federated Resources 和 member cluster 集群的所代表的资源:
- federatedType 定义在 host cluster 在多集群中的状态的 API 资源类型(由用户创建)
- targetType 定义在 member cluster 集群代表 Federated Resources 的 API 资源类型(由 controller 创建)
- statusType 用来更新 federated resource 的状态 (由 controller 创建,默认为 federatedType)
spec 对应的 federatedType 是定义的联邦资源(由用户创建),而 targetType 实际上是真正的 k8s workload 资源, KubeFed 的 controller 可以通过 federatedType 的 placement 和 overrides 来控制各个集群里的对应的 targetType 的资源的分布。具体来说,负责下发 targetType 的 SyncController 会调用 dispatcher 生成 JSONPatch 并向 member cluster 创建底层资源。
kubefed-static-scheduling
KubeFed 静态调度
默认创建 Federated 资源时,可以在 spec 下定义部署的集群(placement)和集群指定参数(overrides),实现多集群的 pod 分配。但是这种调度是静态的。如果 overrides 设置的副本数超过集群剩余可用资源,那么新 pod 在集群里因为资源不够导致 pending。静态方式在大规模场景下无非是低效的,扩集群调度在极端情况下需要人工观测集群剩余资源来规划集群分布。但是很多服务是不需要感知集群信息的,他最在意的是还是服务实例能够正常启动,希望服务在不同集群的打散是一种动态被平台托管的。所以 KubeFed 引入了 ReplicaSchedulingPreference(RSP)的功能。
RSP 的实现依赖于一个新的 CRD——ReplicaSchedulingPreference 关键的几个配置项为:
- totalReplicas:期望的副本数
- rebalance:是否允许已调度的副本被重新平衡
- intersectWithClusterSelector:是否结合 Fed Resources 的 placement 下定义的集群结合
- clusters:定义各集群期望的最大最小副本数,以及分配权重
例如,下面这个例子期望 cluster1 和 cluster2 以 2:3 的比例下发服务的副本。当集群资源充足时,cluster1 会分配 4 个 pod,cluster2 被分配 6 个实例。当集群 cluster2 资源不足时,rebalance 的设置允许将 cluster2 的副本挪到 cluster1。
spec:
targetKind: FederatedDeployment
totalReplicas: 10
rebalance: true
intersectWithClusterSelector: false
clusters:
cluster1:
weight: 2
cluster2:
weight: 3
下图描述了 KubeFed RSP 的工作流程,起作用的组件是 ReplicaScheduler。
kubefed-rsp-sched
KubeFed ReplicaSchedulingPreference workflow
ReplicaScheduler 首先会获取 PreferredClusters,将健康的集群的信息收集。然后获取当前服务副本的集群分布,会统计每个集群的 currentReplicasPerCluster、estimatedCapacity:
- currentReplicasPerCluster:集群内 running 并且 ready 的 pod 数量
- estimatedCapacity:期望的副本数 spec.replicas – unschedulable 的 pod 数量
pod-unschedulable
ReplicaScheduler.Planner 依据上述信息(RSP 的 spec、currentReplicasPerCluster 和 estimatedCapacity),在可用集群中分配副本。Planner 的第一步会依据 ReplicaSchedulingPreference 中集群权重将可用集群排序,并划定每个集群的可分配上界和下界:
- 下界:从 RSP 的 minReplicas 和 totalReplicas(totalReplicas 累减)、estimatedCapacity,三值中选择最小的值作为每个集群的分配下界。
- 上界:如果 rebalance 未开启:首先在 currentReplicasPerCluster 的数量、estimatedCapacity 和 RSP 的 maxReplicas 选择最小值,作为集群可用数量的增量,每个集群的上界是 下界+增量。
最终依据集群权重,将副本分配到各集群中。
ReplicaScheduler.Plugin 获得新的 overrides 信息将原 Federated resources 的 spec 更新。之后的流程就是原有静态调度负责下发到集群。整个 ReplicaSchedulingPreference 能够动态地在多个集群分配 pod,并且包含了处理 Pending Pod 的情况。
但是局限在于只支持 Deployment/ReplicaSet 资源。
Liqo
与 KubeFed 相比,Liqo 有着 pod 无缝调度、去中心化治理等优势。
- 去中心化管理:采用 P2P 的对等管理,减少中心化管理组件。
- 无缝调度:创建用户服务资源时,和单集群操作一致。
Liqo 将集群划分为 home cluster 和 foreign cluster。在每个集群都需要安装 liqo 的组件来管理多集群,这两类集群本质上没有区别。只是对于 home cluster 来说,其他的 foreign cluster 都是通过 Virtual Kubelet[3] 的方式映射成本集群节点。home cluster 将 foreign cluster 当作一个大的节点来使用。这样就能做到用户在 home cluster 上创建资源时,能够调度到 foregin cluster 上。Virutal Kubelet 具体的内容可以看这篇文章《Virtual Kubelet[4]》。Virutal Kubelet 依据目的实现如下功能,就能在 k8s 集群内注册一个虚拟的节点。
自定义的 provider 必须提供以下功能:
- 提供 pod、容器、资源的生命周期管理的功能
- 符合 virtual kubelet 提供的 API
- 不直接访问 k8s apiserver,定义获取数据的回调机制,例如 configmap、secrets
在集群注册方面,Liqo 支持动态发现集群(mDNS、LAN 两种方式)。在安装 Liqo 组件时,可配置集群通过何种方式发现和集群共享出去的资源百分比(默认 30%)。所以 Liqo 的多集群前提是集群之间已经网络可达。
Ligo 调度
在调度方面,因为 remote cluster 通过 virtual kueblet 将该集群视为一个大节点,可以直接依赖本集群的 kube-scheduler 来调度。但是这里还是有个问题。当 remote cluster 的资源碎片比较多时,大节点上报资源时会聚合成一个大块资源。导致大套餐的 pod 调度到 remote cluster 会出现 pending。
Ligo 的工作流程如下图所示 . 当用户创建一个 deployment 时,默认调度器负责判断能否调度到 virtual node 上。之后这个服务的 pod 创建会被 virtual kubelet 接管。当 pod 被调度的 virtual node 上时,virtual kubelet 会在 remote cluster 会对应的 replicaSet。使用 replicaSet 原因是 remote cluster 上的 pod 被驱逐时,能够在集群上重建,而不是在 home cluster 重新调度。
liqo-pods-management
在了解多集群调度的细节之前,需要先弄清楚 virtual kubelet 的工作机制。我们知道 Kubelet 有个 SyncHandler, virtual kubelet 也有 PodLifecycleHandler,处理 pod 被 k8s 创建、更新、删除的情况。例如,Liqo 的 virtual kubelet 处理创建 pod 时,会先将 pod 的元数据里包含 home cluster 的信息转换成 foreign cluster (比如 label 和 namespace),然后将 pod 包在 ReplicaSet 里通过 foreign Client 创建 ReplicaSet 资源。
上面介绍了一个 pod 在不同集群之间的映射。接下来从 foreign cluster 的角度发现 home cluster 的共享资源的具体流程。下属 Controller 和 CRD 都是由 Liqo 安装。
liqo-discovery
第一阶段:发现 foreign cluster 并上报可用资源
- DiscoveryController 是第一个集群注册的 controller,DiscoveryController 发现并创建 foreignCluster 资源:通过 WAN/LAN 发现远端集群,将 remote cluster 的信息填入 foreignCluster.spec,之后的 Controller 通过 foreignCluster 来获取 remote cluster 的信息。
- ForeignClusterController 监听 foreignCluster CR,并维护 remote cluster 的 identity 信息。这些信息用来构建 virtual kubelet,和 crdReplicator 使用。ForeignClusterController 通过 Authentication Service 向远端的集群 ForeignAuthURL 验证 remote cluster 的身份信息,并将集群的证书存储在本地 secret。
- ForeignClusterController 在确认 foreign cluster 期望 outgoingPeering(即 home cluster 可以分享资源给 foreign cluster 使用),会创建 resourceRequest CR(不包含具体资源,只有集群元数据 authURL,用作后续资源请求的处理)。ResourceRequestController 监听 resourceRequest CR,首先确保 foreignCluster 的 foreignCluster/tenant 资源存在;如果不存在则创建 CR,确保 ForeignAuthURL 是 resourceRequest.Spec.AuthURL。
- ResourceRequestController 负责监控本地资源,将需要更新共享资源配额的 remote cluster id 压入 Broadcaster 组件的队列里。将集群 Broadcaster 组件接着调用 OfferUpdater.Push 做资源同步。OfferUpdater 负责组装 resourceOffer 里。resourceOffer 的 spec 保存集群的可共享资源,供远端的 virtual kubelet 查询。
- 并且 ResourceRequestController 还会监听本地 node/pod 事件,维护集群资源缓存(将 home cluster 全局可用资源,和 remote cluster 占用资源分开)。当达到更新阈值(默认 80%),也会主动调用 OfferUpdate.Push 更新可用资源。当创建 pod 时,集群资源减去 pod 资源;当 pod 有 remoteClusterId 标签(vk 添加)时,会累加共享的 remote 集群的资源。做到区分本集群和 remote pod,当集群资源-pod 变化资源,会触发更新。
- 最终 ResourceRequestController 的 OfferUpdater 按序处理 5、6 步 pushed 的 queue 里集群 id,获取 home cluster 集群资源并更新 home 集群的 ResourceOffer 资源上限(offer.Spec.ResourceQuota.Hard) 。计算公式:(本集群可用资源量 + remote cluster 的 pod 资源 )* ResourceSharingPercentage(依据配置共享给远端所有集群的资源)。
第二阶段:virtual kubelet 监听 home cluster 的资源变化
- 从 foreign cluster 的角度,也会发现 home cluster 并创建相应的 foreign cluster。virtual kubelet 在 foreign cluster 注册时,初始化 virtual-kubelet provider。在初始化的时候通过 foreignClusterID 拿到 home cluster 的 kubeClient Config(上一阶段集群注册时生成的 secret)。
- LiqoNodeProvider 监听 resourceOffer 资源(通过 foreignClusterID 获得自己的可用资源量)。从 offer 里的 ResourceQuota 中获的 node 的资源的 Capacity 和 Allocatable 值并调用 onNodeChangeCallback。在 virtual kubelet 机制中,NodeProvider 是被上层 NodeController 调用 NotifyNodeStatus 方法,来监控 virtual node 的变化。
- 实际的状态更新由 NodeController 定义的回调函数将节点信息压入 channel,NodeController 的控制循环实时异步地将资源变化向本集群的 api server 更新。
可以看到 home cluster 和 foreign cluster 没有本质上的区别,两者可以相互使用对方允许共享的机器资源。当然,集群 A 也可以配置成不贡献自己的资源出去,只使用别的集群分享出来的资源。
总结
本文介绍了 KubeFed/Liqo 两个开源项目多集群的资源管理和 pod 调度。
KubeFed 的资源调度总体上比较静态,需要创建 workload 之前确保各集群有充足资源。它的动态调度能力有限,因为只能对 Deployment/ReplicaSet 开启。当 KubeFed 下发 pod 到集群出现 pod pending 的情况时,动态调度的能力能够将 pod 迁移到其他集群。
Liqo 是能做到动态的资源发现和 pod 调度,但是它的方法是将一个集群抽象成一个工作节点。这种方式必然忽略了资源碎片的现象,使得虚拟节点的资源余量存在失真。比如集群内的 2 个节点共剩余 40 核可用资源,并不一定代表集群还能调度一个 40 核资源的 pod。但是从 Liqo 的方式来看,这个集群是能够调度 40 核 pod 的。并且目前来看,如果集群内出现 pod pending 的情况,也没法自动迁移 pod 到其他集群。
不过 Liqo 的整个思想是比较新颖的,通过对等的 P2P 方式将多个集群聚合成一个大的可用资源池。对于现有集群也不会引入一套新的 API。或许 Liqo 可以引入 descheduler 将 pending pod 迁移到其他机器来解决资源余量的误差问题。
参考资料
- A Brief History of Multicluster Kubernetes:https://www.tfir.io/a-brief-history-of-multicluster-kubernetes/
- Simplifying multi-clusters in Kubernetes:https://www.cncf.io/blog/2021/04/12/simplifying-multi-clusters-in-kubernetes/
- kubefed 简介 :https://jimmysong.io/kubernetes-handbook/practice/federation.html
- Unleashing the multi-cluster potential with Liqo:https://youtu.be/Ru-VrLcRXDg
- Virtual Kubelet:https://www.huweihuang.com/kubernetes-notes/virtual-kubelet/virtual-kubelet.html
引用链接
[1]KubeFed: https://github.com/kubernetes-sigs/kubefed
[2]Liqo: https://github.com/liqotech/liqo
[3]Virtual Kubelet: https://github.com/virtual-kubelet/virtual-kubelet
[4]Virtual Kubelet: https://www.huweihuang.com/kubernetes-notes/virtual-kubelet/virtual-kubelet.html