首页 > 解决方案 > 如何在限制 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)并获得实际的新计算能力。

我不敢相信我们是唯一有这个用例的人,但我在任何地方都找不到任何文档!我是不是误会了什么?

标签: kubernetesgoogle-cloud-platformgoogle-kubernetes-engine

解决方案


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 工作负载相关的作业才会在电源池上运行,因为它使用作业 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}
  • 参数注意事项:

    • --node-labels=load=on-demand:为电源池中的节点添加标签,以允许在我们的 AI 作业中使用节点选择器选择它们。
    • --node-taints=reserved-pool=true:NoSchedule:向节点添加 污点 ,以防止任何其他工作负载意外调度到此节点池中。
  • 在这里您可以看到我们创建的两个池,具有 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在此处输入图像描述

GKE: 在此处输入图像描述


最后的想法:

缩减时,集群自动扩缩器会遵守 Pod 上设置的调度和驱逐规则。这些限制可以防止节点被自动缩放器删除。如果节点包含具有以下任何条件的 Pod,则可以防止删除该节点: 应用程序的PodDisruptionBudget也可以防止自动缩放;如果删除节点会导致超出预算,则集群不会缩减。

您可以注意到该过程非常快,在我们的示例中,升级一个节点大约需要 90 秒,完成一个备用节点的降级需要 5 分钟,从而极大地改善了您的计费。

  • 抢占式虚拟机可以进一步减少您的计费,但您必须考虑您正在运行的工作负载类型:

抢占式虚拟机是最长持续 24 小时且不提供可用性保证的 Compute Engine虚拟机实例。抢占式虚拟机的价格低于标准 Compute Engine 虚拟机,并提供相同的机器类型和选项。

我知道您仍在为您的应用考虑最佳架构。

使用APP EngineIA 平台也是最佳解决方案,但由于您目前正在 GKE 上运行工作负载,因此我想根据要求向您展示一个示例。

如果您有任何其他问题,请在评论中告诉我。


推荐阅读