如何在限制 GCP 成本的同时扩展 Kubernetes 集群

Xav*_*kel 2 google-cloud-platform kubernetes google-kubernetes-engine

我们在google云平台上搭建了一个GKE集群。

我们有一项需要“爆发”计算能力的活动。

想象一下,我们通常平均每小时进行 100 次计算,然后突然我们需要能够在不到两分钟的时间内处理 100000 次计算。然而大多数时候,一切都接近闲置。

我们不想为 99% 的时间都闲置的服务器付费,并希望根据实际使用情况扩展集群(不需要数据持久化,之后可以删除服务器)。我查找了 kubernetes 上有关自动缩放的可用文档,以使用HPA添加更多 pod 并使用添加更多 pod 以及使用集群自动缩放器

然而,这些解决方案似乎都不会真正降低我们的成本或提高性能,因为它们的扩展似乎无法超出 GCP 计划:

想象一下,我们有一个带有 8 个 CPU 的google 计划。我的理解是,如果我们使用集群自动缩放器添加更多节点,我们将不再是每个使用 4 个 CPU 的 2 个节点,而是每个使用 2 个 CPU 的 4 个节点。但总可用计算能力仍为 8 个 CPU。对于具有更多 pod 而不是更多节点的 HPA,也有同样的推理。

如果我们有 8 个 CPU 付款计划,但只使用其中 4 个,我的理解是我们仍然需要按 8 个 CPU 付费,因此缩小规模并不是很有用。

我们想要的是自动缩放以暂时更改我们的付款计划(想象一下从 n1-standard-8 到 n1-standard-16)并获得实际的新计算能力。

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

Wil*_*.F. 5

长话短说:

\n\n\n\n
\n\n

GKE 定价:

\n\n
    \n
  • 来自GKE 定价:\n\n
    \n

    从 2020 年 6 月 6 日开始,GKE 将收取每个集群每小时 0.10 美元的集群管理费。集群管理费适用以下条件:

    \n\n
      \n
    • 每个计费帐户一个 区域集群免费的
    • \n
    • 无论集群大小和拓扑如何,费用都是固定的。
    • \n
    • 每个集群的计费是按秒计算的。每月月底,总额四舍五入到最接近的美分。
    • \n
    \n
  • \n
  • 来自工作节点的定价

    \n\n
    \n

    GKE 使用 Compute Engine 实例作为集群中的工作节点。您需要根据Compute Engine 的定价为每个实例付费,直到节点被删除为止。Compute Engine 资源按秒计费,一分钟最低使用成本。

    \n
  • \n
  • 输入,集群自动缩放器

    \n\n
    \n

    根据工作负载的需求自动调整 GKE 集群\xe2\x80\x99s 节点池的大小。当需求较高时,集群自动缩放程序会将节点添加到节点池中。当需求较低时,集群自动缩放程序会缩小到您指定的最小大小。这可以在您需要时提高工作负载的可用性,同时控制成本。

    \n
  • \n
\n\n
\n\n
    \n
  • Cluster Autoscaler 无法将整个集群缩放至零,集群中必须始终至少有一个节点可用才能运行系统 Pod。
  • \n
  • 由于您已经有持久的工作负载,这不会成为问题,我们要做的是创建一个新的节点池

    \n\n
    \n

    节点池是集群中具有相同配置的一组节点。每个集群至少有一个 默认 节点池,但您可以根据需要添加其他节点池。

    \n
  • \n
  • 对于此示例,我将创建两个节点池:

    \n\n
      \n
    • 一个默认节点池,具有固定大小的一个节点和较小的实例大小(模拟您已有的集群)。
    • \n
    • 第二个节点池具有更多计算能力来运行作业(我将其称为电源池)。\n\n
        \n
      • 选择具有运行 AI 作业所需功能的机器类型,在本例中,我将创建一个n1-standard-8.
      • \n
      • 该电源池将自动缩放设置为允许最多 4 个节点,最少 0 个节点。
      • \n
      • 如果你想添加 GPU,你可以查看这个很棒的内容:Guide Scale tomost Zero + GPUs
      • \n
    • \n
  • \n
\n\n

污染和容忍:

\n\n\n\n
\n\n

再生产:

\n\n
    \n
  • 使用持久默认池创建集群:
  • \n
\n\n
PROJECT_ID="YOUR_PROJECT_ID"  \nGCP_ZONE="CLUSTER_ZONE"  \nGKE_CLUSTER_NAME="CLUSTER_NAME"  \nAUTOSCALE_POOL="power-pool"  \n\ngcloud container clusters create ${GKE_CLUSTER_NAME} \\\n--machine-type="n1-standard-1" \\\n--num-nodes=1 \\\n--zone=${GCP_ZONE} \\\n--project=${PROJECT_ID}\n
Run Code Online (Sandbox Code Playgroud)\n\n
    \n
  • 创建自动缩放池:
  • \n
\n\n
gcloud container node-pools create ${GKE_BURST_POOL} \\\n--cluster=${GKE_CLUSTER_NAME} \\\n--machine-type=n1-standard-8 \\\n--node-labels=load=on-demand \\\n--node-taints=reserved-pool=true:NoSchedule \\\n--enable-autoscaling \\\n--min-nodes=0 \\\n--max-nodes=4 \\\n--zone=${GCP_ZONE} \\\n--project=${PROJECT_ID}\n
Run Code Online (Sandbox Code Playgroud)\n\n\n\n

在此输入图像描述

\n\n

由于我们没有在可自动扩展的节点池上运行工作负载,因此它显示 0 个节点正在运行(并且在没有节点执行时免费)。

\n\n
    \n
  • 现在我们将创建一个作业,该作业将创建 4 个运行 5 分钟的并行 Pod。\n\n
      \n
    • 该作业将具有以下参数以区别于普通 Pod:
    • \n
    • parallelism: 4:使用全部 4 个节点来增强性能
    • \n
    • nodeSelector.load: on-demand:分配给具有该标签的节点。
    • \n
    • podAntiAffinity:声明我们不希望两个具有相同标签的 Pod app: greedy-job 在同一节点中运行(可选)。
    • \n
    • tolerations:以使容忍度与我们附加到节点的污点相匹配,因此允许在这些节点中调度这些 pod。
    • \n
  • \n
\n\n
apiVersion: batch/v1  \nkind: Job  \nmetadata:  \n  name: greedy-job  \nspec:  \n  parallelism: 4  \n  template:  \n    metadata:  \n      name: greedy-job  \n      labels: \n        app: greedy-app  \n    spec:  \n      containers:  \n      - name: busybox  \n        image: busybox  \n        args:  \n        - sleep  \n        - "300"  \n      nodeSelector: \n        load: on-demand \n      affinity:  \n        podAntiAffinity:  \n          requiredDuringSchedulingIgnoredDuringExecution:  \n          - labelSelector:  \n              matchExpressions:  \n              - key: app  \n                operator: In  \n                values:  \n                - greedy-app  \n            topologyKey: "kubernetes.io/hostname"  \n      tolerations:  \n      - key: reserved-pool  \n        operator: Equal  \n        value: "true"  \n        effect: NoSchedule  \n      restartPolicy: OnFailure\n
Run Code Online (Sandbox Code Playgroud)\n\n
    \n
  • 现在我们的集群处于待机状态,我们将使用刚刚创建的作业 yaml(我将其称为greedyjob.yaml)。此作业将运行四个并行运行的进程,并将在大约 5 分钟后完成。
  • \n
\n\n
$ kubectl get nodes\nNAME                                                  STATUS   ROLES    AGE   VERSION\ngke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb   Ready    <none>   42m   v1.14.10-gke.27\n\n$ kubectl get pods\nNo resources found in default namespace.\n\n$ kubectl apply -f greedyjob.yaml \njob.batch/greedy-job created\n\n$ kubectl get pods\nNAME               READY   STATUS    RESTARTS   AGE\ngreedy-job-2xbvx   0/1     Pending   0          11s\ngreedy-job-72j8r   0/1     Pending   0          11s\ngreedy-job-9dfdt   0/1     Pending   0          11s\ngreedy-job-wqct9   0/1     Pending   0          11s\n
Run Code Online (Sandbox Code Playgroud)\n\n
    \n
  • 我们的工作已申请,但仍在等待中,让我们看看这些 Pod 中发生了什么:
  • \n
\n\n
$ kubectl describe pod greedy-job-2xbvx\n...\nEvents:\n  Type     Reason            Age                From                Message\n  ----     ------            ----               ----                -------\n  Warning  FailedScheduling  28s (x2 over 28s)  default-scheduler   0/1 nodes are available: 1 node(s) didn\'t match node selector.\n  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)}]\n
Run Code Online (Sandbox Code Playgroud)\n\n
    \n
  • 由于我们定义的规则,无法在当前节点上调度 Pod,这会触发我们的电源池上的扩展例程。这是一个非常动态的过程,90 秒后第一个节点启动并运行:
  • \n
\n\n
$ kubectl get pods\nNAME               READY   STATUS              RESTARTS   AGE\ngreedy-job-2xbvx   0/1     Pending             0          93s\ngreedy-job-72j8r   0/1     ContainerCreating   0          93s\ngreedy-job-9dfdt   0/1     Pending             0          93s\ngreedy-job-wqct9   0/1     Pending             0          93s\n\n$ kubectl nodes\nNAME                                                  STATUS   ROLES    AGE   VERSION\ngke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb   Ready    <none>   44m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-qxkw   Ready    <none>   11s   v1.14.10-gke.27\n
Run Code Online (Sandbox Code Playgroud)\n\n
    \n
  • 由于我们设置了 pod 反亲和规则,导致第二个 pod 无法调度到已启动的节点上并触发下一次扩容,看一下第二个 pod 上的事件:
  • \n
\n\n
$ k describe pod greedy-job-2xbvx\n...\nEvents:\n  Type     Reason            Age                  From                Message\n  ----     ------            ----                 ----                -------\n  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)}]\n  Warning  FailedScheduling  93s (x3 over 2m50s)  default-scheduler   0/1 nodes are available: 1 node(s) didn\'t match node selector.\n  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.\n  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)}]\n  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.\n
Run Code Online (Sandbox Code Playgroud)\n\n
    \n
  • 重复相同的过程,直到满足所有要求:
  • \n
\n\n
$ kubectl get pods\nNAME               READY   STATUS    RESTARTS   AGE\ngreedy-job-2xbvx   0/1     Pending   0          3m39s\ngreedy-job-72j8r   1/1     Running   0          3m39s\ngreedy-job-9dfdt   0/1     Pending   0          3m39s\ngreedy-job-wqct9   1/1     Running   0          3m39s\n\n$ kubectl get nodes\nNAME                                                  STATUS   ROLES    AGE     VERSION\ngke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb   Ready    <none>   46m     v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-qxkw   Ready    <none>   2m16s   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-sf6q   Ready    <none>   28s     v1.14.10-gke.27\n\n$ kubectl get pods\nNAME               READY   STATUS    RESTARTS   AGE\ngreedy-job-2xbvx   0/1     Pending   0          5m19s\ngreedy-job-72j8r   1/1     Running   0          5m19s\ngreedy-job-9dfdt   1/1     Running   0          5m19s\ngreedy-job-wqct9   1/1     Running   0          5m19s\n\n$ kubectl get nodes\nNAME                                                  STATUS   ROLES    AGE     VERSION\ngke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb   Ready    <none>   48m     v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-39m2   Ready    <none>   63s     v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-qxkw   Ready    <none>   4m8s    v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-sf6q   Ready    <none>   2m20s   v1.14.10-gke.27\n\n$ kubectl get pods\nNAME               READY   STATUS    RESTARTS   AGE\ngreedy-job-2xbvx   1/1     Running   0          6m12s\ngreedy-job-72j8r   1/1     Running   0          6m12s\ngreedy-job-9dfdt   1/1     Running   0          6m12s\ngreedy-job-wqct9   1/1     Running   0          6m12s\n\n$ kubectl get nodes\nNAME                                                  STATUS   ROLES    AGE     VERSION\ngke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb   Ready    <none>   48m     v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-39m2   Ready    <none>   113s    v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-ggxv   Ready    <none>   26s     v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-qxkw   Ready    <none>   4m58s   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-sf6q   Ready    <none>   3m10s   v1.14.10-gke.27\n
Run Code Online (Sandbox Code Playgroud)\n\n

在此输入图像描述\n在此输入图像描述\n在这里我们可以看到所有节点现在都已启动并正在运行(因此,按秒计费)

\n\n
    \n
  • 现在所有作业都在运行,几分钟后作业完成其任务:
  • \n
\n\n
$ kubectl get pods\nNAME               READY   STATUS      RESTARTS   AGE\ngreedy-job-2xbvx   1/1     Running     0          7m22s\ngreedy-job-72j8r   0/1     Completed   0          7m22s\ngreedy-job-9dfdt   1/1     Running     0          7m22s\ngreedy-job-wqct9   1/1     Running     0          7m22s\n\n$ kubectl get pods\nNAME               READY   STATUS      RESTARTS   AGE\ngreedy-job-2xbvx   0/1     Completed   0          11m\ngreedy-job-72j8r   0/1     Completed   0          11m\ngreedy-job-9dfdt   0/1     Completed   0          11m\ngreedy-job-wqct9   0/1     Completed   0          11m\n
Run Code Online (Sandbox Code Playgroud)\n\n
    \n
  • 任务完成后,自动缩放程序开始缩小集群规模。
  • \n
  • 您可以在此处详细了解此过程的规则:GKE Cluster AutoScaler
  • \n
\n\n
$ while true; do kubectl get nodes ; sleep 60; done\nNAME                                                  STATUS   ROLES    AGE     VERSION\ngke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb   Ready    <none>   54m     v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-39m2   Ready    <none>   7m26s   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-ggxv   Ready    <none>   5m59s   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-qxkw   Ready    <none>   10m     v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-sf6q   Ready    <none>   8m43s   v1.14.10-gke.27\n\nNAME                                                  STATUS     ROLES    AGE   VERSION\ngke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb   Ready      <none>   62m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-39m2   Ready      <none>   15m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-ggxv   Ready      <none>   14m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-qxkw   Ready      <none>   18m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-sf6q   NotReady   <none>   16m   v1.14.10-gke.27\n
Run Code Online (Sandbox Code Playgroud)\n\n
    \n
  • 一旦满足条件,自动缩放器就会将该节点标记为NotReady并开始删除它们:
  • \n
\n\n
NAME                                                  STATUS     ROLES    AGE   VERSION\ngke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb   Ready      <none>   64m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-39m2   NotReady   <none>   17m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-ggxv   NotReady   <none>   16m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-qxkw   Ready      <none>   20m   v1.14.10-gke.27\n\nNAME                                                  STATUS     ROLES    AGE   VERSION\ngke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb   Ready      <none>   65m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-39m2   NotReady   <none>   18m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-ggxv   NotReady   <none>   17m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-qxkw   NotReady   <none>   21m   v1.14.10-gke.27\n\nNAME                                                  STATUS     ROLES    AGE   VERSION\ngke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb   Ready      <none>   66m   v1.14.10-gke.27\ngke-autoscale-to-zero-clus-power-pool-564148fd-ggxv   NotReady   <none>   18m   v1.14.10-gke.27\n\nNAME                                                  STATUS   ROLES    AGE   VERSION\ngke-autoscale-to-zero-cl-default-pool-9f6d80d3-x9lb   Ready    <none>   67m   v1.14.10-gke.27\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n
    \n
  • 以下是节点已从 GKE 和虚拟机中删除的确认信息(请记住,每个节点都是一个作为计算引擎计费的虚拟机):
  • \n
\n\n

计算引擎:(请注意,这是来自另一个集群,我将其添加到屏幕截图中是为了向您展示除了默认的持久节点之外,gke-cluster-1-default-pool集群中没有其他节点。)\ngke-autoscale-to-zero在此输入图像描述

\n\n

GKE:\n在此输入图像描述

\n\n
\n\n

最后的想法:

\n\n
\n

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

\n
\n\n

您可以注意到,这个过程非常快,在我们的示例中,升级节点大约需要 90 秒,完成缩减备用节点需要 5 分钟,这为您的计费带来了巨大的改善。

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

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

\n
\n\n

我知道您仍在考虑最适合您的应用程序的架构。

\n\n

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

\n\n

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

\n