Istio 如何用 prometheus.istio.io/scrape-targets 注解合并多容器 Prometheus 指标
一个 Pod 里跑主应用(:8080/metrics)再配一个 sidecar exporter(:9100/metrics)是很常见的形态,但在 Istio sidecar 接管指标合并后,第二个容器的指标会被静默丢弃:pilot-agent 的 handleStats 只抓取 ISTIO_PROMETHEUS_ANNOTATIONS 里编码的单个端点,而且注入 webhook 会把 prometheus.io/port 改写为 15020,在 STRICT mTLS 下 Prometheus 本来就摸不到应用端口,只能走 agent 的 :15020/stats/prometheus——于是第二个端点从 scrape 配置里彻底消失,且没有任何报错。
Istio 针对这个问题引入了 prometheus.istio.io/scrape-targets 注解:在 Pod 上声明多个 port:path 抓取目标,注入时由 webhook 校验并编码进 sidecar 环境变量,运行时由 pilot-agent 并发抓取后按声明顺序合并进 :15020/stats/prometheus。本文基于仓库中的设计文档 multi-port-metrics-merging.md、实现代码和集成测试,给出配置方式、行为边界与验证手段。
问题背景:单端点合并为什么不够
按 设计文档 的 Problem Statement,现状是:
handleStats(pilot/cmd/pilot-agent/status/server.go)只抓取一个编码在ISTIO_PROMETHEUS_ANNOTATIONSJSON 里的端点(一个port+ 一个path)。- STRICT mTLS 下 Prometheus 无法直接到达应用端口,所有抓取必须经过 agent 的
:15020/stats/prometheus。 - 注入时的
applyPrometheusMerge会把prometheus.io/port改写为15020,导致第二个容器的端点根本没有被抓取的路径。 - 结果:第二个容器的指标被静默丢弃,没有错误提示。
设计文档同时说明了为什么不扩展 prometheus.io/port 本身:该注解被 kube-state-metrics、Prometheus operator CRD 等外部工具直接消费,改变其语义会静默破坏不在 Istio agent 路径上的抓取配置。新注解使用 prometheus.istio.io/ 前缀,语义归属 Istio,不与外部工具冲突。
配置 prometheus.istio.io/scrape-targets 注解
注解格式
值是一个逗号分隔的 port:path 列表:
annotations:
prometheus.istio.io/scrape-targets: "8080:/metrics,9100:/custom-metrics"
解析规则(来自 server.go 的 ParseScrapeTargets 与设计文档):
- 每个条目会做空白裁剪(whitespace trimming),所以
" 8080:/metrics , 9100:/custom"是合法写法,webhook 单测覆盖了这一场景; - 路径为空时默认
/metrics,例如"9100"等价于"9100:/metrics"; - 条目顺序保留,顺序决定合并输出的顺序;
- 任一条目端口为空(如
":/metrics")则解析失败,webhook 记录一条 warn 日志且Targets留空,不会阻断注入。
webhook 注入时的处理
注入 webhook 的 pkg/kube/inject/webhook.go 中 applyPrometheusMerge 对该注解做四件事:
-
逐个校验目标端口:必须是 1–65535 的数字;不能等于 agent 状态端口(15020);不能是 Istio 保留的数据面端口。违反时注入直接失败,错误信息示例:
invalid prometheus scrape targets: target port 15020 conflicts with agent port invalid prometheus scrape targets: target port 15021 is reserved for Istio (Envoy health) and cannot be scraped保留端口清单定义在 server.go 的
istioReservedPorts:15000(Envoy admin)、15001(outbound 流量捕获)、15004(pilot debug)、15006(inbound 流量捕获)、15008(HBONE tunnel)、15020(pilot-agent status / 合并 Prometheus)、15021(Envoy health)、15053(DNS proxy)、15090(Envoy Prometheus,已合并)。 -
兼容旧版 agent:用第一个目标回填 legacy 的
Port/Path字段,使得不认识Targets的旧 agent 至少还能抓取主端点。 -
编码进环境变量:把完整结构(含
Targets)JSON 编码后写入 sidecar 容器的ISTIO_PROMETHEUS_ANNOTATIONS环境变量。 -
改写标准注解:清空原有 prometheus 注解后统一设置:
prometheus.io/port: "15020" prometheus.io/path: "/stats/prometheus" prometheus.io/scrape: "true"注意
prometheus.istio.io/scrape-targets本身在注入完成后会从 Pod 上被移除(webhook 测试断言 "happy path strips prometheus.istio.io/scrape-targets annotation"),因为它只服务于注入时的解析。
测试用 Pod 样例见 pkg/kube/inject/testdata/inject/hello-multiport-metrics.yaml,注解写法即 "8080:/metrics,9100:/custom-metrics"。
向后兼容:不写该注解时,webhook 与 agent 的行为与之前完全一致;已经按旧格式注入的 Pod 里 ISTIO_PROMETHEUS_ANNOTATIONS 的旧 JSON({"scrape":"true","port":"8080","path":"/metrics"})仍然有效,Targets 为 nil 时单端点流程原样运行。
agent 的合并行为
当 Targets 数量大于 1 时,handleStats 走 scrapeMultipleApps 并发抓取路径(server.go);单目标(≤1 个)保持原有流式 io.Copy 热路径,输出字节级不变,老 Pod 没有并发和缓冲开销。多目标路径的关键行为:
- 顺序确定:结果按
Targets下标写入,而不是按 goroutine 完成顺序,响应内容确定; - 单个目标失败不阻断:失败目标(连接失败、读取错误)只记录错误并递增
istio_agent_scrape_failures_total{type="application"},合并响应仍以 200 返回其余数据,与现有 "we do not return any errors here" 的语义一致; - 10 MiB 上限:每个目标响应体超过 10 MiB 会被丢弃并计一次 scrape 失败,用于约束 agent 内存;
- 格式协商:响应
Content-Type取第一个成功目标的协商格式;如果后续目标格式不一致(OpenMetrics 与 text 混用),整体降级为text/plain,因为一个响应体不能同时是两种结构格式; - OpenMetrics
# EOF:每个 body 的# EOF先被剥掉,仅当最终协商格式为 OpenMetrics 时在响应末尾统一追加一次# EOF,保证合并响应恰好一个终止符; - 指标族名冲突不处理:两个目标暴露同名指标族仍会导致 Prometheus 端解析错误,与设计文档中 Envoy 与 app 冲突时的既有行为一致——目标指标命名空间需要保持互不重叠。
以上行为也见发布说明 multi-port-metrics-fanout.yaml 与 multi-port-scrape-targets.yaml。
部署示例与验证方式
仓库的集成测试 tests/integration/telemetry/api/multiport_metrics_test.go 提供了一个可参照的完整部署:两个 busybox 容器分别用 httpd 在 8080 和 9100 提供指标文件,指标内容由 ConfigMap 挂载。核心部分:
apiVersion: apps/v1
kind: Deployment
metadata:
name: multiport-metrics-app
spec:
replicas: 1
selector:
matchLabels:
app: multiport-metrics-app
template:
metadata:
labels:
app: multiport-metrics-app
annotations:
prometheus.istio.io/scrape-targets: "8080:/metrics,9100:/metrics"
spec:
containers:
- name: primary
image: busybox:1.28
command: ["httpd", "-f", "-p", "8080", "-h", "/www"]
ports:
- containerPort: 8080
volumeMounts:
- name: primary-metrics
mountPath: /www
- name: secondary
image: busybox:1.28
command: ["httpd", "-f", "-p", "9100", "-h", "/www"]
ports:
- containerPort: 9100
volumeMounts:
- name: secondary-metrics
mountPath: /www
volumes:
- name: primary-metrics
configMap:
name: multiport-primary-metrics
- name: secondary-metrics
configMap:
name: multiport-secondary-metrics
两个 ConfigMap 各提供一个指标族(测试中的示例内容):
# multiport-primary-metrics
data:
metrics: |
# HELP primary_metric_total Test metric from container 1 (port 8080).
# TYPE primary_metric_total counter
primary_metric_total{container="primary"} 1
# multiport-secondary-metrics
data:
metrics: |
# HELP secondary_metric_total Test metric from container 2 (port 9100).
# TYPE secondary_metric_total counter
secondary_metric_total{container="secondary"} 42
该测试在 STRICT mTLS 的 PeerAuthentication 下运行,验证链路正是 注解 → webhook → 环境变量 → handleStats → 合并响应。
验证注入结果
对已注入的 Pod 执行:
kubectl get pod <POD_NAME> -o jsonpath='{.metadata.annotations.prometheus\.io/port} {.metadata.annotations.prometheus\.io/path}'
期望输出 15020 /stats/prometheus,同时 prometheus.istio.io/scrape-targets 注解应已不在 Pod 上(注入时被剥离)。
验证合并抓取
直接向 agent 端点确认两个目标都在输出中:
kubectl exec <POD_NAME> -c <APP_CONTAINER> -- curl -s http://localhost:15020/stats/prometheus
或通过 Prometheus 查询(集成测试使用的判断方式,<NS> 替换为你的命名空间):
primary_metric_total{namespace="<NS>"}
secondary_metric_total{namespace="<NS>"}
两个查询都能返回数据,说明双端点合并生效。
部分失败的判断
集成测试 TestMultiPortMetricsMergePartialFailure 还覆盖了 9100 未监听(连接被拒)的情形,判断条件是:primary_metric_total 仍然可查,且 istio_agent_scrape_failures_total{type="application"} 累计值大于等于 1。如果你某个目标容器临时挂了,可以用同样的两个查询区分"agent 整体没工作"和"单个目标抓取失败"。
限制与边界
- 目标端口与 15020 或任一保留端口(15000/15001/15004/15006/15008/15020/15021/15053/15090)冲突时,注入阶段直接报错,不会生成 Pod。
- 注解值格式非法时 webhook 只记 warn 日志、
Targets留空,注入成功但多端点能力不生效——配置后建议按上文步骤核对实际抓取行为。 - 合并响应不承诺指标去重:两个目标暴露同名指标族会在 Prometheus 端解析报错,命名空间隔离由使用方保证。
- 单目标 Pod 走的是原有流式路径,行为与引入该注解前字节级一致;多目标路径才有并发、缓冲和 10 MiB 上限。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00