Custom、External Metrics 与指标适配器
促销流量刚起来,队列监控已经显示积压持续增加,HPA 却停在 3 个副本。值班同学打开 Prometheus,checkout_queue_depth 曲线完整;再看 HPA Event,只得到 unable to get external metric。有人建议把阈值从 100 改成 10,有人准备手工扩容,但真正断掉的是另一段链路:Prometheus 中存在时序,不等于 Kubernetes 的 external.metrics.k8s.io 已经发现、授权并返回这条指标。阈值调得再激进,也不会让一个不可发现的 API 变得可用。
另一个集群的现象相反。kubectl get --raw 能读到 pods/http_requests_per_second,HPA 也短暂扩容,几轮之后指标却间歇性消失。应用每分钟上报一次,Prometheus 经过远端写入和聚合规则后延迟更长,而适配器的发现年龄窗口比完整采集链还短。旧时序从发现缓存中被淘汰,控制器看到的不是业务流量归零,而是“没有指标”。如果多指标 HPA 同时建议缩容,任一指标读取失败还会阻止这次缩容。此时必须排查时间窗口和失败语义,而不是把缺失值粗暴填成零。
先分清三条指标管道
Kubernetes 自动伸缩常见的指标入口有三类。metrics.k8s.io 提供 Pod 和 Node 的 CPU、内存资源指标,通常由 Metrics Server 实现;custom.metrics.k8s.io 把指标关联到某类 Kubernetes 对象,例如某个 Pod 的每秒请求数、某个 Deployment 的可用副本数;external.metrics.k8s.io 表示不必绑定 Kubernetes 对象的信号,例如云消息队列积压、负载均衡器请求率或外部作业数量。
Custom 和 External 描述的是 API 语义,不是存储产品。Prometheus 可以是后端,云监控服务也可以是后端;适配器负责把后端查询翻译成 Kubernetes Metrics API。它既不是 Prometheus 的长期存储替代品,也不负责决定副本数。HPA 才是指标消费者,HPA 再通过目标对象的 /scale 子资源改变副本。
优先选择 Custom Metric,前提是信号确实属于某个 Kubernetes 对象。对象身份可以进入 RBAC、标签选择和租户边界,误读全局同名指标的风险更低。只有信号天然位于集群之外或不能稳定映射到对象时,才选择 External Metric。Kubernetes 官方的 HPA 操作演练也明确建议:能够使用 Custom Metric 时,通常更容易做安全约束。
请求真正经过哪些组件
客户端请求 /apis/custom.metrics.k8s.io/v1beta1/... 时,路径并不是由 CRD 保存对象。kube-apiserver 的聚合层根据 APIService 把请求代理给扩展 API Server,也就是指标适配器。适配器先认证来自聚合器的请求,再根据请求中的资源、名称、namespace、metric name 和 selector 生成后端查询,最后把查询结果包装成 MetricValueList 返回。
这条链至少有六个可分离状态:后端时序存在、发现规则命中、资源标签能映射、APIService Available、请求身份有权读取、返回值的新鲜度与单位可用于伸缩。任何一层失败都可能被上层压缩成一句 FailedGetPodsMetric 或 FailedGetExternalMetric。Kubernetes 的 API 聚合层说明还要求扩展 API 的发现请求低延迟返回;适配器后端查询很快,不代表 Service、TLS 和聚合发现链也健康。
先用下面的盘点命令建立基线:
kubectl version
kubectl get apiservice | grep metrics.k8s.io
kubectl get apiservice v1beta1.custom.metrics.k8s.io -o yaml
kubectl get apiservice v1beta1.external.metrics.k8s.io -o yaml
kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 | head
kubectl get --raw /apis/external.metrics.k8s.io/v1beta1 | headAPIService 的 Available=True 只能证明聚合器能完成发现和基础通信,不能证明某条业务指标规则正确。发现列表中出现 metric name,也只能证明适配器缓存里有对应资源描述;还要对具体对象发起取值请求,才能验证标签、PromQL、数量单位与授权。
用 Helm 启用 Prometheus Adapter
实验需要一个可销毁集群、可用的 Prometheus、Helm、kubectl,以及创建 namespace、Deployment、Service、ConfigMap、RBAC 与 APIService 相关资源的权限。生产环境应把安装权交给平台 owner,应用团队只提交经审查的指标契约。Prometheus Adapter 的正式镜像位于 registry.k8s.io/prometheus-adapter/prometheus-adapter;版本、参数和镜像入口可从 kubernetes-sigs/prometheus-adapter 核对。
先查看 Chart 与默认值,再固定经过兼容评审的 Chart 版本和镜像摘要:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm search repo prometheus-community/prometheus-adapter --versions | head
export ADAPTER_CHART_VERSION='<reviewed-chart-version>'
helm show values prometheus-community/prometheus-adapter \
--version "${ADAPTER_CHART_VERSION}" > adapter-values.reference.yaml创建 adapter-values.yaml。Service 名称、端口、路径前缀和认证方式必须按实际 Prometheus 调整;不要把带 Token 的 URL 直接写进 values 或 Git。
prometheus:
url: http://prometheus.monitoring.svc
port: 9090
path: ""
replicas: 2
rules:
default: false
custom:
- seriesQuery: 'kube_deployment_status_replicas_available{namespace!="",deployment!=""}'
resources:
overrides:
namespace:
resource: namespace
deployment:
group: apps
resource: deployment
name:
matches: '^kube_deployment_status_replicas_available$'
as: 'available_replicas'
metricsQuery: 'max(<<.Series>>{<<.LabelMatchers>>}) by (<<.GroupBy>>)'
external:
- seriesQuery: 'kube_pod_status_phase{phase="Pending"}'
resources:
overrides:
namespace:
resource: namespace
name:
matches: '^kube_pod_status_phase$'
as: 'pending_pods'
metricsQuery: 'sum(<<.Series>>{<<.LabelMatchers>>,phase="Pending"})'
extraArguments:
metrics-relist-interval: 2m
metrics-max-age: 5m这里的示例依赖 Prometheus 已抓取 kube-state-metrics。replicas: 2 提高适配器本身的可用性,却不会修复单点 Prometheus、错误规则或聚合层证书。安装时保持 release、values 和 APIService 由同一交付方式管理:
helm upgrade --install prometheus-adapter \
prometheus-community/prometheus-adapter \
--namespace custom-metrics \
--create-namespace \
--version "${ADAPTER_CHART_VERSION}" \
--values adapter-values.yaml \
--atomic --timeout 10m
kubectl -n custom-metrics rollout status deployment/prometheus-adapter --timeout=300s
kubectl get apiservice | grep -E 'custom.metrics|external.metrics'
kubectl -n custom-metrics logs deployment/prometheus-adapter --tail=100预期 Deployment 完成 rollout,两类 APIService 最终为 True。如果 Chart 的对象名与示例不同,用 helm -n custom-metrics get manifest prometheus-adapter 确认,不要凭名称猜测。
四段规则分别改变什么
seriesQuery 只负责发现候选时序,应尽量用稳定 metric name 和必要标签收窄集合。把高基数业务标签、用户 ID、URL 原始路径或订单号放进发现集合,会增加 Prometheus 查询、适配器缓存和 API 发现负担,还可能把敏感维度暴露给集群用户。
resources 把 Prometheus label 翻译成 Kubernetes GroupResource。上例中的 deployment="checkout" 只有在映射到 apps/deployments 后,才能响应“某 namespace 下某 Deployment 的指标”。标签值必须与对象名一致;若采集链把 Deployment 名改成 release 名,即使 PromQL 有值,Custom Metrics 查询仍会返回空列表或对象不匹配。
name.matches 与 name.as 决定 Kubernetes API 暴露的 metric name。改名属于 API 变更:已有 HPA 继续请求旧名时会失败。团队应把它纳入版本化契约,先并行暴露新旧名称、迁移消费者,再删除旧规则。
metricsQuery 才执行取值。<<.Series>>、<<.LabelMatchers>> 与 <<.GroupBy>> 由适配器按请求展开。Custom Metric 通常必须按对象维度分组;External Metric 是否聚合成单值,要由容量模型决定。计数器要转换成速率,Gauge 才能直接求和或取最大值。把累计请求总数直接与每秒目标比较,会让副本数只增不减。
metrics-relist-interval 控制重新发现名称的周期,metrics-max-age 控制多老的时序仍可出现在发现结果中。后者至少要覆盖抓取间隔、recording rule 计算、远端读取延迟和可接受抖动。项目 README 对这两个参数及“指标出现又消失”的原因有专门说明;窗口拉大能减少误消失,也会延长陈旧信号存活时间,必须同时设置业务新鲜度告警。
正向实验:从对象到指标值
创建一个容易识别的 Deployment。它不产生真实业务数据,只用于证明 kube-state-metrics、Prometheus、适配器与对象映射连通:
apiVersion: v1
kind: Namespace
metadata:
name: adapter-lab
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: adapter-demo
namespace: adapter-lab
spec:
replicas: 2
selector:
matchLabels:
app: adapter-demo
template:
metadata:
labels:
app: adapter-demo
spec:
containers:
- name: pause
image: registry.k8s.io/pause:3.10kubectl apply -f adapter-demo.yaml
kubectl -n adapter-lab rollout status deployment/adapter-demo --timeout=180s
kubectl get --raw \
'/apis/custom.metrics.k8s.io/v1beta1/namespaces/adapter-lab/deployments.apps/adapter-demo/available_replicas'
kubectl get --raw \
'/apis/external.metrics.k8s.io/v1beta1/namespaces/adapter-lab/pending_pods'等 Prometheus 完成至少一次抓取且适配器完成一次 relist 后,第一条响应应是 MetricValueList,describedObject 指向 apps/v1 的 Deployment/adapter-demo,metric.name 为 available_replicas,value 接近可用副本数。第二条返回 namespace 选择下的 Pending Pod 汇总;没有 Pending Pod 时可能得到零值或空列表,取决于后端时序是否存在,二者在 HPA 中不是同一种语义。
再分别验证调用身份和 label selector:
kubectl auth can-i get deployments.apps/available_replicas \
--api-group=custom.metrics.k8s.io -n adapter-lab
kubectl auth can-i get pending_pods \
--api-group=external.metrics.k8s.io -n adapter-lab
kubectl get --raw \
'/apis/custom.metrics.k8s.io/v1beta1/namespaces/adapter-lab/deployments.apps/*/available_replicas?labelSelector=app%3Dadapter-demo'验证证据至少保存 APIService condition、发现列表片段、具体对象响应、适配器日志中的后端查询,以及同一时刻 Prometheus 查询结果。单独保存任何一项都不足以证明端到端契约成立。
反向实验:制造标签映射断裂
把 custom rule 中资源映射的 label 从 deployment 临时改成不存在的 workload_name,保留其他字段不变,然后升级实验 release:
resources:
overrides:
namespace:
resource: namespace
workload_name:
group: apps
resource: deploymenthelm upgrade prometheus-adapter prometheus-community/prometheus-adapter \
--namespace custom-metrics \
--version "${ADAPTER_CHART_VERSION}" \
--values adapter-values-broken.yaml \
--atomic --timeout 10m
kubectl get --raw \
'/apis/custom.metrics.k8s.io/v1beta1/namespaces/adapter-lab/deployments.apps/adapter-demo/available_replicas'
kubectl -n custom-metrics logs deployment/prometheus-adapter --since=5m预期 Prometheus 原始查询仍能找到 kube_deployment_status_replicas_available,APIService 也可能保持 Available=True,但具体对象查询为空或返回错误。这个反例把“后端有数据”“API 可发现”“对象映射正确”三件事拆开。适配器以 --v=6 启动时可看到生成的查询;只在隔离环境短期开启较高日志级别,并检查日志是否带有租户 label、查询条件或内部拓扑。
恢复时重新使用正确的 adapter-values.yaml,等待 rollout 与 relist,再重复对象查询:
helm upgrade prometheus-adapter prometheus-community/prometheus-adapter \
--namespace custom-metrics \
--version "${ADAPTER_CHART_VERSION}" \
--values adapter-values.yaml \
--atomic --timeout 10m
kubectl -n custom-metrics rollout status deployment/prometheus-adapter --timeout=300s不要用 insecureSkipTLSVerify 或集群管理员权限修复标签问题;那只会引入新的安全缺口。
指标消失时按层取证
先看 HPA Event 中是 FailedGetResourceMetric、FailedGetPodsMetric 还是 FailedGetExternalMetric,再沿对应 API 路径向下。APIService Available=False 时检查 Service endpoints、端口、NetworkPolicy、serving certificate、caBundle 与 request-header 认证。聚合层双向认证的 CA 角色和代理流程以 Kubernetes 官方的 聚合层配置说明为准,不能用通用集群 CA 随意替代 request-header CA。
APIService 正常而发现列表没有 metric name,检查 seriesQuery、Prometheus 中最近窗口是否有样本、relist/max-age 与抓取延迟。发现列表有名字但对象取值为空,检查资源 label、namespace、对象名和 metricsQuery 分组。取值存在但 HPA 不动作,再检查 Quantity 单位、target 类型、HPA condition 和多指标失败策略。
kubectl describe apiservice v1beta1.custom.metrics.k8s.io
kubectl -n custom-metrics get service,endpointslice,pod
kubectl -n custom-metrics logs deployment/prometheus-adapter --since=10m
kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1
kubectl -n adapter-lab get events --sort-by=.lastTimestampKubernetes Quantity 中 10500m 表示 10.5,不是 10500。容量评审必须同时记录原始 PromQL 单位、API Quantity 和 HPA target,避免毫单位被当成整数放大一千倍。
RBAC、凭证与敏感标签
权限有三段:kube-apiserver 聚合器到适配器的 request-header 身份;适配器读取 Kubernetes 对象、ConfigMap 和必要认证 API 的权限;HPA 或人工用户读取 Custom/External Metrics API 的权限。不要把三段都绑定到 cluster-admin。应用身份通常只需要自己 namespace 中指定资源和指标的 get 权限,适配器 ServiceAccount 也不应读取全体 Secret。
连接托管 Prometheus 时,优先工作负载身份或短期令牌,把 CA、endpoint 与凭证引用分离。Secret 不进入 values 明文、命令历史、适配器日志和海报。External Metric 的标签可能包含队列名、租户、区域和客户维度;即使值本身不敏感,发现列表与 selector 也可能暴露业务拓扑。平台团队应维护允许暴露的 metric name、label allowlist 和 namespace 访问矩阵。
API 聚合还扩大了控制面故障面。NetworkPolicy 必须允许 kube-apiserver 到适配器 Service 的通信;某些托管集群的控制面不受普通 namespace 出口规则控制,需要按发行方网络模型验证。证书轮换前同时检查 serving cert、APIService caBundle 和 request-header 信任,不要只滚动 Pod。
容量与成本不只在 Prometheus
每个发现规则都会增加时序扫描与缓存,具体查询还会消耗 Prometheus CPU、内存和查询并发。高基数 label、宽正则、长 rate window 和多 HPA 高频查询可以把伸缩控制链本身变成瓶颈。适配器副本增加后,请求吞吐提升,但每个副本都可能维护发现缓存并访问后端;需要结合 API QPS、查询延迟、错误率和 Prometheus 并发做容量测量。
容量模型至少记录:HPA 数量、每个 HPA 的 metric 数、控制器同步周期、适配器副本、发现时序数、PromQL 延迟分位、后端保留与远端读取成本。业务信号的采样周期要显著短于允许的扩容响应时间;若一个队列指标经过分钟级采集、分钟级聚合和分钟级 relist,副本再快也追不上突发。
External Metric 还可能触发云监控 API 调用费用和限流。应在适配器侧缓存与限流,在云侧设置专用身份和预算告警,并设计后端不可用时的失败策略。对核心在线流量,常见选择是保留保守的 minReplicas 与独立资源指标兜底,而不是把远端查询失败解释为零负载。
架构选择与所有权
只有 CPU/内存需求时,Metrics Server 链更短、成本更低。已有 Prometheus 且业务指标能映射 Kubernetes 对象时,Prometheus Adapter 适合统一暴露 Custom Metrics。信号来自云队列且已由云厂商适配器稳定提供时,可选择专用 External Metrics Adapter;需要缩到零、认证触发器和事件源语义时,则评估 KEDA,但仍要明确它与生成 HPA 的所有权。
平台团队拥有 adapter release、APIService、证书、RBAC 基线和共享规则;应用团队拥有指标定义、单位、目标值、低流量语义与容量压测证据;观测团队拥有采集、recording rule、新鲜度和后端 SLO;值班团队拥有从 HPA Event 到后端查询的排障手册。任何一方单独修改 metric name、label 或聚合窗口,都可能破坏整个契约。
团队准入应要求每条伸缩指标同时提交正向样本、零值样本、缺失样本、延迟样本和高基数评估。上线门禁观察的不是“图上有曲线”,而是具体 API 请求在连续多个采样周期内稳定返回,Quantity 单位正确,适配器错误率和延迟处于容量预算内。
升级、回滚与安全退出
升级前导出 Helm values、APIService、RBAC 和规则清单,并在隔离 namespace 回放发现与具体取值请求。先升级一个非关键集群或影子 release,比较旧新适配器暴露的 metric name、GroupResource、Quantity 和查询结果;规则兼容后再迁移 HPA。只比较 Pod Ready 无法发现 API 语义漂移。
回滚优先恢复上一版 Chart、镜像摘要和 values。若改过 metric name,旧 HPA 也必须恢复或在过渡期保留别名。删除 adapter 前先枚举所有 HPA 对 custom、object、pods、external metric source 的引用,迁移到资源指标、其他适配器或人工固定副本;否则卸载会留下持续报错但表面仍在运行的 HPA。
实验清理顺序如下:
kubectl delete -f adapter-demo.yaml
helm uninstall prometheus-adapter -n custom-metrics
kubectl get apiservice | grep -E 'custom.metrics|external.metrics' || true
kubectl delete namespace custom-metrics共享集群中不要直接执行卸载。先确认 APIService 没有其他 release 接管、没有 HPA 消费这些 API、没有残留 ClusterRoleBinding 和证书 Secret,再由平台 owner 核销。真正的退出完成标志,是消费者迁移、权限撤销、凭证失效、外部监控调用停止、规则与审计记录归档,而不只是 Deployment 消失。
