先发制人遇事不慌:Kubernetes 集群主动扩展

149 阅读8分钟

当集群资源不足时,Cluster Autoscaler 会提供新节点并将其加入集群。使用 Kubernetes 时你可能会注意到,创建节点并将其加入集群的过程可能需要花费数分钟。在这段时间里,应用程序很容易被连接淹没,因为已经无法进一步扩展了。

如何消除如此长的等待时间?

主动扩展(Proactive scaling),或者:

  • 理解集群 Autoscaler 的工作原理并最大限度提升其效用;
  • 使用 Kubernetes scheduler 为节点分配另一个 Pod;以及
  • 主动配置工作节点,以改善扩展效果。

注意:本文涉及的所有代码都已发布至 LearnK8s GitHub


延伸阅读,了解 Akamai cloud-computing

[出海云服务,选择 Akamai Linode!](www.akamai.com/zh/solution…&utm_id=APJCC2023)


Cluster Autoscaler 如何在 Kubernetes 中生效

Cluster Autoscaler 在触发自动扩展时并不检查内存或 CPU 的可用数,而是会对事件作出反应,检查所有不可调度的 Pod。当调度器找不到能容纳某个 Pod 的节点时,我们就说这个 Pod 是不可调度的。

我们可以这样创建一个集群来测试看看。

bash
$ linode-cli lke cluster-create \
 --label learnk8s \
 --region eu-west \
 --k8s_version 1.23 \
 --node_pools.count 1 \
 --node_pools.type g6-standard-2 \
 --node_pools.autoscaler.enabled enabled \
 --node_pools.autoscaler.max 10 \
 --node_pools.autoscaler.min 1 \
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig

请留意下列细节:

  • 每个节点有 4GB 内存和 2 个 vCPU(例如 “g6-standard-2” 实例)
  • 集群中只有一个节点,并且
  • Cluster autoscaler 被配置为从 1 个节点扩展至 10 个节点

我们可以用下列命令验证安装已成功完成:

bash
$ kubectl get pods -A --kubeconfig=kubeconfig

用环境变量导出 kubeconfig 文件通常是一种很方便的做法,为此我们可以运行:

bash
$ export KUBECONFIG=${PWD}/kubeconfig
$ kubectl get pods

部署应用程序

让我们部署一个需要 1GB 内存和 250m* CPU 的应用程序。

注意:m = 内核的千分之一容量,因此 250m = CPU 的 25% 容量。

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 1
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
   spec:
     containers:
       - name: podinfo
         image: stefanprodan/podinfo
         ports:
           - containerPort: 9898
         resources:
           requests:
             memory: 1G
             cpu: 250m

用下列命令将资源提交至集群:

bash
$ kubectl apply -f podinfo.yaml

随后很快会发现一些情况。首先,三个 Pod 几乎会立即开始运行,另有一个 Pod 处于 “未决” 状态。

 

随后很快:

  • 几分钟后,Autoscaler 创建了一个额外的 Pod,并且
  • 第四个 Pod 会被部署到一个新节点中。

第四个 Pod 为何没有部署到第一个节点中?让我们一起看看已分配的资源。

Kubernetes 节点中资源的分配

Kubernetes 集群中部署的 Pod 会消耗内存、CPU 以及存储资源。而且在同一个节点上,操作系统和 Kubelet 也需要消耗内存和 CPU

在 Kubernetes 工作节点上,内存和 CPU 会被拆分为:

  1. 运行操作系统和系统守护进程(如 SSH、Systemd 等)所需的资源。
  2. 运行 Kubernetes 代理程序(如 Kubelet、容器运行时以及节点故障检测程序等)所需的资源。
  3. 可用于 Pod 的资源。
  4. 为排空阈值(Eviction threshold)保留的资源。

 

如果集群运行了 DaemonSet(如 kube-proxy),那么可用内存和 CPU 数量还将进一步减少。

那么我们不妨降低需求,以确保能将所有 Pod 都放入同一个节点中:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 4
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
   spec:
     containers:
       - name: podinfo
         image: stefanprodan/podinfo
         ports:
           - containerPort: 9898
         resources:
           requests:
             memory: 0.8G # <- lower memory
             cpu: 200m    # <- lower CPU

我们可以使用下列命令修改这个部署:

bash
$ kubectl apply -f podinfo.yaml

选择恰当数量的 CPU 和内存以优化实例的运行,这是个充满挑战的工作。Learnk8s 计算器工具可以帮助我们更快速地完成这项工作。

一个问题解决了,但是创建新节点花费的时间呢?

迟早我们会需要四个以上的副本,我们是否真的需要等待好几分钟,随后才能创建新的 Pod?

简单来说:是的!Linode 必须从头开始创建和配置新虚拟机,随后将其连接到集群。这个过程经常会超过两分钟。

但其实还有替代方案:我们可以在需要时主动创建已经配置好的节点

例如:我们可以配置让 Autoscaler 始终准备好一个备用节点。当 Pod 被部署到备用节点后,Autoscaler 可以主动创建另一个备用节点。然而 Autoscaler 并没有内置这样的功能,但我们可以很容易地重新创建。

我们可以创建一个请求数与节点资源相等的 Pod:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.8G

用下列命令将资源提交至集群:

bash

kubectl apply -f placeholder.yaml

这个 Pod 完全不执行任何操作。

该节点的作用只是确保节点能够被充分使用起来。

随后还需要确保当工作负载需要扩展时,这个占位 Pod 能够被快速清除。为此我们可以使用 Priority Class

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
 name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     priorityClassName: overprovisioning # <--
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.8G

用下列命令将其提交至集群:

bash
kubectl apply -f placeholder.yaml

至此,配置工作已全部完成。

我们可能需要等待一会让 Autoscaler 创建节点,随后我们将有两个节点:

  1. 一个包含四个 Pod 的节点
  2. 一个包含一个占位 Pod 的节点

如果将部署扩展为 5 个副本会怎样?是否要等待 Autoscaler 创建另一个新节点?

用下列命令测试看看吧:

bash
kubectl scale deployment/podinfo --replicas=5

我们将会看到:

  1. 第五个 Pod 会立即创建出来,并在 10 秒内变为 “正在运行” 的状态。
  2. 占位 Pod 会被清除,以便为第五个 Pod 腾出空间。

随后:

  1. Cluster autoscaler 会注意到未决的占位 Pod 并配置一个新的节点。
  2. 占位 Pod 会被部署到新创建的节点中。

在可以有更多节点时,为何又要主动创建出一个节点?

我们可以将占位 Pod 扩展到多个副本,每个副本都会预配置一个 Kubernetes 节点,准备接受标准工作负载。然而这些节点虽然是闲置的,但它们产生的费用依然会计入云服务账单。因此一定要慎重,不要创建太多节点。

将 Cluster Autoscaler 与 Horizontal Pod Autoscaler 配合使用

为理解这项技术的含义,我们可以将 Cluster autoscaler 和 Horizontal Pod Autoscaler(HPA)结合在一起来看。HPA 可用于提高部署中的副本数量。

随着应用程序收到越来越多流量,我们可以让 Autoscaler 调整处理请求的副本数量。当 Pod 耗尽所有可用资源后,会触发 Cluster autoscaler 新建一个节点,这样 HPA 就可以继续创建更多副本。

可以这样新建一个集群来测试上述效果:

bash
$ linode-cli lke cluster-create \
 --label learnk8s-hpa \
 --region eu-west \
 --k8s_version 1.23 \
 --node_pools.count 1 \
 --node_pools.type g6-standard-2 \
 --node_pools.autoscaler.enabled enabled \
 --node_pools.autoscaler.max 10 \
 --node_pools.autoscaler.min 3 \
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig-hpa

用下列命令验证安装过程已成功完成:

bash
$ kubectl get pods -A --kubeconfig=kubeconfig-hpa

使用环境变量导出 kubeconfig 文件是一种方便的做法,为此我们可以运行:

bash
$ export KUBECONFIG=${PWD}/kubeconfig-hpa
$ kubectl get pods

接下来使用 Helm 安装 Prometheus 并查看该部署的相关指标。我们可以在官网上了解安装 Helm 的详细方法。

bash
$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
$ helm install prometheus prometheus-community/prometheus

Kubernetes 为 HPA 提供了一个控制器,借此可以动态增减副本数量。然而 HPA 也有一些局限性:

  1. 无法拆箱即用。需要安装 Metrics Server 来汇总并暴露出指标。
  2. PromQL 查询无法做到拆箱即用。

好在我们可以使用 KEDA,它通过一些实用功能(包括从 Prometheus 读取指标)扩展了 HPA 控制器的用法。KEDA 是一种 Autoscaler,可适用于下列三个组件:

  • Scaler
  • Metrics Adapter
  • Controller

 

我们可以通过 Helm 安装 KEDA:

bash
$ helm repo add kedacore https://kedacore.github.io/charts
$ helm install keda kedacore/keda

安装好 Prometheus 和 KEDA 之后,来创建一个部署吧。

在这个实验中,我们将使用一个每秒可以处理固定数量请求的应用。每个 Pod 每秒最多可以处理十个请求,如果 Pod 收到第 11 个请求,会将请求挂起,稍后再处理。

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 4
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
     annotations:
       prometheus.io/scrape: "true"
   spec:
     containers:
       - name: podinfo
         image: learnk8s/rate-limiter:1.0.0
         imagePullPolicy: Always
         args: ["/app/index.js", "10"]
         ports:
           - containerPort: 8080
         resources:
           requests:
             memory: 0.9G
---
apiVersion: v1
kind: Service
metadata:
 name: podinfo
spec:
 ports:
   - port: 80
     targetPort: 8080
 selector:
   app: podinfo

使用下列命令将资源提交至集群:

bash
$ kubectl apply -f rate-limiter.yaml

为了生成一些流量,我们可以使用 Locust。下列 YAML 定义将创建一个分布式负载测试集群:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
 name: locust-script
data:
 locustfile.py: |-
   from locust import HttpUser, task, between
   class QuickstartUser(HttpUser):
       @task
       def hello_world(self):
           self.client.get("/", headers={"Host": "example.com"})
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: locust
spec:
 selector:
   matchLabels:
     app: locust-primary
 template:
   metadata:
     labels:
       app: locust-primary
   spec:
     containers:
       - name: locust
         image: locustio/locust
         args: ["--master"]
         ports:
           - containerPort: 5557
             name: comm
           - containerPort: 5558
             name: comm-plus-1
           - containerPort: 8089
             name: web-ui
         volumeMounts:
           - mountPath: /home/locust
             name: locust-script
     volumes:
       - name: locust-script
         configMap:
           name: locust-script
---
apiVersion: v1
kind: Service
metadata:
 name: locust
spec:
 ports:
   - port: 5557
     name: communication
   - port: 5558
     name: communication-plus-1
   - port: 80
     targetPort: 8089
     name: web-ui
 selector:
   app: locust-primary
 type: LoadBalancer
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
 name: locust
spec:
 selector:
   matchLabels:
     app: locust-worker
 template:
   metadata:
     labels:
       app: locust-worker
   spec:
     containers:
       - name: locust
         image: locustio/locust
         args: ["--worker", "--master-host=locust"]
         volumeMounts:
           - mountPath: /home/locust
             name: locust-script
     volumes:
       - name: locust-script
         configMap:
           name: locust-script

运行下列命令将其提交至集群:

bash
$ kubectl locust.yaml

Locust 会读取下列 locustfile.py 文件,该文件存储在一个 ConfigMap 中:

py
from locust import HttpUser, task, between
class QuickstartUser(HttpUser):
   @task
   def hello_world(self):
       self.client.get("/")

该文件并没有什么特别的作用,只是向一个 URL 发出请求。若要连接至 Locust 仪表板,我们需要提供其负载均衡器的 IP 地址。为此可使用下列命令获取地址:

bash
$ kubectl get service locust -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

随后打开浏览器并访问该 IP 地址即可。

此外还需要注意一个问题:Horizontal Pod Autoscaler。KEDA autoscaler 会用一个名为 ScaledObject 的特殊对象来封装 Horizontal Autoscaler。

yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: podinfo
spec:
scaleTargetRef:
  kind: Deployment
  name: podinfo
minReplicaCount: 1
maxReplicaCount: 30
cooldownPeriod: 30
pollingInterval: 1
triggers:
- type: prometheus
  metadata:
    serverAddress: http://prometheus-server
    metricName: connections_active_keda
    query: |
      sum(increase(http_requests_total{app="podinfo"}[60s]))
    threshold: "480" # 8rps * 60s

KEDA 可以连接由 Prometheus 收集的指标,并将其发送给 Kubernetes。最后,它还将使用这些指标创建一个 Horizontal Pod Autoscaler (HPA)

我们可以用下列命令手工检查 HPA:

bash
$ kubectl get hpa
$ kubectl describe hpa keda-hpa-podinfo

并使用下列命令提交该对象:

bash
$ kubectl apply -f scaled-object.yaml

接下来可以测试扩展效果了。请在 Locust 仪表板中用下列设置启动一项实验:

可以看到,副本的数量增加了!

效果不错,但有个问题不知道你是否注意到。

当该部署扩展到 8 个 Pod 后,需要等待几分钟,随后才能在新节点中创建新的 Pod。在这段时间里,每秒处理的请求数量也不再增加了,因为当前的 8 个副本每个都只能处理 10 个请求。

让我们试试看收缩容量并重复该实验:

bash
kubectl scale deployment/podinfo --replicas=4 # or wait for the autoscaler to remove pods

这次,我们将用一个占位 Pod 实现超量配置(Overprovision):

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
 name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     priorityClassName: overprovisioning
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.9G

运行下列命令将其提交至集群:

bash
kubectl apply -f placeholder.yaml

打开 Locust 仪表板并用下列设置重复实验:

这一次,新节点将在后台创建,每秒请求数量将持续增减,不会原地踏步。很棒!

总结

本文介绍了下列内容:

  • Cluster autoscaler 并不追踪 CPU 或内存用量,而是会监控未决的 Pod。
  • 我们可以用可用内存和 CPU 的总量来创建一个 Pod,从而主动配置 Kubernetes 节点。
  • Kubernetes 节点会为 Kubelet、操作系统以及排空阈值保留一定的资源。
  • 我们可以结合使用 Prometheus 和 KEDA,从而通过 PromQL 查询扩展自己的 Pod。

这篇文章的内容感觉还行吧?有没有想要立即在 Linode 平台上亲自尝试一下?别忘了,现在注册可以免费获得价值 100 美元的使用额度,快点自己动手体验本文介绍的功能和服务吧↓↓↓

出海云服务,Akamai 是您的不二之选!

欢迎关注 Akamai ,第一时间了解高可用的 MySQL/MariaDB 参考架构,以及丰富的应用程序示例。