kubernetes - 如何在限制 GCP 成本的同时扩展 Kubernetes 集群
问题描述
我们在谷歌云平台上建立了一个 GKE 集群。
我们有一项需要“爆发”计算能力的活动。
想象一下,我们通常平均每小时进行 100 次计算,然后突然间我们需要能够在不到两分钟的时间内处理 100000 次。然而大多数时候,一切都接近空闲。
我们不想为 99% 的空闲服务器付费,并且希望根据实际使用来扩展集群(不需要数据持久化,服务器可以事后删除)。我查看了 kubernetes 上有关自动缩放的文档,用于使用HPA添加更多 pod 并使用集群自动缩放器添加更多节点
然而,似乎这些解决方案中的任何一个都不会真正降低我们的成本或提高性能,因为它们似乎无法超越 GCP 计划:
想象一下,我们有一个8 个 CPU的谷歌计划。我的理解是,如果我们使用集群自动缩放器添加更多节点,我们将不再拥有例如 2 个节点,每个节点使用 4 个 CPU,我们将拥有 4 个节点,每个节点使用 2 个 CPU。但总可用计算能力仍将是 8 个 CPU。同样的推理也适用于具有更多 pod 而不是更多节点的 HPA。
如果我们有 8 个 CPU 付费计划但只使用其中的 4 个,我的理解是我们仍然需要为 8 个 CPU 付费,因此缩减规模并没有真正的用处。
我们想要的是自动缩放以暂时更改我们的付款计划(想象从 n1-standard-8 到 n1-standard-16)并获得实际的新计算能力。
我不敢相信我们是唯一有这个用例的人,但我在任何地方都找不到任何文档!我是不是误会了什么?
解决方案
TL;博士:
GKE 定价:
- 来自GKE 定价:
自 2020 年 6 月 6 日起,GKE 将对每个集群每小时收取 0.10 美元的集群管理费。以下条件适用于集群管理费:
- 每个计费帐户 一个 地区集群是免费的。
- 无论集群大小和拓扑如何,费用都是固定的。
- 计费是按每个集群的每秒计算的。在每个月底,总金额四舍五入到最接近的分。
来自工作节点的定价:
GKE 将 Compute Engine 实例用于集群中的工作程序节点。您需要根据Compute Engine 的定价为每个实例付费,直到删除节点。Compute Engine 资源按秒计费,最低使用费用为一分钟。
进入,集群自动缩放器:
根据工作负载的需求自动调整 GKE 集群的节点池大小。当需求很高时,集群自动扩缩器会将节点添加到节点池中。当需求较低时,集群自动扩缩器会缩减到您指定的最小大小。这可以在您需要时提高工作负载的可用性,同时控制成本。
- Cluster Autoscaler 无法将整个集群缩放到零,集群中必须至少有一个节点始终可用以运行系统 Pod。
由于您已经有一个持久的工作负载,这不会是一个问题,我们要做的是创建一个新的节点池:
节点池是集群中的一组节点,它们都具有相同的配置。每个集群至少有一个 默认 节点池,但您可以根据需要添加其他节点池。
对于此示例,我将创建两个节点池:
- 一个具有固定大小的默认节点池,其中一个节点具有较小的实例大小(模拟您已经拥有的集群)。
- 具有更多计算能力的第二个节点池来运行作业(我称之为电源池)。
- 选择具有运行 AI 作业所需功能的机器类型,在此示例中,我将创建一个
n1-standard-8
. - 此电源池将设置自动缩放以允许最多 4 个节点,最少 0 个节点。
- 如果您想添加 GPU,您可以查看以下内容:Guide Scale to most zero + GPUs。
- 选择具有运行 AI 作业所需功能的机器类型,在此示例中,我将创建一个
污点和容忍度:
- 只有与 AI 工作负载相关的作业才会在电源池上运行,因为它使用作业 pod 中的节点选择器 来确保它们在电源池节点中运行。
- 设置反关联规则以确保您的两个训练 Pod 不能调度在同一个节点上(优化性价比,这取决于您的工作负载是可选的)。
- 向电源池添加污点,以避免在可自动缩放池上安排其他工作负载(和系统资源)。
- 将容忍度添加到 AI 作业中,让它们在这些节点上运行。
再生产:
- 使用持久默认池创建集群:
PROJECT_ID="YOUR_PROJECT_ID"
GCP_ZONE="CLUSTER_ZONE"
GKE_CLUSTER_NAME="CLUSTER_NAME"
AUTOSCALE_POOL="power-pool"
gcloud container clusters create ${GKE_CLUSTER_NAME} \
--machine-type="n1-standard-1" \
--num-nodes=1 \
--zone=${GCP_ZONE} \
--project=${PROJECT_ID}
- 创建自动缩放池:
gcloud container node-pools create ${GKE_BURST_POOL} \
--cluster=${GKE_CLUSTER_NAME} \
--machine-type=n1-standard-8 \
--node-labels=load=on-demand \
--node-taints=reserved-pool=true:NoSchedule \
--enable-autoscaling \
--min-nodes=0 \
--max-nodes=4 \
--zone=${GCP_ZONE} \
--project=${PROJECT_ID}
参数注意事项:
在这里您可以看到我们创建的两个池,具有 1 个节点的静态池和具有 0-4 个节点的可自动缩放池。
由于我们没有在可自动扩展的节点池上运行工作负载,因此它显示 0 个节点正在运行(并且在没有节点在执行时免费)。
- 现在我们将创建一个作业,创建 4 个运行 5 分钟的并行 pod。
- 该作业将具有以下参数以区别于普通 pod:
parallelism: 4
:使用所有 4 个节点来提高性能nodeSelector.load: on-demand
:分配给具有该标签的节点。podAntiAffinity
:声明我们不希望app: greedy-job
在同一个节点上运行两个具有相同标签的 pod(可选)。tolerations:
将容忍度与我们附加到节点的污点相匹配,因此允许在这些节点中调度这些 pod。
apiVersion: batch/v1
kind: Job
metadata:
name: greedy-job
spec:
parallelism: 4
template:
metadata:
name: greedy-job
labels:
app: greedy-app
spec:
containers:
- name: busybox
image: busybox
args:
- sleep
- "300"
nodeSelector:
load: on-demand
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- greedy-app
topologyKey: "kubernetes.io/hostname"
tolerations:
- key: reserved-pool
operator: Equal
value: "true"
effect: NoSchedule
restartPolicy: OnFailure
- 现在我们的集群处于待机状态,我们将使用我们刚刚创建的作业 yaml(我称之为 yaml
greedyjob.yaml
)。该作业将运行四个并行运行的进程,大约 5 分钟后完成。
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb Ready <none> 42m v1.14.10-gke.27
$ kubectl get pods
No resources found in default namespace.
$ kubectl apply -f greedyjob.yaml
job.batch/greedy-job created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
greedy-job-2xbvx 0/1 Pending 0 11s
greedy-job-72j8r 0/1 Pending 0 11s
greedy-job-9dfdt 0/1 Pending 0 11s
greedy-job-wqct9 0/1 Pending 0 11s
- 我们的工作已申请,但仍在等待中,让我们看看这些 pod 中发生了什么:
$ kubectl describe pod greedy-job-2xbvx
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 28s (x2 over 28s) default-scheduler 0/1 nodes are available: 1 node(s) didn't match node selector.
Normal TriggeredScaleUp 23s cluster-autoscaler pod triggered scale-up: [{https://content.googleapis.com/compute/v1/projects/owilliam/zones/us-central1-b/instanceGroups/gke-autoscale-to-zero-clus-power-pool-564148fd-grp 0->1 (max: 4)}]
- 由于我们定义的规则,无法在当前节点上调度 pod,这会在我们的电源池上触发 Scale Up 例程。这是一个非常动态的过程,90 秒后第一个节点启动并运行:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
greedy-job-2xbvx 0/1 Pending 0 93s
greedy-job-72j8r 0/1 ContainerCreating 0 93s
greedy-job-9dfdt 0/1 Pending 0 93s
greedy-job-wqct9 0/1 Pending 0 93s
$ kubectl nodes
NAME STATUS ROLES AGE VERSION
gke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb Ready <none> 44m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-qxkw Ready <none> 11s v1.14.10-gke.27
- 由于我们设置了 pod 反亲和规则,所以第二个 pod 无法调度到已经启动的节点上并触发下一次扩容,看一下第二个 pod 上的事件:
$ k describe pod greedy-job-2xbvx
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal TriggeredScaleUp 2m45s cluster-autoscaler pod triggered scale-up: [{https://content.googleapis.com/compute/v1/projects/owilliam/zones/us-central1-b/instanceGroups/gke-autoscale-to-zero-clus-power-pool-564148fd-grp 0->1 (max: 4)}]
Warning FailedScheduling 93s (x3 over 2m50s) default-scheduler 0/1 nodes are available: 1 node(s) didn't match node selector.
Warning FailedScheduling 79s (x3 over 83s) default-scheduler 0/2 nodes are available: 1 node(s) didn't match node selector, 1 node(s) had taints that the pod didn't tolerate.
Normal TriggeredScaleUp 62s cluster-autoscaler pod triggered scale-up: [{https://content.googleapis.com/compute/v1/projects/owilliam/zones/us-central1-b/instanceGroups/gke-autoscale-to-zero-clus-power-pool-564148fd-grp 1->2 (max: 4)}]
Warning FailedScheduling 3s (x3 over 68s) default-scheduler 0/2 nodes are available: 1 node(s) didn't match node selector, 1 node(s) didn't match pod affinity/anti-affinity, 1 node(s) didn't satisfy existing pods anti-affinity rules.
- 重复相同的过程,直到满足所有要求:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
greedy-job-2xbvx 0/1 Pending 0 3m39s
greedy-job-72j8r 1/1 Running 0 3m39s
greedy-job-9dfdt 0/1 Pending 0 3m39s
greedy-job-wqct9 1/1 Running 0 3m39s
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb Ready <none> 46m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-qxkw Ready <none> 2m16s v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-sf6q Ready <none> 28s v1.14.10-gke.27
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
greedy-job-2xbvx 0/1 Pending 0 5m19s
greedy-job-72j8r 1/1 Running 0 5m19s
greedy-job-9dfdt 1/1 Running 0 5m19s
greedy-job-wqct9 1/1 Running 0 5m19s
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb Ready <none> 48m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-39m2 Ready <none> 63s v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-qxkw Ready <none> 4m8s v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-sf6q Ready <none> 2m20s v1.14.10-gke.27
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
greedy-job-2xbvx 1/1 Running 0 6m12s
greedy-job-72j8r 1/1 Running 0 6m12s
greedy-job-9dfdt 1/1 Running 0 6m12s
greedy-job-wqct9 1/1 Running 0 6m12s
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb Ready <none> 48m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-39m2 Ready <none> 113s v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-ggxv Ready <none> 26s v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-qxkw Ready <none> 4m58s v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-sf6q Ready <none> 3m10s v1.14.10-gke.27
在这里我们可以看到所有节点现在都已启动并运行(因此,按秒计费)
- 现在所有作业都在运行,几分钟后作业完成它们的任务:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
greedy-job-2xbvx 1/1 Running 0 7m22s
greedy-job-72j8r 0/1 Completed 0 7m22s
greedy-job-9dfdt 1/1 Running 0 7m22s
greedy-job-wqct9 1/1 Running 0 7m22s
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
greedy-job-2xbvx 0/1 Completed 0 11m
greedy-job-72j8r 0/1 Completed 0 11m
greedy-job-9dfdt 0/1 Completed 0 11m
greedy-job-wqct9 0/1 Completed 0 11m
- 任务完成后,自动扩缩器开始缩小集群规模。
- 您可以在此处了解有关此过程规则的更多信息:GKE Cluster AutoScaler
$ while true; do kubectl get nodes ; sleep 60; done
NAME STATUS ROLES AGE VERSION
gke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb Ready <none> 54m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-39m2 Ready <none> 7m26s v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-ggxv Ready <none> 5m59s v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-qxkw Ready <none> 10m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-sf6q Ready <none> 8m43s v1.14.10-gke.27
NAME STATUS ROLES AGE VERSION
gke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb Ready <none> 62m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-39m2 Ready <none> 15m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-ggxv Ready <none> 14m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-qxkw Ready <none> 18m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-sf6q NotReady <none> 16m v1.14.10-gke.27
- 满足条件后,自动缩放器将节点标记为
NotReady
并开始删除它们:
NAME STATUS ROLES AGE VERSION
gke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb Ready <none> 64m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-39m2 NotReady <none> 17m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-ggxv NotReady <none> 16m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-qxkw Ready <none> 20m v1.14.10-gke.27
NAME STATUS ROLES AGE VERSION
gke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb Ready <none> 65m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-39m2 NotReady <none> 18m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-ggxv NotReady <none> 17m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-qxkw NotReady <none> 21m v1.14.10-gke.27
NAME STATUS ROLES AGE VERSION
gke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb Ready <none> 66m v1.14.10-gke.27
gke-autoscale-to-zero-clus-power-pool-564148fd-ggxv NotReady <none> 18m v1.14.10-gke.27
NAME STATUS ROLES AGE VERSION
gke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb Ready <none> 67m v1.14.10-gke.27
- 以下是节点已从 GKE 和 VM 中删除的确认信息(请记住,每个节点都是按计算引擎计费的虚拟机):
Compute Engine:(请注意,它来自另一个集群,我将其添加到屏幕截图中,以显示集群中除了默认的持久
节点之外gke-cluster-1-default-pool
没有其他节点。)gke-autoscale-to-zero
最后的想法:
缩减时,集群自动扩缩器会遵守 Pod 上设置的调度和驱逐规则。这些限制可以防止节点被自动缩放器删除。如果节点包含具有以下任何条件的 Pod,则可以防止删除该节点: 应用程序的PodDisruptionBudget也可以防止自动缩放;如果删除节点会导致超出预算,则集群不会缩减。
您可以注意到该过程非常快,在我们的示例中,升级一个节点大约需要 90 秒,完成一个备用节点的降级需要 5 分钟,从而极大地改善了您的计费。
- 抢占式虚拟机可以进一步减少您的计费,但您必须考虑您正在运行的工作负载类型:
抢占式虚拟机是最长持续 24 小时且不提供可用性保证的 Compute Engine虚拟机实例。抢占式虚拟机的价格低于标准 Compute Engine 虚拟机,并提供相同的机器类型和选项。
我知道您仍在为您的应用考虑最佳架构。
使用APP Engine和IA 平台也是最佳解决方案,但由于您目前正在 GKE 上运行工作负载,因此我想根据要求向您展示一个示例。
如果您有任何其他问题,请在评论中告诉我。
推荐阅读
- java - 为什么我的安卓应用无法连接到数据库?
- csv - 在 Cypress 中上传 csv 文件
- xamarin - Xamarin Page.Title 访问但不是作为字符串
- c# - 将规则集迁移到 editorconfig 后的问题
- ansible - 是否可以从 ansible.cfg 为 Ansible 禁用 ssl 验证?
- spring-boot - 如何使用现有的 yaml 文件在 SpringBoot 中公开 Swagger-ui 端点?
- r - 如何处理“pmap”错误和“安全”包装的函数的结果输出。不能取消嵌套
- powerbi - 使用从另一个表中选择的值过滤表
- c# - .Net Core Webapi 在 Automapper 配置文件上获取基地址 URL
- if-statement - groovy 脚本中的 if-else 和默认条件