一、问题背景:弹性伸缩为啥突然“罢工”

很多做云原生应用的开发者,都用过Knative Serving来做服务的弹性伸缩——简单说就是服务访问量多的时候自动多开实例扛压力,没人访问的时候自动关掉省资源,不用人工盯着调。但不少人遇到过一个坑:明明配置了自定义指标(比如按接口请求延迟、队列长度伸缩),结果要么缩到只剩一个实例卡着不动,要么访问量爆了实例数纹丝不变,完全没弹性。我之前帮一家做在线课程平台的客户排查过这个问题,他们的直播回放服务,高峰期明明延迟已经飙到2秒,实例数还是只有3个,导致大量用户加载失败,后来发现就是自定义指标伸缩的配置异常导致的。

二、核心原理:Knative自定义指标伸缩怎么工作

要解决问题,得先搞懂Knative自定义指标伸缩的基本逻辑,不然改配置都是瞎蒙。Knative的弹性伸缩核心是一个叫Autoscaler的组件,它的工作流程分三步:

  1. 收集指标:从Prometheus(Knative默认用的监控组件)拉取你定义的自定义指标,比如接口的平均响应延迟、任务队列的长度;
  2. 计算实例数:用你配置的目标值,算出来当前需要多少个实例——比如目标延迟是500ms,现在平均延迟是2000ms,那需要的实例数就是原来的4倍;
  3. 调整实例:把算出来的实例数告诉Kubernetes的控制器,让它开或者关Pod。

这里的关键是“目标值”的配置,还有指标的拉取规则,只要有一个环节错了,弹性就会失效。

三、常见异常场景与处理方案

3.1 场景1:自定义指标拉取失败,Autoscaler拿不到数据

问题表现

最明显的是Knative的Autoscaler日志里会出现“failed to fetch metric”的错误,同时你用kubectl describe看服务的伸缩状态,会发现没有实例数的变化记录。比如我之前排查的课程平台案例,他们配置的是按接口延迟伸缩,但Autoscaler连延迟数据都拉不到,自然不会调整实例数。

技术栈与示例

这里我们用Knative Serving + Prometheus + Go(服务用Go写,方便模拟延迟)的技术栈来做示例,所有配置都符合Knative的官方规范。

首先,先确认Prometheus有没有正确拉取到服务的指标。先写一个简单的Go服务,暴露Prometheus格式的延迟指标:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

// 定义自定义指标:接口的平均响应延迟,单位是毫秒
var apiLatency = prometheus.NewGaugeVec(
	prometheus.GaugeOpts{
		Name: "api_response_latency_ms", // 指标名称,必须和Prometheus里的一致
		Help: "Average response latency of the API in milliseconds",
	},
	[]string{"path"}, // 标签,用来区分不同接口
)

func init() {
	// 注册指标到Prometheus的默认注册表
	prometheus.MustRegister(apiLatency)
}

func main() {
	// 模拟课程列表接口,随机生成延迟
	http.HandleFunc("/course/list", func(w http.ResponseWriter, r *http.Request) {
		// 随机生成100-3000ms的延迟,模拟高峰期的情况
		delay := time.Duration(100+rand.Intn(2900)) * time.Millisecond
		time.Sleep(delay)
		// 更新延迟指标,path标签为/course/list
		apiLatency.WithLabelValues("/course/list").Set(float64(delay.Milliseconds()))
		w.Write([]byte("course list"))
	})

	// 暴露Prometheus的指标接口,路径是/metrics
	http.Handle("/metrics", promhttp.Handler())
	log.Println("server started on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

然后,我们需要给这个Go服务写Knative的服务配置,同时配置自定义指标伸缩:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: course-service
  namespace: default
spec:
  template:
    metadata:
      annotations:
        # 开启自定义指标伸缩,类型是目标指标(target)
        autoscaling.knative.dev/target: "500" # 目标延迟是500ms
        autoscaling.knative.dev/metric: "api_response_latency_ms" # 自定义指标名称
        autoscaling.knative.dev/metric-type: "target" # 指标类型,必须是target
        autoscaling.knative.dev/scale-down-delay: "1m" # 缩容延迟,1分钟
        autoscaling.knative.dev/scale-up-delay: "10s" # 扩容延迟,10秒
    spec:
      containers:
        - image: your-registry/course-service:v1 # 替换成你自己的镜像地址
          ports:
            - containerPort: 8080

排查与修复步骤

如果出现指标拉取失败,首先要检查三个地方:

  1. 指标名称是否完全一致:比如Go代码里的指标名是api_response_latency_ms,Knative配置里的autoscaling.knative.dev/metric必须完全一样,大小写、下划线都不能错,差一个字符都拉不到;
  2. Prometheus的抓取配置:要确保Prometheus的配置里有抓取这个服务的/metrics接口的规则,比如:
scrape_configs:
  - job_name: 'course-service'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_label_app]
        regex: course-service
        action: keep
    metrics_path: '/metrics'

如果没有这个配置,Prometheus根本不会去拉这个服务的指标; 3. 指标的标签:如果指标有标签(比如我们的例子里的path标签),Knative配置里要指定标签的匹配规则,不然Autoscaler不知道要取哪个标签对应的指标,比如要加autoscaling.knative.dev/metric-selector: "path=/course/list",表示只取path标签为/course/list的延迟指标。

3.2 场景2:目标值配置不合理,伸缩逻辑出错

问题表现

比如访问量已经爆了,实例数还是上不去,或者稍微有点访问就开很多实例。比如之前有个客户配置的目标延迟是100ms,结果高峰期延迟到150ms,按道理应该扩容,但Autoscaler算出来的实例数反而比原来少,后来发现是目标值的单位错了——他们的指标是秒,但配置的目标值是按毫秒算的。

排查与修复步骤

  1. 确认指标的单位:比如你的指标是秒还是毫秒,目标值要和指标的单位一致。比如我们的例子里指标是毫秒,目标值是500(即500ms),如果指标是秒,目标值就要改成0.5;
  2. 确认指标类型:Knative的自定义指标分两种,一种是“目标指标”(target),比如延迟,是每个实例的目标值;另一种是“聚合指标”(aggregate),比如队列长度,是整个服务的总目标值。如果把队列长度的指标配置成target类型,伸缩逻辑就会错,比如总队列长度是100,目标值是10,那需要的实例数是10个,但如果配置成target,Autoscaler会认为每个实例的队列长度是100,需要的实例数是100/10=10?不对,其实target类型是按每个实例的指标值算,aggregate是按总指标值算,所以如果是总队列长度,必须配置autoscaling.knative.dev/metric-type: "aggregate";
  3. 确认伸缩的上下限:比如你配置了autoscaling.knative.dev/min-scale: "1"(最小实例数1),autoscaling.knative.dev/max-scale: "3"(最大实例数3),那哪怕需要10个实例,也最多只能开3个,这也是常见的坑,很多人忘了改最大实例数的限制。

3.3 场景3:Autoscaler的配置冲突,导致伸缩逻辑混乱

问题表现

比如你既配置了按请求数伸缩,又配置了按自定义指标伸缩,结果两个逻辑互相冲突,导致实例数波动异常。比如有的开发者为了保险,同时配置了autoscaling.knative.dev/target-requests: "100"(按请求数伸缩)和autoscaling.knative.dev/metric: "api_response_latency_ms"(按延迟伸缩),结果Autoscaler会按两个指标里算出来的最大实例数来调整,导致实例数突然变多。

排查与修复步骤

  1. 检查配置的冲突:Knative的Autoscaler不建议同时配置多种伸缩策略,除非你明确知道自己要什么。如果是自定义指标伸缩,要把默认的按请求数伸缩的配置去掉,比如不要加autoscaling.knative.dev/target-requests;
  2. 检查全局配置和服务级配置的冲突:Knative的全局Autoscaler配置(在ConfigMap里)可能会有默认的目标值,比如全局配置了target: "100",而你的服务级配置了target: "500",这时候服务级配置会覆盖全局配置,但如果全局配置的指标类型和服务级的不一样,就会出问题,所以最好只在服务级配置自定义指标的所有参数,不要依赖全局配置。

四、应用场景、技术优缺点与注意事项

4.1 应用场景

自定义指标伸缩适合所有需要根据业务实际负载调整实例数的场景,比如:

  • 在线服务:按接口延迟、错误率伸缩,保证服务质量;
  • 异步任务服务:按任务队列长度伸缩,保证任务及时处理;
  • 数据处理服务:按CPU使用率、内存使用率伸缩,保证数据处理的效率。 比如我们之前的课程平台,直播回放的服务,高峰期用户访问量暴增,接口延迟飙升,这时候用自定义指标伸缩,就能自动扩容实例,保证用户体验;平时访问量少的时候自动缩容,节省资源。

4.2 技术优缺点

优点

  • 灵活性高:可以根据自己的业务指标来伸缩,而不是只能用请求数、CPU这些通用指标;
  • 精准性强:能更准确地反映业务的实际负载,比如延迟高的时候,哪怕请求数不多,也需要扩容;
  • 节省资源:比固定实例数的部署方式节省资源,比按通用指标伸缩更能适应业务的变化。

缺点

  • 配置复杂:需要自己写指标的收集、暴露,配置Knative的伸缩参数,比默认的按请求数伸缩麻烦;
  • 依赖监控组件:需要Prometheus等监控组件的支持,如果监控组件出问题,伸缩就会失效;
  • 调试难度大:如果伸缩出问题,需要排查指标拉取、计算、调整等多个环节,不容易定位问题。

4.3 注意事项

  • 指标的准确性:要确保你的自定义指标是准确的,比如延迟指标不能有延迟,不然Autoscaler的计算会出错;
  • 伸缩的平滑性:要配置合理的扩容和缩容延迟,比如扩容延迟设成10秒,避免访问量突然增加时瞬间开太多实例,缩容延迟设成1分钟,避免访问量波动时频繁开关实例;
  • 实例数的限制:要根据业务的实际情况配置最小和最大实例数,最小实例数不能设成0(除非你能接受服务启动的延迟),最大实例数不能设得太小(避免高峰期不够用);
  • 监控Autoscaler的状态:要定期监控Autoscaler的日志和状态,及时发现指标拉取失败、计算错误等问题。

五、文章总结

Knative Serving的自定义指标伸缩是一个非常实用的功能,但配置不当很容易导致弹性失效。常见的异常场景包括指标拉取失败、目标值配置不合理、配置冲突等,解决这些问题的核心是搞懂自定义指标伸缩的原理,然后逐一排查每个环节的配置是否正确。

在实际使用中,要注意指标的准确性、伸缩的平滑性、实例数的限制,同时要监控Autoscaler的状态,及时发现和解决问题。如果你是第一次用自定义指标伸缩,建议先在测试环境里验证配置,比如模拟高峰期的负载,观察实例数的变化,确认伸缩逻辑正常后再上线。

最后,再给大家一个小技巧:如果遇到伸缩失效的问题,先看Autoscaler的日志,用kubectl logs -n knative-serving <autoscaler-pod-name>,日志里会明确告诉你是拉不到指标,还是计算有问题,或者是其他错误,这能帮你快速定位问题。