在本博客中,我们将深入探讨 MyScale 如何在复制的集群中实现 Kubernetes(k8s)升级期间的零停机。MyScale 托管在 AWS 云上,并利用 Amazon EKS,提供弹性和可扩展的数据库即服务(DBaaS)。我们将探讨 MyScale 架构的全局和区域方面,解决自动扩展和 Kubernetes 升级的挑战,并概述我们的策略,以实现无缝用户集群迁移,同时确保服务不间断。
# 探索 MyScale 的架构
MyScale (opens new window) 是基于 AWS 云平台构建的数据库即服务(DBaaS)。所有服务都部署在 AWS 托管的 Kubernetes 服务 Amazon EKS (opens new window) 上,使 MyScale 能够充分利用 Kubernetes 的强大功能,如服务发现、负载均衡、自动扩展和安全隔离。
如下图所示,我们设计了一个高度弹性和可扩展的架构,包括一个全局平面和Web 服务 (opens new window)、用户管理、计费支付等。我们还在不同地理位置上部署了多个区域服务,每个区域服务提供相同的功能,如创建、管理、升级和销毁用户集群,以及使用情况监控和应用接口。
此外,每个区域服务包括一个单独的控制平面和多个数据平面。
每个区域的控制平面是其大脑,负责任务(或请求)管理和调度。它还作为数据平面与全局平面之间的链接,接受全局平面发出的管理请求,并将其传递给适当的数据平面执行。此外,该控制平面负责收集数据并监控每个数据平面的状态和详细使用指标,确保用户集群在数据平面上正确运行。
每个平面对应一个独立的 K8s 集群,在正常情况下,创建和维护这些集群涉及大量的云资源,特别是创建和配置 Kubernetes 集群。我们使用 Crossplane (opens new window) 来以统一的方式高效地管理基础设施、应用程序和用户集群,采用 Kubernetes 模式。
由于不同的用户集群规格,我们为每个单独的用户集群规格和工作负载创建了不同的 NodeGroup (opens new window),以最大化数据平面上的资源利用率。借助 Cluster Autoscaler (opens new window),我们实现了 Kubernetes 集群的自动扩展。
apiVersion: eks.aws.crossplane.io/v1alpha1
kind: NodeGroup
metadata:
name: <eks-nodegroup-name>
spec:
forProvider:
region: us-east-1
clusterNameRef:
name: <eks-cluster-name>
subnets:
- subnet-for-us-east-1a
- subnet-for-us-east-1b
- subnet-for-us-east-1c
labels:
myscale.com/workload: db
myscale.com/instance-type: <myscale-instance-type>
taints:
- key: myscale.com/workload
value: db
effect: NO_EXECUTE
instanceTypes:
- <instance-type>
- <instance-type>
scalingConfig:
maxSize: 100
minSize: 3
# 解决 Kubernetes 中的自动扩展挑战
最初使用 NodeGroup 的效果非常好,但随着用户集群规格的扩大,我们遇到了两个问题:
- NodeGroup 在创建、删除、启动和停止这些集群时,并没有按预期均匀分布在可用区,而是逐渐将用户集群集中在一个可用区中。
- 每个 NodeGroup 的最小大小(
minSize
)元素必须始终为非零。如果在某些情况下将其设置为零,例如当一个 Pod 使用现有的 PVC 时,自动扩展将不会触发,Pod 将永久处于挂起状态。
为了解决这些问题,我们在研究 Cluster Autoscaler 日志以及以下文档时发现了对这些现象的参考:
日志:
Found multiple availability zones for ASG "eks-nodegroup-xxxxxxx-90c4246a-215b-da2a-0ce8-c871017528ab"; using us-east-1a for failure-domain.beta.kubernetes.io/zone label
文档:
- https://aws.github.io/aws-eks-best-practices/cluster-autoscaling/#scaling-from-0 (opens new window)
- https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/README.md#auto-discovery-setup (opens new window)
根据参考文档中的最佳实践,我们修改了 NodeGroup 中原始的多可用区配置,将其表示为多个单可用区的 NodeGroup。此外,通过配置 ASG 标签实现了从零开始扩展。
以下是配置的 YAML 脚本示例:
apiVersion: eks.aws.crossplane.io/v1alpha1
kind: NodeGroup
metadata:
name: <eks-nodegroup-name-for-az1>
spec:
forProvider:
region: us-east-1
clusterNameRef:
name: <eks-cluster-name>
subnets:
- subnet-for-us-east-1a
labels:
myscale.com/workload: db
myscale.com/instance-type: <myscale-instance-type>
tags:
k8s.io/cluster-autoscaler/node-template/label/topology.kubernetes.io/region: us-east-1
k8s.io/cluster-autoscaler/node-template/label/topology.kubernetes.io/zone: us-east-1a
k8s.io/cluster-autoscaler/node-template/label/topology.ebs.csi.aws.com/zone: us-east-1a
k8s.io/cluster-autoscaler/node-template/taint/myscale.com/workload: db:NO_EXECUTE
k8s.io/cluster-autoscaler/node-template/label/myscale.com/workload: db
k8s.io/cluster-autoscaler/node-template/label/myscale.com/instance-type: <myscale-instance-type>
taints:
- key: myscale.com/workload
value: db
effect: NO_EXECUTE
instanceTypes:
- <instance-type>
- <instance-type>
scalingConfig:
maxSize: 100
minSize: 0
# 处理 Kubernetes 升级
由于 AWS 对 EKS 1.24 发布了弃用通知,我们很快面临升级 K8s 集群的任务,以在支持截止日期之前升级到最新版本。
根据 发布日历 (opens new window),AWS 将在新版本发布日期后的十四个月内提供对这些新版本的支持。
我们根据 AWS 提供的文档测试了他们的升级过程:
- https://docs.aws.amazon.com/eks/latest/userguide/update-cluster.html (opens new window)
- https://docs.aws.amazon.com/eks/latest/userguide/update-managed-node-group.html (opens new window)
- https://docs.aws.amazon.com/eks/latest/userguide/managed-node-update-behavior.html (opens new window)
在测试过程中,我们遇到了以下问题:
- 不可能同时升级多个版本,只能一次升级一个版本,一年内最多可以升级三个版本。
- NodeGroup 的升级时间相对较长。单个 NodeGroup 需要几十分钟来迁移或强制迁移 Pod。
频繁的版本升级,加上长时间的升级时间和强制 Pod 迁移,可能会导致现有网络连接异常中断,严重影响数据平面上的用户集群,并迫使用户关注 系统状态 (opens new window),以处理可能的问题。
为了解决这些挑战,我们研究了相关的 AWS EKS 文档,并找到了以下有用的信息:
- EKS 控制平面和 NodeGroup 可以相差一个版本。从 EKS 1.28 开始,允许相差两个版本。
- 在 EKS 控制平面升级后,可以创建一个与未升级的旧 NodeGroup 共存的具有控制平面版本的 NodeGroup。
基于这些要点,我们进行了以下更改:
- 我们在现有 NodeGroup 的配置中添加了一个
instance-version
标签:
labels:
myscale.com/instance-version: v1.24
- 然后,我们升级了 EKS 控制平面:
apiVersion: eks.aws.crossplane.io/v1beta1
kind: Cluster
metadata:
name: <eks-cluster-name>
spec:
forProvider:
version: "1.25"
完成 EKS 控制平面升级后,我们不会升级旧/现有的 NodeGroup。相反,我们基于升级后的 EKS 控制平面版本创建一个新的 NodeGroup,并添加一个标签来标记版本。
labels:
myscale.com/instance-version: v1.25
随后,我们在集群控制器中添加了以下配置,以支持在升级版本的过渡期间调度用户集群:
node_group:
default: v1.25
allowed:
- "v1.25"
- "v1.24"
现在,对于新创建的用户集群,或者当用户主动触发版本升级、重启等操作时,默认将其调度到标记为 v1.25 的 NodeGroup,而其他集群保持不变。我们解决了 EKS 升级期间的 Pod 迁移问题,对用户来说是透明的。
请注意,EKS 1.28 支持两个版本的偏差,对于 1.26 及以上版本,我们可以从 EKS 1.26 开始连续升级两个版本的 EKS 控制平面,从而将升级次数减少一半。
# 用户集群升级策略
然而,一些用户集群仍在旧的 v1.24 NodeGroup 上运行。
这些集群将何时迁移?
在讨论这个问题之前,让我们首先将用户集群分为以下几类:
- 多副本用户集群
- 单副本用户集群
多副本集群的副本位于不同可用区的多个节点上;新的用户请求可以通过负载均衡器将请求调度到其他 Pod,使得要迁移的 Pod 不再处理新的用户请求。一旦现有请求处理完毕,可以迁移该 Pod。通过重复此过程,可以完成多副本用户集群中所有 Pod 的迁移。
然而,单副本用户集群类型只包含一个 Pod;无法应用多副本用户集群的逻辑。
那么,我们如何处理这种迁移?我们能否参考和重用多副本用户集群的方法?
当 MyScale 的副本机制扩展集群并增加副本时,新创建的副本会自动与现有副本同步数据,完成数据同步后,多个副本同时提供服务。
因此,我们只需要配置要迁移的用户集群。
apiVersion: db.myscale.com/v1alpha1
kind: Cluster
metadata:
name: <cluster-name>
spec:
replicas: 1
shards: 1
......
在从单副本扩展到双副本之前进行迁移:
apiVersion: db.myscale.com/v1alpha1
kind: Cluster
metadata:
name: <cluster-name>
spec:
replicas: 2
shards: 1
......
然后,重用多副本用户集群的迁移逻辑进行迁移。迁移完成后,缩减规模并删除额外的 Pod。
最后,所有用户集群都已迁移完成。旧 NodeGroup 上的节点上的 Pod 已全部迁移。Cluster Autoscaler 会自动删除旧 NodeGroup 中的空节点。我们只需要检查并删除节点大小为零的 NodeGroup。此过程对用户没有任何影响,EKS 也完全升级到新版本。
# 总结
我们在解决这些架构挑战和升级过程中提高了 MyScale 的稳定性和效率,并确保在关键的 Kubernetes 升级期间实现了零停机。最后,这个过程反映了我们致力于为用户提供无缝、高性能的数据库即服务体验的承诺。