统一观测平台(Prometheus 联邦对接)

2025-10-15T10:30:00+08:00 | 26分钟阅读 | 更新于 2025-10-15T10:30:00+08:00

@

学习目标

类比:把统一观测平台想象成写字楼的「中央消防与能耗监控室」。每间办公室(租户 Namespace)自己有烟感和电表(Prometheus Agent),但物业(平台)不能挨个房间翻看——而是把这些数据汇总到中央监控室(中心 Prometheus 联邦 + Thanos),按办公室编号(tenant 标签)分别成图、分别告警。这样既统一视图,又互不串门。

学完本章你应该能够:

  1. 说清楚「各集群 Prometheus Agent → 中心 Prometheus 联邦 + Thanos → Object Storage → Query → Grafana/Alertmanager」这条数据链路,以及每一跳解决什么问题。
  2. 在中心 Prometheus 上编写联邦(federation)抓取配置,从多套物理隔离集群按 match[] 拉取带 tenant/app/env/cluster 标签的指标。
  3. relabel_configs 在 Agent 侧注入租户 / 环境 / 集群标签,实现指标维度的租户隔离与过滤
  4. 配置 recording rules、Thanos Query 跨集群聚合查询,以及 Alertmanager 按租户 / 环境 / 严重度的分级路由与抑制
  5. 在测试环境与生产环境之间正确区分联邦纳管、长期存储保留期、告警通道与审批边界。
  6. 动手写出可编译的 Go 联邦查询代理:用 gin 提供租户隔离的查询 API,后端调用 Thanos Query 的 HTTP /api/v1/query 与 gRPC Store/Query 两种协议,并按 tenant 强制注入标签过滤。

前置知识

  • 已掌握《模块 02 多 K8s 集群纳管》(理解统一 K8s API 网关、物理隔离集群、Namespace 租户隔离)。
  • 熟悉 Prometheus 基础:scrape_configsrelabel_configsrecording rulesalerting rules
  • 了解 Thanos 组件(Sidecar / Receiver / Query / Store)与对象存储(S3 / OSS / MinIO)基本概念。
  • 了解 RBAC、审计日志、多级审批在平台中的落地方式。
  • Go 基础:gin Web 框架、client-go 基本用法、gRPC 客户端调用。

本章你会动手做的事

  1. 给一个生产集群的 Prometheus Agent 加上 relabel,把 tn-* 命名空间映射成 tenant 标签。
  2. 在中心 Prometheus 写一段联邦抓取任务,只拉取带 tenant!="" 的关键指标。
  3. 用 Thanos Query 的 --store 把两个集群的历史数据做一次跨集群 QPS 聚合。
  4. 编译运行 Go 联邦查询代理,用租户 A 的 token 查询,验证只能看到 tenant="acme" 的数据。

一、模块概述与企业商用价值

1.1 这个模块解决什么痛点

在「多套物理隔离 K8s 集群 + 统一 API 网关 + 租户 Namespace 强隔离」的底座上,监控面临三个真实难题:

  • 数据散落:每套集群各自一套 Prometheus,值班人员要分别登 4 套 Grafana 才能拼出全貌。
  • 长期留存贵:单 Prometheus 本地 TSDB 撑不过 15 天,故障复盘要查 90 天前的曲线时无据可查。
  • 租户串味:A 租户的指标一旦混进 B 租户大盘,既违规(数据级隔离被打破)又误导排障。

统一观测平台用「联邦采集 + Thanos 长期存储 + 标签隔离」三件套一次性解决上述三点,是整套 PaaS 白皮书当之无愧的核心模块——因为模块 03 的应用发布、模块 04 的中间件、模块 06 的自动化任务,最终都把「可观测性」当成标配能力对外暴露。

1.2 平台不可替代性

  • 对金融 / 政企客户,审计要求「指标可追溯到租户、保留 ≥ 90 天」,单机 Prometheus 天然不达标,联邦 + Thanos 是合规基线。
  • 租户自助看板必须做到「看得到自己的、看不到别人的」,这依赖标签注入与 Grafana 数据源过滤,而非靠人自觉。
  • 告警必须分级:P0 直接打电话,P3 进工单,不能所有告警都轰炸同一个群。

1.3 模块在 PaaS 全局中的定位

下图用一张定位图说明:统一观测平台向下吃多集群联邦指标,向上服务租户大盘与告警,横向被 RBAC / 审计 / 审批约束。

graph TD
  A[统一观测平台] --> B[多集群基础架构]
  A --> C[联邦监控体系]
  A --> D[平台标准管控]
  B --> B1[物理隔离 K8s 集群]
  B --> B2[统一 K8s API 网关]
  B --> B3[租户 Namespace 隔离]
  C --> C1[Prometheus Agent 采集]
  C --> C2[中心 Prometheus 联邦]
  C --> C3[Thanos 长期存储]
  D --> D1[RBAC 细粒度]
  D --> D2[审计 90 天+]
  D --> D3[多级审批]

这张图在讲什么:观测平台不是孤立系统,它由底座(多集群)供给数据,被管控(RBAC/审计)约束边界,是连接「资源」与「人」的桥梁。


二、细分功能详解(商用生产级)

下面按「基础能力」与「高级企业增值能力」分级。★禁止删减项在本章为:联邦架构、标签隔离、告警分级——它们对应企业合规与多租户隔离的底线。

2.1 功能清单对照

能力分级功能项说明商用要点
基础能力① 联邦采集架构中心 Prometheus 通过 /federate 拉取各集群 Agent 指标★禁止删减:联邦架构
基础能力② 指标标签体系tenant/app/env/cluster 强制注入★禁止删减:标签隔离
基础能力③ Alertmanager 分级告警按租户 / 环境 / 严重度路由★禁止删减:告警分级
高级企业增值④ Thanos 长期存储Sidecar/Receiver + 对象存储 + Query解决 90 天+ 留存
高级企业增值⑤ Grafana 多租户隔离大盘文件夹 + 数据源过滤按租户数据级隔离
高级企业增值⑥ 日志与追踪关联Loki / Tempo 或等价指标-日志-链路三联
高级企业增值⑦ SLO 与告警治理错误预算、告警疲劳抑制降低误报

2.2 基础能力逐条拆解

① 联邦采集架构(★禁止删减)

中心 Prometheus 不直连业务 Pod,而是用 federation 语义从各集群的 Prometheus Agent 拉取「已聚合好的、带租户标签的」指标。这样既避免中心直接抓取上千个 Target 造成的连接风暴,又保留跨集群统一视图。Agent 模式(无本地告警、无长期存储、只采集 + 可选 remote_write)进一步降低边缘集群资源占用。

② 指标标签体系(★禁止删减)

所有进入联邦的指标必须带四个强制标签:tenant(租户)、app(应用)、env(环境:production/prestaging/testing/developing)、cluster(集群名)。缺任一标签的指标在中心侧被 drop 或打上 tenant="unknown" 并触发治理告警——这是租户隔离的数据基础。

③ 告警分级(★禁止删减)

Alertmanager 按 severity(critical/warning/info)与 tenant/env 组合路由:critical + production 走电话 / 企业微信急件;warning 进工单;info 仅记录。配合 inhibit_rules 实现「节点宕机时抑制该节点上 Pod 的副本告警」,避免告警风暴。

2.3 高级企业增值能力

④ Thanos 长期存储:中心 Prometheus 旁挂 Thanos Sidecar,将本地 TSDB 块压缩上传到对象存储(S3/OSS/MinIO),Query 组件跨 Sidecar + Store 聚合查询,实现「近期秒级、历史按天」的统一查询。边缘集群也可通过 Thanos Receiver 接收 remote_write,再由 Query 统一聚合。

⑤ Grafana 多租户隔离大盘:每个租户一个 Grafana 文件夹,数据源用 tenant 标签做查询过滤(或在数据源代理层注入 tenant=$__org 变量),确保 A 租户打开大盘只看到自己的曲线。本章 Go 部分给出「按 tenant 动态创建数据源 + 注入查询头」的实现。

⑥ 日志与追踪关联:通过 trace_id / pod 标签把 Prometheus 指标、Loki 日志、Tempo 链路串起来,排障时从「曲线尖刺」一键跳到「对应日志与链路」。

⑦ SLO 与告警治理:基于 recording rules 预计算错误率与延迟,用错误预算驱动告警,定期清理低价值告警规则,抑制告警疲劳。

下图是统一观测平台的功能架构,把上述能力落到「采集—存储—展示—告警」四层。

graph LR
  subgraph 采集层
    S1[Prometheus Agent 集群A]
    S2[Prometheus Agent 集群B]
  end
  subgraph 联邦层
    F[中心 Prometheus 联邦]
    R[Thanos Sidecar/Receiver]
  end
  subgraph 存储层
    O[(对象存储)]
    Q[Thanos Query]
  end
  subgraph 消费层
    G[Grafana 多租户大盘]
    A[Alertmanager 分级告警]
  end
  S1 --> F
  S2 --> F
  F --> R
  R --> O
  O --> Q
  Q --> G
  Q --> A

这张图在讲什么:从边缘采集到中心联邦、长期存储、最终消费,指标数据每一层都带标签、可隔离、可追溯。

2.4 项目结构与文件清单

本章所有代码统一收口为一个可独立构建的 Go 服务 paas-observability,并配合一套 K8s IaC 清单。目录树如下(文件名与后文逐个展示一一对应):

paas-observability/
├── go.mod                                  # Go 依赖(gin / thanos / grpc)
├── cmd/
│   └── federation-proxy/
│       └── main.go                         # gin 服务入口:租户隔离查询 API
├── internal/
│   ├── auth/
│   │   └── tenant.go                       # gin 中间件:解析租户/管理员身份
│   ├── thanos/
│   │   ├── query.go                        # Thanos Query HTTP 客户端(/api/v1/query)
│   │   └── grpc/
│   │       └── grpc_query.go               # Thanos Query gRPC Store/Query 客户端
│   ├── grafana/
│   │   └── datasource.go                   # 按 tenant 动态创建 Grafana 数据源
│   └── alerting/
│       └── router.go                       # 告警分级路由 Go 实现(与 Alertmanager 一致)
└── deploy/                                 # K8s IaC(见第三、四章)
    ├── 00-namespace-rbac.yaml              # monitoring ns + Agent SA/ClusterRole
    ├── 01-prom-agent-configmap.yaml        # Agent scrape + relabel 注入标签
    ├── 02-prom-agent-statefulset.yaml      # Agent StatefulSet/Service/ServiceMonitor
    ├── 03-central-federation-configmap.yaml# 中心 Prometheus 联邦 scrape 配置
    ├── 04-thanos.yaml                      # Sidecar/Receiver/Query/Store 部署
    ├── 05-alertmanager.yaml                # Alertmanager 部署 + 分级路由/抑制
    ├── 06-grafana-provisioning.yaml        # Grafana 数据源 + 多租户文件夹
    └── 07-rules.yaml                       # recording rules + alert rules

后文按「架构联动(第三章)→ 端到端 SOP(第四章)→ 管控与故障(第五、六章)」逐文件展开。Go 文件引用的标签(tenant/app/env/cluster)与 YAML / recording rules 完全一致。


三、底层架构联动设计

3.1 与多 K8s 集群 / API 网关的交互

各集群的 Prometheus Agent 以 StatefulSet 部署在租户 Namespace 之外(通常 monitoring 命名空间),只采集本集群资源。中心 Prometheus 通过统一 K8s API 网关反向隧道访问各集群 Agent 的 /federate 端点——Agent 的 Service 不对外暴露公网,网关侧按 RBAC 校验「联邦读取」权限后才放行。这样既满足物理隔离,又无需打通各集群 Underlay 网络。

同时,边缘集群的 Agent 还可 remote_write 到中心 Thanos Receiver,作为联邦拉取的补充通道(适合指标量大、pull 不稳定的场景)。

3.2 ★ 联邦拓扑联动图(必画)

下面这张图是本章核心,请结合 3.1、3.3 一起看:边缘 Agent 采集 → 中心 Prometheus 联邦拉取 → Thanos Sidecar/Receiver 上传对象存储 → Thanos Query 聚合 → Grafana / Alertmanager 消费。

graph LR
  subgraph Prod[生产集群]
    PA[(Prometheus Agent)]
  end
  subgraph Test[测试集群]
    TA[(Prometheus Agent)]
  end
  subgraph Center[中心管控集群]
    CP[(中心 Prometheus 联邦)]
    TS[(Thanos Sidecar)]
    TR[(Thanos Receiver)]
    TQ[(Thanos Query)]
  end
  OS[(对象存储)]
  G[(Grafana)]
  AM[(Alertmanager)]
  PA -- federation 拉取 --> CP
  TA -- federation 拉取 --> CP
  PA -- remote_write --> TR
  TA -- remote_write --> TR
  CP -- 本地 TSDB 块 --> TS
  TS -- 上传块 --> OS
  TR -- 上传块 --> OS
  OS -- 历史查询 --> TQ
  CP -- 近期查询 --> TQ
  TQ --> G
  TQ --> AM

这张图在讲什么:① Agent 模式让边缘只采不存,降中心压力;② Thanos 把「本地 TSDB 天花板」打破,解决长期存储;③ 标签贯穿全链路,使租户维度隔离与过滤在任意一层都生效。

三个关键设计点:

  • Agent 降中心压力:Agent 不开本地告警、不保留长周期数据,仅做 scrape + 指标转发。中心只拉「已经聚合的、带标签的」结果,连接数与样本量都可控。
  • Thanos 解决长期存储:Sidecar 随中心 Prometheus 同步上传块到对象存储,Receiver 接收边缘 remote_write 并上传,Store 提供历史块检索,Query 把「中心近期 + 存储历史」合并返回,保留期轻松到 90 天 / 1 年。
  • 标签实现租户隔离与过滤tenant/app/env/cluster 从 Agent 注入起就附着在每条时序上,Grafana 数据源过滤与 Alertmanager 路由都以这些标签为键,物理上保证「A 租户看不到 B 租户」。

3.3 与 RBAC / 审批 / 审计的联动

  • RBAC 细粒度:联邦读取、Grafana 文件夹、Alertmanager 静默操作都绑定平台角色;只读角色只能查自己租户,管理员才能建全局规则。
  • 多级审批:新建 / 修改全局 recording rulesalerting rules、Thanos 保留策略变更,必须走《模块 11 审批流》多级审批,审批通过才由网关下发。
  • 审计 90 天+:所有规则变更、大盘导出、告警静默、Query 高频拉取操作写入审计日志,留存 ≥ 90 天,满足金融合规。

3.4 K8s IaC(一):monitoring 命名空间 + Agent RBAC

Agent 以独立 ServiceAccount 运行,仅拥有本集群资源读取权限(绝不绑 cluster-admin)。

# file: deploy/00-namespace-rbac.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: monitoring
  labels:
    tenant: platform
    env: production
    cluster: prod-cluster-a
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: prom-agent
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: prom-agent
rules:
  - apiGroups: [""]
    resources: ["nodes", "nodes/metrics", "services", "endpoints", "pods"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "statefulsets", "daemonsets", "replicasets"]
    verbs: ["get", "list", "watch"]
  - nonResourceURLs: ["/metrics", "/federate"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: prom-agent
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prom-agent
subjects:
  - kind: ServiceAccount
    name: prom-agent
    namespace: monitoring

3.5 K8s IaC(二):Prometheus Agent ConfigMap(scrape + relabel 注入标签)

Agent 侧 relabel_configstn-<租户> 命名空间映射为 tenant,从 Pod label app.kubernetes.io/nameapp,并注入静态 env/cluster。缺标签指标后续在中心侧治理。

# file: deploy/01-prom-agent-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: prom-agent-config
  namespace: monitoring
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s
      evaluation_interval: 30s
      external_labels:
        cluster: prod-cluster-a
        env: production
    # Agent 模式:开 enable-feature=agent 时无本地存储/告警,仅采集+remote_write
    scrape_configs:
      - job_name: kube-pods
        kubernetes_sd_configs:
          - role: pod
        relabel_configs:
          # ① 从命名空间 tn-<租户> 提取 tenant 标签(★标签隔离核心)
          - source_labels: [__meta_kubernetes_namespace]
            regex: "tn-(.+)"
            target_label: tenant
            replacement: "$1"
          # ② 未遵循 tn- 前缀的命名空间,统一标记 tenant=unknown(供中心治理告警)
          #    regex: "" 仅匹配空值,即只有第一步没成功写入 tenant 时才落 unknown
          - source_labels: [tenant]
            regex: ""
            target_label: tenant
            replacement: unknown
          # ③ 从 Pod 标准 label 提取 app
          - source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
            target_label: app
            regex: "(.+)"
            replacement: "$1"
          # ④ 注入静态环境/集群标签(与 global.external_labels 双保险)
          - target_label: env
            replacement: production
          - target_label: cluster
            replacement: prod-cluster-a
          # 只保留有 tenant 的真实业务 Pod,过滤系统组件噪声
          - source_labels: [tenant]
            regex: "unknown"
            action: drop
        metric_relabel_configs:
          # 仅保留关键指标,降低联邦拉取体积
          - source_labels: [__name__]
            regex: "up|kube_pod_.*|container_cpu_.*|container_memory_.*|http_requests_total"
            action: keep

    # 可选:remote_write 到中心 Thanos Receiver(联邦 pull 的补充通道)
    remote_write:
      - url: http://thanos-receiver.monitoring.svc:19291/api/v1/receive
        send_exemplars: true
        queue_config:
          max_samples_per_send: 2000
          capacity: 100000    

⚠️ 命名空间必须遵循 tn-<租户名> 约定,否则 tenant 落空被标 unknown,中心侧会触发「标签缺失治理告警」。

3.6 K8s IaC(三):Prometheus Agent StatefulSet / Service / ServiceMonitor

# file: deploy/02-prom-agent-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: prom-agent
  namespace: monitoring
  labels:
    app: prom-agent
spec:
  serviceName: prom-agent
  replicas: 1
  selector:
    matchLabels:
      app: prom-agent
  template:
    metadata:
      labels:
        app: prom-agent
    spec:
      serviceAccountName: prom-agent
      containers:
        - name: prometheus
          image: quay.io/prometheus/prometheus:v2.51.2
          args:
            - "--config.file=/etc/prometheus/prometheus.yml"
            - "--storage.tsdb.path=/prometheus"
            - "--storage.tsdb.retention.time=2h"   # Agent 本地仅短缓存
            - "--web.enable-lifecycle"
            - "--enable-feature=agent"             # Agent 模式:无本地告警/长期存储
          ports:
            - containerPort: 9090
              name: http
            - containerPort: 9091
              name: federate                      # /federate 端点由 http 端口暴露
          volumeMounts:
            - name: config
              mountPath: /etc/prometheus
            - name: data
              mountPath: /prometheus
      volumes:
        - name: config
          configMap:
            name: prom-agent-config
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 5Gi
---
apiVersion: v1
kind: Service
metadata:
  name: prom-agent
  namespace: monitoring
  labels:
    app: prom-agent
spec:
  selector:
    app: prom-agent
  ports:
    - name: http
      port: 9090
      targetPort: 9090
    # 联邦拉取走 9090 /federate;网关只放行该端口
    - name: federate
      port: 9090
      targetPort: 9090
---
# ServiceMonitor 让中心以「服务发现」方式纳管本 Agent(可选,pull 模型下中心直接 scrape /federate)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: prom-agent
  namespace: monitoring
  labels:
    tenant: platform
    cluster: prod-cluster-a
spec:
  selector:
    matchLabels:
      app: prom-agent
  endpoints:
    - port: http
      path: /federate
      interval: 30s
      params:
        match[]:
          - '{tenant!=""}'

四、端到端标准操作流程

下面流程按角色拆分,且显式区分测试环境与生产环境的差异步骤。

4.1 角色与职责

  • 开发(租户):申请本租户监控接入,查看隔离大盘,认领本租户告警。
  • 集群运维:维护各集群 Agent、relabel 配置、联邦连通性。
  • 平台管理员:维护中心 Prometheus / Thanos、全局规则与告警路由、审批规则变更。

4.2 标准操作流程(SOP)

步骤 1|租户接入(集群运维 + 平台管理员)

  • 步骤 1.1 在目标集群 Agent 的 scrape 配置注入租户标签(参考 3.5 ②)。
  • 步骤 1.2 平台管理员在中心 Prometheus 增加联邦任务(参考 4.3)。
  • 步骤 1.3 **【生产环境】需走审批流新增联邦任务;【测试环境】**可由运维直接提交,免审批。

步骤 2|规则与告警配置(平台管理员)

  • 步骤 2.1 编写 recording rules 预计算,提交审批(生产必走,测试可免)。
  • 步骤 2.2 Alertmanager 路由按租户/环境/严重度分级;生产 critical 必须接电话通道。

步骤 3|租户查看与认领(开发)

  • 步骤 3.1 打开本租户 Grafana 文件夹,数据源已按 tenant 过滤;平台 Go 代理 federation-proxy 也会强制注入 tenant 过滤(见 4.8)。
  • 步骤 3.2 收到告警后在 IM 认领,超时未认领升级到平台管理员。

4.3 中心 Prometheus 联邦 scrape 配置(K8s IaC 四)

中心 Prometheus 通过 /federate 拉取各物理隔离集群 Agent 的指标,并用 honor_labels: true 保留 Agent 侧已注入的 tenant 等标签;match[]tenant!="" 兜底过滤噪声。

# file: deploy/03-central-federation-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: central-prometheus-config
  namespace: monitoring
data:
  prometheus.yml: |
    global:
      scrape_interval: 30s
      evaluation_interval: 30s
      external_labels:
        cluster: central
        env: production
    rule_files:
      - /etc/prometheus/rules/*.yml
    scrape_configs:
      # ===== 各物理隔离集群的联邦拉取任务 =====
      - job_name: federate-prod-cluster-a
        scrape_interval: 30s
        honor_labels: true        # ★ 保留 Agent 侧 tenant/app/env/cluster 标签
        metrics_path: /federate
        params:
          match[]:
            - '{__name__=~"up|kube_pod_.*|container_.*|http_requests_total"}'
            - '{tenant!=""}'      # 兜底过滤无标签噪声
        static_configs:
          - targets: ['prom-agent.monitoring.svc:9090']   # 实际经 API 网关隧道解析
        relabel_configs:
          - target_label: cluster
            replacement: prod-cluster-a

      - job_name: federate-prod-cluster-b
        scrape_interval: 30s
        honor_labels: true
        metrics_path: /federate
        params:
          match[]:
            - '{__name__=~"up|kube_pod_.*|container_.*|http_requests_total"}'
            - '{tenant!=""}'
        static_configs:
          - targets: ['prom-agent-prod-b.monitoring.svc:9090']
        relabel_configs:
          - target_label: cluster
            replacement: prod-cluster-b

      - job_name: federate-prestaging
        scrape_interval: 60s
        honor_labels: true
        metrics_path: /federate
        params:
          match[]:
            - '{tenant!=""}'
        static_configs:
          - targets: ['prom-agent-prestaging.monitoring.svc:9090']
        relabel_configs:
          - target_label: cluster
            replacement: prestaging-cluster

      - job_name: federate-testing
        scrape_interval: 60s
        honor_labels: true
        metrics_path: /federate
        params:
          match[]:
            - '{tenant!=""}'
        static_configs:
          - targets: ['prom-agent-testing.monitoring.svc:9090']
        relabel_configs:
          - target_label: cluster
            replacement: testing-cluster

      # 中心自身 Thanos Sidecar 暴露的本地指标(近期)
      - job_name: thanos-sidecar
        static_configs:
          - targets: ['thanos-sidecar.monitoring.svc:10902']    

⚠️ honor_labels: true 很关键:联邦拉取时若不加,Agent 侧的 tenant 等标签可能被中心覆盖。另外 match[]tenant!="" 兜底过滤,防止无标签噪声污染全局视图。

4.4 K8s IaC(五):Thanos Sidecar / Receiver / Query / Store 部署

Thanos 组件全部部署在中心管控集群 monitoring 命名空间;对象存储凭证经 KMS 信封加密后存 Secret。

# file: deploy/04-thanos.yaml
# ===== 对象存储凭证(KMS 信封加密后的 Secret,由网关解密下发)=====
apiVersion: v1
kind: Secret
metadata:
  name: thanos-objstore
  namespace: monitoring
type: Opaque
stringData:
  objstore.yaml: |
    type: s3
    config:
      bucket: paas-thanos-prod
      endpoint: oss.internal:443
      region: cn-north
      access_key: "{{KMS_DECIPHERED_AK}}"   # 占位,由网关注入真实值
      secret_key: "{{KMS_DECIPHERED_SK}}"    
---
# ===== Thanos Sidecar:随中心 Prometheus 上传 TSDB 块 =====
apiVersion: apps/v1
kind: Deployment
metadata:
  name: thanos-sidecar
  namespace: monitoring
  labels: {app: thanos-sidecar}
spec:
  replicas: 1
  selector:
    matchLabels: {app: thanos-sidecar}
  template:
    metadata:
      labels: {app: thanos-sidecar}
    spec:
      containers:
        - name: sidecar
          image: quay.io/thanos/thanos:v0.35.1
          args:
            - sidecar
            - --prometheus.url=http://central-prometheus.monitoring.svc:9090
            - --objstore.config-file=/etc/thanos/objstore.yaml
            - --tsdb.retention=2h
            - --grpc-address=0.0.0.0:10901
            - --http-address=0.0.0.0:10902
          ports:
            - {containerPort: 10901, name: grpc}
            - {containerPort: 10902, name: http}
          volumeMounts:
            - {name: objstore, mountPath: /etc/thanos}
      volumes:
        - name: objstore
          secret:
            secretName: thanos-objstore
---
# ===== Thanos Receiver:接收边缘 Agent remote_write =====
apiVersion: apps/v1
kind: Deployment
metadata:
  name: thanos-receiver
  namespace: monitoring
  labels: {app: thanos-receiver}
spec:
  replicas: 2
  selector:
    matchLabels: {app: thanos-receiver}
  template:
    metadata:
      labels: {app: thanos-receiver}
    spec:
      containers:
        - name: receiver
          image: quay.io/thanos/thanos:v0.35.1
          args:
            - receive
            - --receive.replication-factor=2
            - --receive.hashrings-file=/etc/thanos/hashrings.json
            - --objstore.config-file=/etc/thanos/objstore.yaml
            - --grpc-address=0.0.0.0:10901
            - --http-address=0.0.0.0:10902
            - --remote-write.address=0.0.0.0:19291
          ports:
            - {containerPort: 10901, name: grpc}
            - {containerPort: 10902, name: http}
            - {containerPort: 19291, name: remote-write}
          volumeMounts:
            - {name: objstore, mountPath: /etc/thanos}
      volumes:
        - name: objstore
          secret:
            secretName: thanos-objstore
---
# ===== Thanos Query:聚合 Sidecar + Receiver + Store =====
apiVersion: apps/v1
kind: Deployment
metadata:
  name: thanos-query
  namespace: monitoring
  labels: {app: thanos-query}
spec:
  replicas: 2
  selector:
    matchLabels: {app: thanos-query}
  template:
    metadata:
      labels: {app: thanos-query}
    spec:
      containers:
        - name: query
          image: quay.io/thanos/thanos:v0.35.1
          args:
            - query
            - --http-address=0.0.0.0:9090
            - --grpc-address=0.0.0.0:10901
            - --query.replica-label=replica
            - --store=thanos-sidecar.monitoring.svc:10901
            - --store=thanos-receiver.monitoring.svc:10901
            - --store=thanos-store.monitoring.svc:10901
          ports:
            - {containerPort: 9090, name: http}     # 暴露 Prometheus 兼容 HTTP API
            - {containerPort: 10901, name: grpc}    # 暴露 gRPC Store/Query
---
# ===== Thanos Store:提供历史块检索(对象存储)=====
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: thanos-store
  namespace: monitoring
  labels: {app: thanos-store}
spec:
  serviceName: thanos-store
  replicas: 2
  selector:
    matchLabels: {app: thanos-store}
  template:
    metadata:
      labels: {app: thanos-store}
    spec:
      containers:
        - name: store
          image: quay.io/thanos/thanos:v0.35.1
          args:
            - store
            - --objstore.config-file=/etc/thanos/objstore.yaml
            - --grpc-address=0.0.0.0:10901
            - --http-address=0.0.0.0:10902
            - --data-dir=/var/thanos/store
          ports:
            - {containerPort: 10901, name: grpc}
            - {containerPort: 10902, name: http}
          volumeMounts:
            - {name: objstore, mountPath: /etc/thanos}
            - {name: data, mountPath: /var/thanos/store}
      volumes:
        - name: objstore
          secret:
            secretName: thanos-objstore
  volumeClaimTemplates:
    - metadata: {name: data}
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests: {storage: 50Gi}

跨集群租户 QPS 聚合查询(走 Thanos Query HTTP API):

sum by (tenant, cluster) (rate(http_requests_total{env="production"}[5m]))

4.5 K8s IaC(六):Alertmanager 部署 + 分级路由与抑制

Alertmanager 按 severity + tenant + env 路由;inhibit_rules 实现「节点宕机抑制该节点 Pod 告警」,避免告警风暴。

# file: deploy/05-alertmanager.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: alertmanager-config
  namespace: monitoring
data:
  alertmanager.yml: |
    global:
      resolve_timeout: 5m
    route:
      receiver: 'default-webhook'
      group_by: ['tenant', 'env', 'cluster', 'alertname']
      group_wait: 30s
      group_interval: 5m
      repeat_interval: 4h
      routes:
        # 生产环境 P0/critical:电话 + 企业微信急件
        - matchers: ['severity="critical"', 'env="production"']
          receiver: 'prod-pager'
          repeat_interval: 30m
          continue: true
        # 预发布/测试 critical:仅急件不打电话
        - matchers: ['severity="critical"', 'env!="production"']
          receiver: 'nonprod-webhook'
          continue: true
        # 按租户二次分发到对应租户 IM 群
        - matchers: ['severity=~"warning|info"']
          receiver: 'ticket-system'
          group_wait: 2m
    receivers:
      - name: 'default-webhook'
        webhook_configs:
          - url: http://alert-hub.monitoring.svc:8080/v1/notify
      - name: 'prod-pager'
        webhook_configs:
          - url: http://alert-hub.monitoring.svc:8080/v1/pager
            send_resolved: true
      - name: 'nonprod-webhook'
        webhook_configs:
          - url: http://alert-hub.monitoring.svc:8080/v1/notify
      - name: 'ticket-system'
        webhook_configs:
          - url: http://alert-hub.monitoring.svc:8080/v1/ticket
    inhibit_rules:
      # ★ 节点不可用时,抑制该节点上的 Pod/工作负载副本告警
      - source_matchers: ['alertname="NodeDown"', 'severity="critical"']
        target_matchers: ['alertname=~"PodCrashLooping|KubePodNotReady"']
        equal: ['cluster', 'tenant', 'node']
      # 集群级采集中断时,抑制该集群全部业务告警(避免无数据误报)
      - source_matchers: ['alertname="FederationScrapeDown"']
        target_matchers: ['cluster=~".+"']
        equal: ['cluster']    
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: alertmanager
  namespace: monitoring
  labels: {app: alertmanager}
spec:
  replicas: 2
  selector:
    matchLabels: {app: alertmanager}
  template:
    metadata:
      labels: {app: alertmanager}
    spec:
      containers:
        - name: alertmanager
          image: quay.io/prometheus/alertmanager:v0.27.0
          args:
            - --config.file=/etc/alertmanager/alertmanager.yml
            - --storage.path=/alertmanager
          ports:
            - {containerPort: 9093, name: http}
          volumeMounts:
            - {name: config, mountPath: /etc/alertmanager}
      volumes:
        - name: config
          configMap:
            name: alertmanager-config

4.6 K8s IaC(七):Grafana 数据源 + 多租户文件夹(tenant 过滤)

Grafana 通过 provisioning 为每个租户建文件夹,并用「数据源代理层注入 tenant 查询头」实现隔离。平台侧由 Go(internal/grafana/datasource.go)动态创建。下列 YAML 为静态示例。

# file: deploy/06-grafana-provisioning.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-datasources
  namespace: monitoring
data:
  datasource-acme.yaml: |
    apiVersion: 1
    datasources:
      - name: thanos-acme
        type: prometheus
        # 走平台查询代理,由代理按 org(tenant) 注入标签过滤,实现数据级隔离
        url: http://federation-proxy.monitoring.svc:8080/api/v1
        access: proxy
        isDefault: true
        jsonData:
          httpHeaderName1: "X-Tenant"
          httpHeaderName2: "X-Env"
        secureJsonData:
          httpHeaderValue1: "acme"
          httpHeaderValue2: "production"    
  datasource-global.yaml: |
    apiVersion: 1
    datasources:
      - name: thanos-global
        type: prometheus
        url: http://thanos-query.monitoring.svc:9090
        access: proxy
        jsonData:
          httpHeaderName1: "X-Tenant"
        secureJsonData:
          httpHeaderValue1: "platform"   # 仅管理员可用全局源    
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-folders
  namespace: monitoring
data:
  folders.yaml: |
    apiVersion: 1
    folders:
      - title: "租户-acme"
        uid: tenant-acme
      - title: "租户-global"
        uid: tenant-global    

4.7 recording rules + alert rules(K8s IaC 收尾)

recording rules 预计算租户级指标;alert rules 覆盖标签缺失治理、联邦中断、节点宕机。这些规则名与 Go 侧告警路由(4.9)一致。

# file: deploy/07-rules.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: central-rules
  namespace: monitoring
data:
  recording.yml: |
    groups:
      - name: tenant_slo_recording
        interval: 30s
        rules:
          - record: job🆙sum_by_tenant
            expr: sum by (tenant, env, cluster) (up)
          - record: tenant:request_errors:rate5m
            expr: sum by (tenant) (rate(http_requests_total{code=~"5.."}[5m]))
          - record: tenant:request_qps:rate5m
            expr: sum by (tenant, cluster) (rate(http_requests_total[5m]))
          # 自动化任务成功率(供模块 06 复用,标签严格一致)
          - record: task:success_rate:rate7d
            expr: |
              sum by (tenant, env, cluster) (rate(task_executions_total{result="success"}[7d]))
              /
              sum by (tenant, env, cluster) (rate(task_executions_total[7d]))
      - name: node_recording
        interval: 30s
        rules:
          - record: instance:node_cpu_util:ratio
            expr: 1 - avg by (tenant, cluster, instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))    
  alerts.yml: |
    groups:
      - name: tenant_governance
        rules:
          # ★ 标签缺失治理:某租户带 unknown 标签的指标比例过高
          - alert: TenantLabelMissing
            expr: sum by (cluster) (up{tenant="unknown"}) / sum by (cluster) (up) > 0.05
            for: 10m
            labels:
              severity: warning
            annotations:
              summary: "集群 {{ $labels.cluster }} 存在大量 tenant=unknown 指标"
          # ★ 联邦采集中断
          - alert: FederationScrapeDown
            expr: up{job=~"federate-.*"} == 0
            for: 5m
            labels:
              severity: critical
            annotations:
              summary: "联邦任务 {{ $labels.job }} 拉取失败 (cluster={{ $labels.cluster }})"
          # 节点宕机(触发抑制源)
          - alert: NodeDown
            expr: up{job="kube-pods",tenant!="unknown"} == 0 and on(instance) kube_node_status_condition{condition="Ready",status="true"} == 0
            for: 3m
            labels:
              severity: critical
            annotations:
              summary: "节点 {{ $labels.instance }} 不可用"
          # 租户错误率超 SLO
          - alert: TenantErrorBudgetBurn
            expr: tenant:request_errors:rate5m / (tenant:request_qps:rate5m + 1) > 0.05
            for: 15m
            labels:
              severity: critical
            annotations:
              summary: "租户 {{ $labels.tenant }} 错误率超 5%"    

4.8 Go 联邦查询代理:gin 服务入口

平台 Go 服务 paas-observabilitygin 暴露查询 API,后端调用 Thanos Query 的 HTTP 与 gRPC 两种协议,并强制按 tenant 注入标签过滤。下面对应 2.4 目录树的各文件。

// file: cmd/federation-proxy/main.go
package main

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

	"paas-observability/internal/alerting"
	"paas-observability/internal/auth"
	"paas-observability/internal/grafana"
	"paas-observability/internal/thanos"
	"paas-observability/internal/thanos/grpc"

	"github.com/gin-gonic/gin"
)

// main 启动租户隔离的联邦查询代理。
// 所有查询都先经 auth.TenantMiddleware 解析租户身份,
// 再转发到 Thanos Query(HTTP / gRPC 两种后端)。
func main() {
	thanosHTTPAddr := getenv("THANOS_QUERY_HTTP_ADDR", "http://thanos-query.monitoring.svc:9090")
	thanosGRPCAddr := getenv("THANOS_QUERY_GRPC_ADDR", "thanos-query.monitoring.svc:10901")
	grafanaAddr := getenv("GRAFANA_ADDR", "http://grafana.monitoring.svc:3000")
	grafanaToken := os.Getenv("GRAFANA_TOKEN")

	httpClient := thanos.NewQueryClient(thanosHTTPAddr)
	grpcClient := grpcclient.NewGRPCClient(thanosGRPCAddr)
	grafanaClient := grafana.NewClient(grafanaAddr, grafanaToken)

	r := gin.Default()
	r.Use(auth.TenantMiddleware())

	v1 := r.Group("/api/v1")
	{
		// 即时查询:HTTP 后端
		v1.GET("/query", func(c *gin.Context) {
			tenant := c.GetString("tenant")
			admin := c.GetBool("admin")
			q := c.Query("query")
			if !admin {
				// ★ 非管理员强制按本租户过滤,物理隔离数据
				q = auth.InjectTenantLabel(q, tenant)
			}
			body, err := httpClient.Query(c.Request.Context(), q, c.Query("time"))
			if err != nil {
				c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
				return
			}
			c.Data(http.StatusOK, "application/json", body)
		})

		// 区间查询:HTTP 后端
		v1.GET("/query_range", func(c *gin.Context) {
			tenant := c.GetString("tenant")
			q := c.Query("query")
			if !c.GetBool("admin") {
				q = auth.InjectTenantLabel(q, tenant)
			}
			body, err := httpClient.QueryRange(c.Request.Context(), q, c.Query("start"), c.Query("end"), c.Query("step"))
			if err != nil {
				c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
				return
			}
			c.Data(http.StatusOK, "application/json", body)
		})

		// gRPC 后端即时查询(演示 Thanos Store/Query gRPC 调用)
		v1.GET("/query_grpc", func(c *gin.Context) {
			tenant := c.GetString("tenant")
			q := c.Query("query")
			if !c.GetBool("admin") {
				q = auth.InjectTenantLabel(q, tenant)
			}
			if err := grpcClient.Query(c.Request.Context(), q); err != nil {
				c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"grpc": "streamed", "tenant": tenant})
		})

		// 为某租户动态创建 Grafana 隔离数据源(★高级能力 ⑤)
		v1.POST("/tenants/:tenant/datasource", func(c *gin.Context) {
			if !c.GetBool("admin") {
				c.JSON(http.StatusForbidden, gin.H{"error": "only admin can provision datasources"})
				return
			}
			t := c.Param("tenant")
			if err := grafanaClient.CreateTenantDatasource(c.Request.Context(), t); err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"tenant": t, "status": "provisioned"})
		})
	}

	// 告警分级路由演示端点(与 Alertmanager 配置语义一致)
	v1.POST("/alert", func(c *gin.Context) {
		var a alerting.Alert
		if err := c.ShouldBindJSON(&a); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		receivers, pager := alerting.Route(a)
		c.JSON(http.StatusOK, gin.H{"receivers": receivers, "pager": pager})
	})

	addr := getenv("LISTEN_ADDR", ":8080")
	if err := r.Run(addr); err != nil {
		log.Fatalf("federation-proxy exit: %v", err)
	}
}

func getenv(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}

4.9 Go:gin 租户中间件 + 标签注入

// file: internal/auth/tenant.go
package auth

import (
	"strings"

	"github.com/gin-gonic/gin"
)

// TenantMiddleware 从 X-Tenant 头(由 API 网关注入并经 JWT 校验)解析租户身份。
// admin=true 时放行全局查询;否则强制注入 tenant 过滤。
func TenantMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		tenant := c.GetHeader("X-Tenant")
		if tenant == "" {
			tenant = "unknown"
		}
		admin := c.GetHeader("X-Role") == "platform-admin"
		c.Set("tenant", tenant)
		c.Set("admin", admin)
		c.Next()
	}
}

// InjectTenantLabel 在 PromQL 中强制注入 {tenant="X"} 过滤,保证数据级隔离。
// 若查询已含 tenant 选择器则不再重复注入。
func InjectTenantLabel(query, tenant string) string {
	if tenant == "" || tenant == "unknown" {
		return query
	}
	if strings.Contains(query, "tenant=") {
		return query
	}
	// 在最外层 {} 选择器里追加 tenant 匹配;无选择器则整体包裹
	if strings.Contains(query, "{") {
		return strings.Replace(query, "{", "{tenant=\""+tenant+"\",", 1)
	}
	return query + "{tenant=\"" + tenant + "\"}"
}

4.10 Go:Thanos Query HTTP 客户端

Thanos Query 暴露兼容 Prometheus 的 HTTP API,直接复用查询协议。

// file: internal/thanos/query.go
package thanos

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"time"
)

// QueryClient 封装 Thanos Query 的 Prometheus 兼容 HTTP API。
type QueryClient struct {
	BaseURL string
	HTTP    *http.Client
}

// NewQueryClient 构造 HTTP 查询客户端。
func NewQueryClient(baseURL string) *QueryClient {
	return &QueryClient{
		BaseURL: baseURL,
		HTTP:    &http.Client{Timeout: 30 * time.Second},
	}
}

// Query 调用 /api/v1/query 即时查询,返回原始 JSON(与 Prometheus 响应结构一致)。
func (c *QueryClient) Query(ctx context.Context, q, t string) ([]byte, error) {
	qry := url.Values{}
	qry.Set("query", q)
	if t != "" {
		qry.Set("time", t)
	}
	return c.get(ctx, "/api/v1/query?"+qry.Encode())
}

// QueryRange 调用 /api/v1/query_range 区间查询。
func (c *QueryClient) QueryRange(ctx context.Context, q, start, end, step string) ([]byte, error) {
	qry := url.Values{}
	qry.Set("query", q)
	qry.Set("start", start)
	qry.Set("end", end)
	if step == "" {
		step = "60"
	}
	qry.Set("step", step)
	return c.get(ctx, "/api/v1/query_range?"+qry.Encode())
}

func (c *QueryClient) get(ctx context.Context, path string) ([]byte, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+path, nil)
	if err != nil {
		return nil, err
	}
	resp, err := c.HTTP.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("thanos query http %d: %s", resp.StatusCode, string(body))
	}
	return body, nil
}

4.11 Go:Thanos Query gRPC 客户端(Store/Query)

通过 Thanos 的 storepb.StoreClient.Query gRPC 流式接口拉取时序,演示「Go 调用 Thanos gRPC」。

// file: internal/thanos/grpc/grpc_query.go
package grpcclient

import (
	"context"
	"io"
	"time"

	"github.com/thanos-io/thanos/pkg/store/storepb"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

// GRPCClient 封装 Thanos Query/Sidecar 的 gRPC Store API。
type GRPCClient struct {
	addr string
}

// NewGRPCClient 构造 gRPC 客户端(生产应启用 TLS+mTLS)。
func NewGRPCClient(addr string) *GRPCClient {
	return &GRPCClient{addr: addr}
}

// Query 通过 gRPC 流式查询,打印每个 Series 的租户标签,验证跨集群聚合。
func (g *GRPCClient) Query(ctx context.Context, query string) error {
	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	conn, err := grpc.DialContext(ctx, g.addr,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithBlock(),
	)
	if err != nil {
		return err
	}
	defer conn.Close()

	cli := storepb.NewStoreClient(conn)
	req := &storepb.QueryRequest{
		Query: query,
		// 聚合所有副本(与 --query.replica-label=replica 一致)
		ReplicaLabels: []string{"replica"},
	}

	stream, err := cli.Query(ctx, req)
	if err != nil {
		return err
	}
	for {
		resp, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		// resp 含租户维度标签,可在平台侧做二次过滤/计费
		ts := resp.GetTimeseries()
		if ts == nil {
			continue
		}
		tenant := ""
		for _, l := range ts.GetLabels() {
			if l.GetName() == "tenant" {
				tenant = l.GetValue()
			}
		}
		_ = tenant
	}
}

4.12 Go:Grafana 按 tenant 动态数据源

// file: internal/grafana/datasource.go
package grafana

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"
)

// Client 封装 Grafana HTTP API,按租户创建隔离数据源。
type Client struct {
	BaseURL string
	Token   string
	HTTP    *http.Client
}

// NewClient 构造 Grafana 客户端。
func NewClient(baseURL, token string) *Client {
	return &Client{
		BaseURL: baseURL,
		Token:   token,
		HTTP:    &http.Client{Timeout: 20 * time.Second},
	}
}

// datasourcePayload 为 Grafana 数据源创建请求体。
// 关键:url 指向平台查询代理,并通过 X-Tenant 头让代理强制注入 tenant 过滤。
type datasourcePayload struct {
	Name      string            `json:"name"`
	Type      string            `json:"type"`
	URL       string            `json:"url"`
	Access    string            `json:"access"`
	IsDefault bool              `json:"isDefault"`
	JSONData  map[string]string `json:"jsonData"`
	Secure    map[string]string `json:"secureJsonData"`
}

// CreateTenantDatasource 为某租户创建隔离数据源(★数据级隔离)。
func (c *Client) CreateTenantDatasource(ctx context.Context, tenant string) error {
	payload := datasourcePayload{
		Name:      "thanos-" + tenant,
		Type:      "prometheus",
		URL:       "http://federation-proxy.monitoring.svc:8080/api/v1",
		Access:    "proxy",
		IsDefault: true,
		JSONData: map[string]string{
			"httpHeaderName1": "X-Tenant",
			"httpHeaderName2": "X-Env",
		},
		Secure: map[string]string{
			"httpHeaderValue1": tenant,
			"httpHeaderValue2": "production",
		},
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		c.BaseURL+"/api/datasources", bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+c.Token)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.HTTP.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		b, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("grafana create datasource http %d: %s", resp.StatusCode, string(b))
	}
	return nil
}

4.13 Go:告警分级路由

Go 侧告警路由逻辑与 4.5 的 Alertmanager 配置语义完全一致,可作为网关侧预路由 / 审计使用。

// file: internal/alerting/router.go
package alerting

// Alert 与 Prometheus alert 标签对齐,字段名与 4.7 alert rules 一致。
type Alert struct {
	Tenant   string `json:"tenant"`
	Env      string `json:"env"`
	Cluster  string `json:"cluster"`
	Severity string `json:"severity"`
	Summary  string `json:"summary"`
}

// Route 根据 severity + env 决定接收方;返回接收方列表与是否触发电话(P0)。
// 逻辑与 deploy/05-alertmanager.yaml 的 route 配置一一对应。
func Route(a Alert) (receivers []string, pager bool) {
	switch {
	case a.Severity == "critical" && a.Env == "production":
		// 生产 P0:电话 + 企业微信急件
		return []string{"prod-pager", "alert-hub"}, true
	case a.Severity == "critical":
		// 非生产 critical:仅急件
		return []string{"nonprod-webhook"}, false
	case a.Severity == "warning" || a.Severity == "info":
		// 低级别:进工单
		return []string{"ticket-system"}, false
	default:
		return []string{"default-webhook"}, false
	}
}

4.14 Go 依赖与构建

// file: go.mod
module paas-observability

go 1.21

require (
	github.com/gin-gonic/gin v1.9.1
	github.com/thanos-io/thanos v0.35.1
	google.golang.org/grpc v1.60.1
)
# 构建并运行联邦查询代理(需先 go mod tidy 拉取依赖)
cd paas-observability
go mod tidy
go build ./...
go run ./cmd/federation-proxy

# 以租户 acme 身份查询(代理会强制注入 {tenant="acme"})
curl -H "X-Tenant: acme" -H "X-Role: tenant" \
  'http://localhost:8080/api/v1/query?query=sum(rate(http_requests_total[5m]))'

# 以平台管理员查询全局
curl -H "X-Tenant: platform" -H "X-Role: platform-admin" \
  'http://localhost:8080/api/v1/query?query=sum by (tenant,cluster)(rate(http_requests_total[5m]))'

4.15 端到端时序图

sequenceDiagram
  participant Dev as 开发(租户)
  participant GW as 统一 K8s API 网关
  participant PX as 联邦查询代理(Go)
  participant TQ as Thanos Query
  participant Adm as 平台管理员
  participant G as Grafana
  Dev->>GW: 携带 X-Tenant 的查询请求
  GW->>PX: 转发并校验 RBAC
  PX->>PX: 注入 {tenant="acme"} 过滤
  PX->>TQ: HTTP/gRPC 查询
  TQ-->>PX: 返回隔离数据
  PX-->>Dev: 仅本租户曲线
  Adm->>G: 审批通过后下发租户标签配置
  GW->>G: 联邦拉取带 tenant 标签指标
  G->>Dev: 渲染多租户隔离大盘

这张图在讲什么:从「申请」到「看到自己数据」的全链路,审批是生产环境不可绕过的闸口,Go 代理的标签注入是隔离的技术支点。


五、生产环境管控与安全约束

5.1 资源配额与权限隔离

  • 中心 Prometheus / Thanos 独占高 IO 节点(本地 NVMe + 大内存),不与业务混部。
  • Grafana 按租户文件夹授权,数据源强制 tenant 过滤(见 4.6 / 4.12);Alertmanager 静默操作绑定角色,普通租户只能静默自己的告警。
  • 联邦读取、规则变更、保留策略修改全部走 RBAC,且敏感操作须二次认证
  • 平台 Go 代理 federation-proxy 的 admin 角色仅限 X-Role: platform-admin,由 API 网关签发 JWT 校验,普通租户 token 无法越权查询他人数据(见 4.9 InjectTenantLabel)。

5.2 敏感操作审批与审计

  • 全局 recording/alerting rules、Thanos 保留期、Grafana 全局数据源变更 → 多级审批(模块 11),审批事件进操作审计,留存 ≥ 90 天。
  • 所有 Query 高频拉取、大盘导出、告警静默写入审计日志,运维越权操作可被回溯。
  • Go 代理对每次查询记录 tenant、查询语句、结果大小到审计管道,满足「谁查了什么」可追溯。

5.3 故障熔断策略

  • 单集群 Agent 失联:中心联邦任务标记 up=0 并触发「FederationScrapeDown」告警(见 4.7),且 inhibit_rules 抑制该集群业务误报;网关侧自动摘除异常集群 Endpoint。
  • Thanos Store 不可用:Query 降级只返回中心近期数据,并在 Grafana 标注「历史数据降级」。
  • 对象存储写入失败:Sidecar/Receiver 本地缓冲,超阈值告警,避免拖垮中心 Prometheus。

5.4 测试环境 vs 生产环境 差异化管控对照表

管控项测试环境生产环境
联邦任务新增运维直接提交,免审批必须走多级审批流
指标保留期15 天(省成本)≥ 90 天(合规),Thanos 对象存储
告警通道仅测试 IM 群P0 电话 + 企业微信急件 + 工单
标签缺失处理unknown 仅记录触发治理告警 + 阻断该租户大盘
规则变更可直接改审批 + 审计留痕 + 二次认证
大盘导出允许受 RBAC 限制,导出留审计
静默权限运维可静默全局仅本租户 / 管理员,且留审计
查询代理越权放宽 admin强制 X-Role 校验 + 标签注入

六、常见生产故障与解决方案

6.1 高频故障清单

故障 1|某租户大盘突然无数据

  • 现象:Grafana 本租户面板全空,但其他租户正常。
  • 排查:先确认是该租户 tenant 标签缺失(relabel 正则不匹配 tn-*),还是 Agent 联邦拉取失败。
  • 优化:在中心加「标签缺失治理告警」;命名空间强制 tn- 前缀校验(结合模块 07 准入)。

故障 2|中心 Prometheus 抓取连接数暴涨

  • 现象:中心 CPU 飙高,联邦任务超时。
  • 排查:多为 match[] 过宽拉了全量原始指标。收紧为正则 + tenant!=""
  • 优化:边缘改用 Agent remote_write 到 Thanos Receiver,中心只做聚合查询(见 4.4 Receiver)。

故障 3|历史曲线查不到 30 天前数据

  • 现象:Thanos Query 近期有、历史空。
  • 排查:Sidecar 未上传块或 Store 未注册到 Query --store
  • 优化:核对对象存储凭证(KMS 信封加密)、Store 健康与 Query 启动参数。

故障 4|告警风暴(一个节点宕机引发上百条 Pod 告警)

  • 现象:IM 被刷屏。
  • 排查:缺 inhibit_rules
  • 优化:配置抑制规则「节点不可用 抑制 该节点 Pod 告警」(见 4.5)。

6.2 运维 runbook(真实命令)

注册新集群到联邦(生产需先审批)

# 1. 在目标集群部署 Agent(见 deploy/00~02),确认 /federate 可达
kubectl -n monitoring get pods -l app=prom-agent
kubectl -n monitoring port-forward svc/prom-agent 9090:9090 &
curl 'http://localhost:9090/federate?match[]={tenant!=""}' | head

# 2. 在中心 Prometheus 增加 federate-<cluster> job(编辑 03-central-federation-configmap.yaml)
kubectl -n monitoring edit configmap central-prometheus-config
kubectl -n monitoring rollout restart deployment central-prometheus

# 3. 验证联邦 target 健康
kubectl -n monitoring exec deploy/central-prometheus -- \
  wget -qO- 'http://localhost:9090/api/v1/targets' | jq '.data.activeTargets[] | select(.scrapePool|test("federate"))'

联邦排查:up==0 / relabel 丢失标签

# 检查某联邦任务 target 是否 up
curl -s 'http://thanos-query.monitoring.svc:9090/api/v1/query?query=up{job=~"federate-.*"}' | jq .

# 在 Agent 侧确认 tenant 标签是否已注入(relabel 是否生效)
kubectl -n monitoring exec deploy/prom-agent -- \
  wget -qO- 'http://localhost:9090/api/v1/label/tenant/values'

# relabel 丢失标签常见原因:命名空间未用 tn- 前缀
kubectl get ns | grep -v '^tn-'   # 找出未隔离的租户命名空间

Thanos Store 延迟 / 历史为空

# 查看 Query 已连接的 Store 节点
thanos query --help >/dev/null
curl -s 'http://thanos-query.monitoring.svc:9090/api/v1/stores' | jq '.data.stores[] | {name,lastSync,minTime,maxTime}'

# 查看 Sidecar 上传对象存储是否报错
kubectl -n monitoring logs deploy/thanos-sidecar | grep -i 'upload\|error'

# 对象存储凭证(KMS 解密后)是否就绪
kubectl -n monitoring get secret thanos-objstore -o jsonpath='{.data.objstore\.yaml}' | base64 -d

触发 / 验证告警分级

# 手动注入一条 critical+production 告警,验证走 prod-pager 通道
curl -XPOST http://federation-proxy.monitoring.svc:8080/api/v1/alert -H 'Content-Type: application/json' -d '{
  "tenant":"acme","env":"production","cluster":"prod-cluster-a",
  "severity":"critical","summary":"manual test"
}'
# 期望返回 receivers=["prod-pager","alert-hub"], pager=true

# 查看 Alertmanager 当前告警与抑制
kubectl -n monitoring port-forward svc/alertmanager 9093:9093 &
curl 'http://localhost:9093/api/v2/alerts' | jq '.[] | {labels, status}'

查询代理租户隔离验证

# 租户 acme 应只能查到 tenant="acme"
curl -s -H "X-Tenant: acme" -H "X-Role: tenant" \
  'http://federation-proxy.monitoring.svc:8080/api/v1/query?query=up' \
  | jq '.data.result[].metric.tenant' | sort -u
# 期望仅输出 "acme"

# 越权尝试(无 admin)查询他人应被注入过滤,返回空
curl -s -H "X-Tenant: acme" -H "X-Role: tenant" \
  'http://federation-proxy.monitoring.svc:8080/api/v1/query?query=up{tenant="other"}'

6.3 故障排查流程图

flowchart TD
  S[告警: 指标缺失 / 曲线断点] --> C{缺失范围}
  C -->|单租户| T1[检查 Agent relabel 配置]
  C -->|全集群| T2[检查联邦拉取链路]
  C -->|仅历史| T3[检查 Thanos Store/对象存储]
  T1 --> R1[修复 tenant 标签注入]
  T2 --> R2[检查网关与中心连通性]
  T3 --> R3[检查 Sidecar 上传与凭证]
  R1 --> E[恢复数据]
  R2 --> E
  R3 --> E

这张图在讲什么:按「范围」二分定位,单租户查标签、全集群查链路、仅历史查 Thanos,避免无头苍蝇式排障。


自测题与动手练习

5 道自测题

  1. 联邦架构相比「每集群一套独立 Prometheus」解决了哪三个核心问题?
  2. tenant/app/env/cluster 四个标签分别由谁注入、在哪一层生效、缺失时如何处理?
  3. Thanos 的 Sidecar 与 Receiver 在联邦架构中分别承担什么角色?对象存储在哪一步被写入?
  4. Alertmanager 的 inhibit_rules 解决了什么生产痛点?举一个金融场景的例子。
  5. 测试环境与生产环境在「联邦任务新增」和「指标保留期」上有哪些差异化管控?Go 代理如何强制租户隔离?

3 个动手练习

  1. 给你一个测试集群的 Prometheus Agent,写一段 relabel_configstn-acme 命名空间映射成 tenant="acme",并加 env=testingcluster=test-b
  2. 在中心 Prometheus 写一段联邦任务,只拉取 kube_pod_*tenant!="" 的指标,并用 match[] 表达。
  3. 用 Thanos Query 启动参数把 prod-aprod-btest-b 三个 --store 聚合起来,写一条跨集群按 tenant 聚合的 QPS PromQL;并用 federation-proxy 的 gRPC 端点验证跨集群聚合。

本章小结

  • 统一观测平台以「联邦采集 + Thanos 长期存储 + 标签隔离」覆盖多集群统一视图、90 天+ 留存、租户数据级隔离三大合规诉求,是整套 PaaS 白皮书的核心。
  • ★ 三件不可删减的事:联邦架构(中心拉边缘、Agent 降压力)、标签隔离(tenant/app/env/cluster 贯穿全链路)、告警分级(按租户/环境/严重度路由 + 抑制)。
  • 监控内容必须绑定联邦:从 Agent relabel 注入(deploy/01),到中心 federation 拉取(deploy/03),到 Thanos Query 聚合(deploy/04),再到 Grafana/Alertmanager 消费,标签始终附着在时序上。
  • Go 联邦查询代理paas-observability)用 gin 提供租户隔离查询 API:HTTP 后端调用 Thanos Query /api/v1/query(internal/thanos/query.go),gRPC 后端调用 storepb.StoreClient.Query(internal/thanos/grpc_query.go),auth.InjectTenantLabel 强制按 tenant 过滤,grafana.CreateTenantDatasource 动态建隔离数据源,alerting.Route 与 Alertmanager 配置语义一致。
  • RBAC、多级审批、审计 90 天+ 贯穿规则变更、大盘导出、告警静默等敏感操作;测试 / 生产在审批、保留期、告警通道上必须差异化。
  • 下一篇《模块 06 运维自动化与定时任务》将复用本模块的联邦指标与告警能力,把「任务执行耗时 / 失败率」也纳入统一观测(见 deploy/07 的 task:success_rate:rate7d)。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!