学习目标
类比:把统一观测平台想象成写字楼的「中央消防与能耗监控室」。每间办公室(租户 Namespace)自己有烟感和电表(Prometheus Agent),但物业(平台)不能挨个房间翻看——而是把这些数据汇总到中央监控室(中心 Prometheus 联邦 + Thanos),按办公室编号(tenant 标签)分别成图、分别告警。这样既统一视图,又互不串门。
学完本章你应该能够:
- 说清楚「各集群 Prometheus Agent → 中心 Prometheus 联邦 + Thanos → Object Storage → Query → Grafana/Alertmanager」这条数据链路,以及每一跳解决什么问题。
- 在中心 Prometheus 上编写联邦(federation)抓取配置,从多套物理隔离集群按
match[]拉取带tenant/app/env/cluster标签的指标。 - 用
relabel_configs在 Agent 侧注入租户 / 环境 / 集群标签,实现指标维度的租户隔离与过滤。 - 配置
recording rules、Thanos Query 跨集群聚合查询,以及 Alertmanager 按租户 / 环境 / 严重度的分级路由与抑制。 - 在测试环境与生产环境之间正确区分联邦纳管、长期存储保留期、告警通道与审批边界。
- 动手写出可编译的 Go 联邦查询代理:用
gin提供租户隔离的查询 API,后端调用 Thanos Query 的 HTTP/api/v1/query与 gRPCStore/Query两种协议,并按tenant强制注入标签过滤。
前置知识:
- 已掌握《模块 02 多 K8s 集群纳管》(理解统一 K8s API 网关、物理隔离集群、Namespace 租户隔离)。
- 熟悉 Prometheus 基础:
scrape_configs、relabel_configs、recording rules、alerting rules。 - 了解 Thanos 组件(Sidecar / Receiver / Query / Store)与对象存储(S3 / OSS / MinIO)基本概念。
- 了解 RBAC、审计日志、多级审批在平台中的落地方式。
- Go 基础:
ginWeb 框架、client-go基本用法、gRPC 客户端调用。
本章你会动手做的事:
- 给一个生产集群的 Prometheus Agent 加上
relabel,把tn-*命名空间映射成tenant标签。 - 在中心 Prometheus 写一段联邦抓取任务,只拉取带
tenant!=""的关键指标。 - 用 Thanos Query 的
--store把两个集群的历史数据做一次跨集群 QPS 聚合。 - 编译运行 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 rules、alerting 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_configs 把 tn-<租户> 命名空间映射为 tenant,从 Pod label app.kubernetes.io/name 取 app,并注入静态 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-observability 用 gin 暴露查询 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.9InjectTenantLabel)。
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 道自测题
- 联邦架构相比「每集群一套独立 Prometheus」解决了哪三个核心问题?
tenant/app/env/cluster四个标签分别由谁注入、在哪一层生效、缺失时如何处理?- Thanos 的 Sidecar 与 Receiver 在联邦架构中分别承担什么角色?对象存储在哪一步被写入?
- Alertmanager 的
inhibit_rules解决了什么生产痛点?举一个金融场景的例子。 - 测试环境与生产环境在「联邦任务新增」和「指标保留期」上有哪些差异化管控?Go 代理如何强制租户隔离?
3 个动手练习
- 给你一个测试集群的 Prometheus Agent,写一段
relabel_configs把tn-acme命名空间映射成tenant="acme",并加env=testing、cluster=test-b。 - 在中心 Prometheus 写一段联邦任务,只拉取
kube_pod_*且tenant!=""的指标,并用match[]表达。 - 用 Thanos Query 启动参数把
prod-a、prod-b、test-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)。