根据指标优化 Pod 自动扩缩


本教程演示如何根据 Cloud Monitoring 中提供的指标自动扩缩 Google Kubernetes Engine (GKE) 工作负载。

在本教程中,您可以根据以下四个不同指标之一设置自动扩缩:

CPU

CPU 利用率

根据 CPU 在各节点之间的利用率百分比进行扩缩。此指标经济高效,从而最大限度地提高 CPU 资源利用率。不过,由于 CPU 使用率是一个尾随指标,因此您的用户可能会在纵向扩容过程中遇到延迟。

Pub/Sub

Pub/Sub 积压

根据 Pub/Sub 订阅中剩余的未确认消息的数量进行扩缩。此指标在出现问题之前可有效缩短延迟时间,但与基于 CPU 利用率的自动扩缩相比,所使用的资源可能相对较多。

自定义指标

自定义 Cloud Monitoring 指标

根据 Cloud Monitoring 客户端库导出的自定义用户定义指标进行扩缩。如需了解详情,请参阅 Cloud Monitoring 文档中的创建自定义指标

自定义 Prometheus

自定义 Prometheus 指标

根据以 Prometheus 格式导出的自定义用户定义指标进行扩缩。Prometheus 指标必须是采样平均值类型,并且不得包含 custom.googleapis.com 前缀。

自动扩缩从根本上来说就是在费用和延迟之间找到可接受的平衡。您可能希望组合试用这些指标和其他指标以找到适合您的政策。

目标

本教程介绍了以下任务:

  1. 如何部署自定义指标适配器
  2. 如何从应用代码中导出指标。
  3. 如何在 Cloud Monitoring 界面上查看指标。
  4. 如何部署 HorizontalPodAutoscaler (HPA) 资源,以根据 Cloud Monitoring 指标扩缩您的应用。

费用

在本文档中,您将使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用情况来估算费用。 Google Cloud 新用户可能有资格申请免费试用

完成本文档中描述的任务后,您可以通过删除所创建的资源来避免继续计费。如需了解详情,请参阅清理

准备工作

请按照以下步骤启用 Kubernetes Engine API:
  1. 访问 Google Cloud 控制台中的 Kubernetes Engine 页面
  2. 创建或选择项目。
  3. 稍作等待,让 API 和相关服务完成启用过程。 此过程可能耗时几分钟。
  4. 确保您的 Google Cloud 项目已启用结算功能

您可以使用 Cloud Shell 来执行本教程中所述的操作,该环境中预装了本教程中用到的 gcloudkubectl 命令行工具。如果使用 Cloud Shell,则无需在工作站上安装这些命令行工具。

如需使用 Cloud Shell,请执行以下操作:

  1. 前往 Google Cloud 控制台
  2. 点击 Google Cloud 控制台窗口顶部的激活 Cloud Shell 激活 Shell 按钮 按钮。

    一个 Cloud Shell 会话随即会在 Google Cloud 控制台底部的新框内打开,并显示命令行提示符。

    Cloud Shell 会话

设置您的环境

  1. 设置 Google Cloud CLI 的默认可用区:

    gcloud config set compute/zone zone
    

    替换以下内容:

    • zone:选择离您最近的区域。如需了解详情,请参阅区域和可用区
  2. PROJECT_ID 环境变量设置为您的 Google Cloud 项目 ID (project-id):

    export PROJECT_ID=project-id
    
  3. 设置 Google Cloud CLI 的默认可用区:

    gcloud config set project $PROJECT_ID
    
  4. 创建 GKE 集群

    gcloud container clusters create metrics-autoscaling
    

部署自定义指标适配器

自定义指标适配器可让您的集群使用 Cloud Monitoring 发送和接收指标。

CPU

不适用:Pod 横向自动扩缩器可以根据 CPU 利用率本身进行扩缩,因此不需要自定义指标适配器。

Pub/Sub

为您的用户授予创建所需授权角色的权限:

kubectl create clusterrolebinding cluster-admin-binding \
    --clusterrole cluster-admin --user "$(gcloud config get-value account)"

在您的集群上部署新资源模型适配器:

kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/k8s-stackdriver/master/custom-metrics-stackdriver-adapter/deploy/production/adapter_new_resource_model.yaml

自定义指标

为您的用户授予创建所需授权角色的权限:

kubectl create clusterrolebinding cluster-admin-binding \
    --clusterrole cluster-admin --user "$(gcloud config get-value account)"

在您的集群上部署资源模型适配器:

kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/k8s-stackdriver/master/custom-metrics-stackdriver-adapter/deploy/production/adapter_new_resource_model.yaml

自定义 Prometheus

为您的用户授予创建所需授权角色的权限:

kubectl create clusterrolebinding cluster-admin-binding \
    --clusterrole cluster-admin --user "$(gcloud config get-value account)"

在您的集群上部署旧资源模型适配器:

kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/k8s-stackdriver/master/custom-metrics-stackdriver-adapter/deploy/production/adapter.yaml

部署具有指标的应用

下载包含本教程使用的应用代码的代码库:

CPU

git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples.git
cd kubernetes-engine-samples/quickstarts/hello-app

Pub/Sub

git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples.git
cd kubernetes-engine-samples/databases/cloud-pubsub

自定义指标

git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples.git
cd kubernetes-engine-samples/observability/custom-metrics-autoscaling/direct-to-sd

自定义 Prometheus

git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples.git
cd kubernetes-engine-samples/observability/custom-metrics-autoscaling/prometheus-to-sd

该代码库包含用于将指标导出到 Cloud Monitoring 的代码:

CPU

此应用会对端口 8080 上的任何 Web 请求响应“Hello, world!”。Compute Engine CPU 指标由 Cloud Monitoring 自动收集。

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	// register hello function to handle all requests
	mux := http.NewServeMux()
	mux.HandleFunc("/", hello)

	// use PORT environment variable, or default to 8080
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	// start the web server on port and accept requests
	log.Printf("Server listening on port %s", port)
	log.Fatal(http.ListenAndServe(":"+port, mux))
}

// hello responds to the request with a plain-text "Hello, world" message.
func hello(w http.ResponseWriter, r *http.Request) {
	log.Printf("Serving request: %s", r.URL.Path)
	host, _ := os.Hostname()
	fmt.Fprintf(w, "Hello, world!\n")
	fmt.Fprintf(w, "Version: 1.0.0\n")
	fmt.Fprintf(w, "Hostname: %s\n", host)
}

Pub/Sub

此应用会轮询 Pub/Sub 订阅以获取新消息,并在消息到达时对其进行确认。Pub/Sub 订阅指标由 Cloud Monitoring 自动收集。

from google import auth
from google.cloud import pubsub_v1

def main():
    """Continuously pull messages from subsciption"""

    # read default project ID
    _, project_id = auth.default()
    subscription_id = 'echo-read'

    subscriber = pubsub_v1.SubscriberClient()
    subscription_path = subscriber.subscription_path(
        project_id, subscription_id)

    def callback(message: pubsub_v1.subscriber.message.Message) -> None:
        """Process received message"""
        print(f"Received message: ID={message.message_id} Data={message.data}")
        print(f"[{datetime.datetime.now()}] Processing: {message.message_id}")
        time.sleep(3)
        print(f"[{datetime.datetime.now()}] Processed: {message.message_id}")
        message.ack()

    streaming_pull_future = subscriber.subscribe(
        subscription_path, callback=callback)
    print(f"Pulling messages from {subscription_path}...")

    with subscriber:
        try:
            streaming_pull_future.result()
        except Exception as e:
            print(e)

自定义指标

此应用会使用 Cloud Monitoring 客户端库导出常量值指标。

func exportMetric(stackdriverService *monitoring.Service, metricName string,
	metricValue int64, metricLabels map[string]string, monitoredResource string, resourceLabels map[string]string) error {
	dataPoint := &monitoring.Point{
		Interval: &monitoring.TimeInterval{
			EndTime: time.Now().Format(time.RFC3339),
		},
		Value: &monitoring.TypedValue{
			Int64Value: &metricValue,
		},
	}
	// Write time series data.
	request := &monitoring.CreateTimeSeriesRequest{
		TimeSeries: []*monitoring.TimeSeries{
			{
				Metric: &monitoring.Metric{
					Type:   "custom.googleapis.com/" + metricName,
					Labels: metricLabels,
				},
				Resource: &monitoring.MonitoredResource{
					Type:   monitoredResource,
					Labels: resourceLabels,
				},
				Points: []*monitoring.Point{
					dataPoint,
				},
			},
		},
	}
	projectName := fmt.Sprintf("projects/%s", resourceLabels["project_id"])
	_, err := stackdriverService.Projects.TimeSeries.Create(projectName, request).Do()
	return err
}

自定义 Prometheus

此应用会使用 Prometheus 格式导出常量值指标。

metric := prometheus.NewGauge(
	prometheus.GaugeOpts{
		Name: *metricName,
		Help: "Custom metric",
	},
)
prometheus.MustRegister(metric)
metric.Set(float64(*metricValue))

http.Handle("/metrics", promhttp.Handler())
log.Printf("Starting to listen on :%d", *port)
err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil)

代码库还包含一个 Kubernetes 清单,用于将应用部署到您的集群:

CPU

apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloweb
  labels:
    app: hello
spec:
  selector:
    matchLabels:
      app: hello
      tier: web
  template:
    metadata:
      labels:
        app: hello
        tier: web
    spec:
      containers:
      - name: hello-app
        image: us-docker.pkg.dev/google-samples/containers/gke/hello-app:1.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 200m

Pub/Sub

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pubsub
spec:
  selector:
    matchLabels:
      app: pubsub
  template:
    metadata:
      labels:
        app: pubsub
    spec:
      volumes:
      - name: google-cloud-key
        secret:
          secretName: pubsub-key
      containers:
      - name: subscriber
        image: us-docker.pkg.dev/google-samples/containers/gke/pubsub-sample:v2
        volumeMounts:
        - name: google-cloud-key
          mountPath: /var/secrets/google
        env:
        - name: GOOGLE_APPLICATION_CREDENTIALS
          value: /var/secrets/google/key.json

自定义指标

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    run: custom-metric-sd
  name: custom-metric-sd
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      run: custom-metric-sd
  template:
    metadata:
      labels:
        run: custom-metric-sd
    spec:
      containers:
      - command: ["./sd-dummy-exporter"]
        args:
        - --use-new-resource-model=true
        - --use-old-resource-model=false
        - --metric-name=custom-metric
        - --metric-value=40
        - --pod-name=$(POD_NAME)
        - --namespace=$(NAMESPACE)
        image: us-docker.pkg.dev/google-samples/containers/gke/sd-dummy-exporter:v0.3.0
        name: sd-dummy-exporter
        resources:
          requests:
            cpu: 100m
        env:
        # save Kubernetes metadata as environment variables for use in metrics
        - name: POD_NAME
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: metadata.name
        - name: NAMESPACE
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace

自定义 Prometheus

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    run: custom-metric-prometheus-sd
  name: custom-metric-prometheus-sd
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      run: custom-metric-prometheus-sd
  template:
    metadata:
      labels:
        run: custom-metric-prometheus-sd
    spec:
      containers:
      # sample container generating custom metrics
      - name: prometheus-dummy-exporter
        image: us-docker.pkg.dev/google-samples/containers/gke/prometheus-dummy-exporter:v0.2.0
        command: ["./prometheus-dummy-exporter"]
        args:
        - --metric-name=custom_prometheus
        - --metric-value=40
        - --port=8080
      # pre-built 'prometheus-to-sd' sidecar container to export prometheus
      # metrics to Stackdriver
      - name: prometheus-to-sd
        image: gcr.io/google-containers/prometheus-to-sd:v0.5.0
        command: ["/monitor"]
        args:
        - --source=:http://localhost:8080
        - --stackdriver-prefix=custom.googleapis.com
        - --pod-id=$(POD_ID)
        - --namespace-id=$(POD_NAMESPACE)
        env:
        # save Kubernetes metadata as environment variables for use in metrics
        - name: POD_ID
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: metadata.uid
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace

将应用部署到您的集群:

CPU

kubectl apply -f manifests/helloweb-deployment.yaml

Pub/Sub

在您的项目上启用 Pub/Sub API:

gcloud services enable cloudresourcemanager.googleapis.com pubsub.googleapis.com

创建 Pub/Sub 主题和订阅:

gcloud pubsub topics create echo
gcloud pubsub subscriptions create echo-read --topic=echo

创建拥有 Pub/Sub 访问权限的服务账号:

gcloud iam service-accounts create autoscaling-pubsub-sa
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member "serviceAccount:autoscaling-pubsub-sa@$PROJECT_ID.iam.gserviceaccount.com" \
  --role "roles/pubsub.subscriber"

下载服务账号密钥文件:

gcloud iam service-accounts keys create key.json \
  --iam-account autoscaling-pubsub-sa@$PROJECT_ID.iam.gserviceaccount.com

将服务账号密钥作为 Secret 导入到您的集群:

kubectl create secret generic pubsub-key --from-file=key.json=./key.json

将应用部署到您的集群:

kubectl apply -f deployment/pubsub-with-secret.yaml

自定义指标

kubectl apply -f custom-metrics-sd.yaml

自定义 Prometheus

kubectl apply -f custom-metrics-prometheus-sd.yaml

等待应用部署后,所有 Pod 都达到了 Ready 状态:

CPU

kubectl get pods

输出:

NAME                        READY   STATUS    RESTARTS   AGE
helloweb-7f7f7474fc-hzcdq   1/1     Running   0          10s

Pub/Sub

kubectl get pods

输出:

NAME                     READY   STATUS    RESTARTS   AGE
pubsub-8cd995d7c-bdhqz   1/1     Running   0          58s

自定义指标

kubectl get pods

输出:

NAME                                READY   STATUS    RESTARTS   AGE
custom-metric-sd-58dbf4ffc5-tm62v   1/1     Running   0          33s

自定义 Prometheus

kubectl get pods

输出:

NAME                                           READY   STATUS    RESTARTS   AGE
custom-metric-prometheus-sd-697bf7c7d7-ns76p   2/2     Running   0          49s

在 Cloud Monitoring 上查看指标

应用在运行时,会将您的指标写入 Cloud Monitoring。

如需使用 Metrics Explorer 的预览版界面查看受监控资源的指标,请执行以下操作:

  1. 在 Google Cloud 控制台中,选择 Monitoring,然后选择  Metrics Explorer,或点击以下按钮:

    转到 Metrics Explorer

  2. 指标元素中,展开选择指标菜单,然后选择资源类型和指标类型。例如,如需绘制虚拟机的 CPU 利用率图表,请执行以下操作:
    1. (可选)如需减少显示的菜单选项,请在过滤栏中输入部分指标名称。在此示例中,请输入 utilization
    2. 活跃资源菜单中,选择虚拟机实例
    3. 活跃指标类别菜单中,选择实例
    4. 活跃指标菜单中,选择 CPU 利用率,然后点击应用
  3. 如需过滤显示的时序,请使用过滤条件元素

  4. 如需组合时序,请使用聚合元素上的菜单。例如,如需根据虚拟机所在的可用区显示虚拟机的 CPU 利用率,请将第一个菜单设置为平均值并将第二个菜单设置为可用区

    聚合元素的第一个菜单设置为未聚合时,系统会显示所有时序。聚合元素的默认设置由您选择的指标类型决定。

资源类型和指标如下所示:

CPU

指标浏览器

资源类型:gce_instance

指标:compute.googleapis.com/instance/cpu/utilization

Pub/Sub

指标浏览器

资源类型:pubsub_subscription

指标:pubsub.googleapis.com/subscription/num_undelivered_messages

自定义指标

指标浏览器

资源类型:k8s_pod

指标:custom.googleapis.com/custom-metric

自定义 Prometheus

指标浏览器

资源类型:gke_container

指标:custom.googleapis.com/custom_prometheus

创建 HorizontalPodAutoscaler 对象

在 Cloud Monitoring 中看到指标后,您可以部署 HorizontalPodAutoscaler 以根据指标调整 Deployment 的大小。

CPU

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: cpu
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: helloweb
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 30

Pub/Sub

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: pubsub
spec:
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - external:
      metric:
       name: pubsub.googleapis.com|subscription|num_undelivered_messages
       selector:
         matchLabels:
           resource.labels.subscription_id: echo-read
      target:
        type: AverageValue
        averageValue: 2
    type: External
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: pubsub

自定义指标

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: custom-metric-sd
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: custom-metric-sd
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Pods
    pods:
      metric:
        name: custom-metric
      target:
        type: AverageValue
        averageValue: 20

自定义 Prometheus

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: custom-prometheus-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: custom-metric-prometheus-sd
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Pods
    pods:
      metric:
        name: custom_prometheus
      target:
        type: AverageValue
        averageValue: 20

HorizontalPodAutoscaler 部署到您的集群:

CPU

kubectl apply -f manifests/helloweb-hpa.yaml

Pub/Sub

kubectl apply -f deployment/pubsub-hpa.yaml

自定义指标

kubectl apply -f custom-metrics-sd-hpa.yaml

自定义 Prometheus

kubectl apply -f custom-metrics-prometheus-sd-hpa.yaml

生成负载

对于某些指标,您可能需要生成负载以监控自动扩缩:

CPU

模拟发送到 helloweb 服务器的 10,000 个请求:

 kubectl exec -it deployments/helloweb -- /bin/sh -c \
     "for i in $(seq -s' ' 1 10000); do wget -q -O- localhost:8080; done"

Pub/Sub

将 200 条消息发布到 Pub/Sub 主题:

for i in {1..200}; do gcloud pubsub topics publish echo --message="Autoscaling #${i}"; done

自定义指标

不适用:此示例中使用的代码会导出自定义指标的常量值 40。HorizontalPodAutoscaler 的目标值设置为 20,因此它会尝试自动纵向扩容 Deployment。

自定义 Prometheus

不适用:此示例中使用的代码会导出自定义指标的常量值 40。HorizontalPodAutoscaler 的目标值设置为 20,因此它会尝试自动纵向扩容 Deployment。

观察 HorizontalPodAutoscaler 纵向扩容

您可以通过运行以下命令检查 Deployment 的当前副本数:

kubectl get deployments

指标传播一段时间后,Deployment 会创建 5 个 Pod 来处理积压输入。

您还可以通过运行以下命令来检查 HorizontalPodAutoscaler 的状态和近期活动:

kubectl describe hpa

清除数据

为避免因本教程中使用的资源导致您的 Google Cloud 账号产生费用,请删除包含这些资源的项目,或者保留项目但删除各个资源。

CPU

删除 GKE 集群:

 gcloud container clusters delete metrics-autoscaling

Pub/Sub

  1. 清理 Pub/Sub 订阅和主题:

    gcloud pubsub subscriptions delete echo-read
    gcloud pubsub topics delete echo
    
  2. 删除 GKE 集群:

    gcloud container clusters delete metrics-autoscaling
    

自定义指标

删除 GKE 集群:

 gcloud container clusters delete metrics-autoscaling

自定义 Prometheus

删除 GKE 集群:

 gcloud container clusters delete metrics-autoscaling

后续步骤