首页
/ Istio 如何用 prometheus.istio.io/scrape-targets 注解合并多容器 Prometheus 指标

Istio 如何用 prometheus.istio.io/scrape-targets 注解合并多容器 Prometheus 指标

2026-09-09 22:11:31作者:江焘钦

一个 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,现状是:

  • handleStatspilot/cmd/pilot-agent/status/server.go)只抓取一个编码在 ISTIO_PROMETHEUS_ANNOTATIONS JSON 里的端点(一个 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.goParseScrapeTargets 与设计文档):

  • 每个条目会做空白裁剪(whitespace trimming),所以 " 8080:/metrics , 9100:/custom" 是合法写法,webhook 单测覆盖了这一场景;
  • 路径为空时默认 /metrics,例如 "9100" 等价于 "9100:/metrics"
  • 条目顺序保留,顺序决定合并输出的顺序
  • 任一条目端口为空(如 ":/metrics")则解析失败,webhook 记录一条 warn 日志且 Targets 留空,不会阻断注入。

webhook 注入时的处理

注入 webhook 的 pkg/kube/inject/webhook.goapplyPrometheusMerge 对该注解做四件事:

  1. 逐个校验目标端口:必须是 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.goistioReservedPorts: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,已合并)。

  2. 兼容旧版 agent:用第一个目标回填 legacy 的 Port/Path 字段,使得不认识 Targets 的旧 agent 至少还能抓取主端点。

  3. 编码进环境变量:把完整结构(含 Targets)JSON 编码后写入 sidecar 容器的 ISTIO_PROMETHEUS_ANNOTATIONS 环境变量。

  4. 改写标准注解:清空原有 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 时,handleStatsscrapeMultipleApps 并发抓取路径(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.yamlmulti-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 上限。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525