云原生可观测性:Prometheus、Thanos、Loki 与 SLO 实践

2022-02-11T10:00:00+08:00 | 17分钟阅读 | 更新于 2022-02-11T10:00:00+08:00

@
可观测性体系指标采集SLO 管理日志系统Agent 瘦身错误预算Vector 路由Thanos 多租户Pyrra 燃尽图Pyroscope profiling

云原生可观测性:Prometheus、Thanos、Loki 与 SLO 实践

本文按"教材"方式组织:先给一个生活化类比建立直觉,再展开技术细节、架构图与可运行示例,最后用自测题与小结收口。覆盖可观测性平台与 SLI/SLO 的核心面试知识点。

可观测性通常被概括为"日志(Logs)、指标(Metrics)、链路(Traces)“三大支柱,但在云原生大规模场景下,真正的难点不是"采集”,而是降本、隔离、告警噪声治理、跨云归一化。下面分五章逐一拆解。


一、Prometheus 多租户与降本

直觉:把监控平台想象成"小区供电"

一个小区里住着很多租户(业务线 / 团队)。如果每家每户都拉一条独立电缆、配一个独立变压器,成本会爆炸。现实做法是:建一个总配电房(Thanos Receive / 全局存储),再用"分户电表 + 电流上限(限速)“保证某家开空调不会把整栋楼电闸拉垮。

Prometheus 在 label 维度超过 50 万后,内存会从"线性增长"变成"雪崩式增长”——因为每多一个时间序列(series),都要在内存里维护一份 head chunk、索引和倒排表。这一章解决三件事:远端瘦身(Agent)、统一汇聚(Thanos)、按租户限流(WAL 限速)

1.1 当 label 维度超过 50 万,用 Prometheus Agent + 流式聚合降低内存

问题本质:完整 Prometheus 在本地既要采集、又要查询、还要落盘(TSDB)。单机内存扛不住高基数(high cardinality)数据。

解法:把完整 Prometheus 拆成两种角色:

  • Prometheus Agent:只采集、只 remote-write 转发,不落盘、不提供本地查询。内存里只保留"当前在采的 series",没有 TSDB head 的沉重负担。
  • 流式聚合(stream aggregation):在 Agent 与远端存储之间加一层聚合网关(如 Prometheus 的 remote_write + 聚合规则,或 Thanos 的 rule / Grafana Mimir 的 ruler)。把"每请求 1 条"的原始指标,在转发前就聚合成"每租户 1 条 QPS、P99",大幅削减 series 数量。

下面是一份 Agent 模式的最小 prometheus.yml

# prometheus-agent.yml —— 只采集、只转发,不本地查询
global:
  scrape_interval: 15s
  external_labels:
    cluster: prod-bj       # 租户/集群维度,下游据此软隔离
    tenant: team-payment

# 关键:启用 Agent 模式(Prometheus 2.32+)
agent:
  enabled: true            # 关闭本地 TSDB,内存只留采集态

scrape_configs:
  - job_name: app
    static_configs:
      - targets: ['app:9090']

remote_write:
  - url: http://thanos-receive:19291/api/v1/receive
    # 发送端做流式聚合前的预降采样,减少 series
    queue_config:
      max_samples_per_send: 2000
      capacity: 5000

经验值:一套 50 万 series 的完整 Prometheus 可能吃 30GB+ 内存;同样数据量下 Agent + 远端存储的 Agent 侧通常 < 2GB。降低的内存主要来自"不维护 TSDB head 与倒排索引"。

1.2 基于 Thanos Receive HashRing 的多租户软隔离架构

Thanos Receive 是"接收远端写入并统一存储"的组件。多租户下,多个 Receive 实例组成哈希环(HashRing),按 tenant label 决定一份数据落到哪个 Receive 实例——这就是"软隔离":不同租户的数据物理分布在不同实例上,互不抢占内存与磁盘,但共享同一套控制面。

flowchart LR
    A[业务 Pod] -->|remote_write| B[Agent Prometheus]
    C[业务 Pod] -->|remote_write| D[Agent Prometheus]
    B --> E[Thanos Receive 实例 1\n租户: payment]
    D --> F[Thanos Receive 实例 2\n租户: search]
    E --> G[(Object Store S3\n分 tenant 前缀)]
    F --> G
    G --> H[Thanos Query\n统一查询入口]
    H --> I[Grafana]
    E -.HashRing 选主/复制.-> F

Receive 的 HashRing 配置片段:

# thanos-receive.yml 关键配置
receive:
  replication-factor: 2        # 每个租户数据在环上复制 2 份,防单点
  hashrings:
    - hashring: default
      tenants: [team-payment, team-search, team-iot]  # 声明纳管的租户
      endpoints:
        - thanos-receive-0:10901
        - thanos-receive-1:10901
        - thanos-receive-2:10901

软隔离 vs 硬隔离:硬隔离是"每个租户一套独立 Prometheus + 独立存储",成本翻倍;软隔离是"逻辑分租户、物理共用",靠 HashRing 把热点租户分散到不同实例,靠 external_labels.tenant 做查询路由与配额核算。面试常问"为什么不直接每租户一套"——答案就是成本与运维复杂度。

1.3 对 Prometheus WAL 做按租户限速,避免 noisy neighbor

即使有 HashRing,单个 Receive 实例上仍可能挤着多个租户。某租户突发灌量(比如大促压测),会把 WAL(Write-Ahead Log)写满、把 CPU 占满,拖累同实例的其他租户——这就是 noisy neighbor(吵闹的邻居)

Thanos Receive 支持在 tenant_matchers 上施加写入限速(rate limit),对超标的租户直接丢样本或返回 429,保护整体可用性:

receive:
  tenant_matchers:
    - match: 'tenant="team-payment"'
      # 该租户每秒最多 50 万样本,超出则限流
      rate_limit: 500000
    - match: 'tenant="team-iot"'
      # IoT 设备量大但优先级低,给更小配额
      rate_limit: 100000

取舍:限速会丢样本(监控数据允许一定丢失),但比起"整实例雪崩导致所有租户失联",局部降级是更优解。生产上通常配合 capacity 队列与 retry,让短时尖峰在客户端缓一缓再发。


二、基于 SLO 的错误预算与告警

直觉:错误预算就是"这个月能宕机多久"

SLO(Service Level Objective)不是"99.999% 永远在线"这种口号,而是一个可量化的预算。例如约定"月度成功率 99.9%",那么一个月(约 43,200 分钟)允许出错的时间是 43.2 分钟。这 43.2 分钟就是"错误预算(Error Budget)"。预算花得快,说明系统不稳,就该冻结发布、集中排障;预算还有很多,说明可以大胆灰度。

告警如果直接对"错误率"报警,会淹没在噪声里。SLO 体系里真正该报警的是**“错误预算还够不够用”**。

2.1 用 Pyrra + Slack 实现错误预算燃尽图自动推送

Pyrra 是一个把 SLO 定义自动编译成 Prometheus 记录规则(recording rules)和告警规则的工具,并自带燃尽图(burn-rate)面板。

一份面向支付服务的 SLO 定义:

# slo-payment.yaml —— Pyrra 的 SLO CRD
apiVersion: pyrra.dev/v1alpha1
kind: ServiceLevelObjective
metadata:
  name: payment-availability
  namespace: monitoring
spec:
  target: "99.9"            # 月度目标 99.9%
  window: 30d               # 滚动窗口 30 天
  indicator:
    prometheus:
      # SLI:成功响应数 / 总响应数
      total:
        query: sum(rate(http_requests_total{job="payment",code!~"5.."}[2m]))
      errors:
        query: sum(rate(http_requests_total{job="payment",code=~"5.."}[2m]))
  alerting:
    name: PaymentErrorBudgetBurn
    page: true              # 预算快速燃尽时直接 paging

Pyrra 会据此生成 multi-window multi-burn-rate 告警规则(Google SRE 推荐的标准做法)。配合 Slack Incoming Webhook,把燃尽图定时推到值班群:

#!/usr/bin/env bash
# push-burnrate.sh —— 每天 09:00 把燃尽图推到 Slack
set -euo pipefail

# Grafana 渲染燃尽图(需启用渲染服务或 iframe)
DASH_URL="https://grafana.internal/d/slo-payment/burnrate?from=now-30d&to=now&kiosk"
IMG=$(curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" "$DASH_URL" -o /tmp/burn.png && echo /tmp/burn.png)

# 推送到 Slack
curl -s -X POST -H 'Content-type: application/json' \
  --data "{\"text\":\"📉 支付服务 30 天错误预算燃尽图:<@oncall> 请关注剩余额度\"}" \
  "$SLACK_WEBHOOK"
flowchart LR
    S[Prometheus] -->|指标| P[Pyrra 生成规则]
    P --> R[(Recording Rules)]
    R --> B[错误预算燃尽率]
    B --> G[Grafana 燃尽图]
    G -->|每日推送| K[Slack 值班群]
    B -->|预算超速| A[Alertmanager Paging]

2.2 错误预算消耗 80% 自动建 Jira Ticket 并 @值班

当预算燃烧到 80%,不应只是发个消息,而应自动开单、指派、@人,把"该处理"变成"已排期"。这一般用 Alertmanager 的 webhook receiver 接到自动化服务(如用一小段 Go/Python 服务接收告警,再调 Jira API)。

Alertmanager 侧配置:

# alertmanager.yml
route:
  receiver: 'jira-automation'
  matchers:
    - alertname = "PaymentErrorBudgetBurn"
receivers:
  - name: jira-automation
    webhook_configs:
      - url: http://slo-bot:8080/alert    # 自动化服务地址

自动化服务(伪代码,可运行骨架):

# slo_bot.py —— 接收告警,预算超 80% 时建 Jira 并 @值班
import os, requests, json
from flask import Flask, request

app = Flask(__name__)
JIRA = os.environ["JIRA_URL"]
AUTH = (os.environ["JIRA_USER"], os.environ["JIRA_TOKEN"])

@app.route("/alert", methods=["POST"])
def on_alert():
    payload = request.json
    for alert in payload.get("alerts", []):
        # 从标签里读预算消耗比例
        budget = float(alert["labels"].get("budget_consumed_pct", "0"))
        oncall = alert["labels"].get("oncall_user", "sre-team")
        if budget >= 80:
            resp = requests.post(
                f"{JIRA}/rest/api/2/issue", auth=AUTH,
                json={"fields": {
                    "project": {"key": "SRE"},
                    "summary": f"[SLO] 预算消耗 {budget}%:{alert['labels']['service']}",
                    # Jira 支持 @提及用 [~accountid] 语法
                    "description": f"错误预算告急,请冻结发布并排障。@ {oncall}\n详情: {alert['generatorURL']}",
                    "issuetype": {"name": "Incident"},
                }},
            )
            print("created", resp.json().get("key"))
    return "ok", 200

if __name__ == "__main__":
    app.run(port=8080)

关键点:预算阈值的"80%“不是拍脑袋,而是经验线——既给值班留出处置时间,又不至于太晚。超过 80% 通常触发"冻结变更(freeze change)“策略。

2.3 Istio 中定义自定义 SLI 并接入 Google SRE Workbook 公式

Istio 默认提供 istio_requests_total,但业务希望用"核心下单成功率"而非"所有 HTTP 请求成功率"作为 SLI。可以用 EnvoyFilter / WASM 或直接在 Prometheus 里用时序运算定义自定义 SLI,再套用 Google SRE Workbook 的错误预算公式:

SRE Workbook 核心公式:

$$ \text{错误预算消耗率 (1day)} = \frac{1 - \text{实际好事件数}/1d}{\text{1 - SLO 目标}} $$

当某窗口(如 1 小时)的燃烧率(burn rate)超过阈值(如 14.4 对应"剩余预算 30 天将被 1 小时烧光”),就报警。

Istio 侧用 EnvoyFilter 给关键 path 打自定义标签:

# istio-sli.yaml —— 给下单接口打 biz_sli 标签
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: tag-order-sli
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      app: payment-gateway
  configPatches:
    - applyTo: HTTP_FILTER
      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.lua
          typed_config:
            "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
            inlineCode: |
              function envoy_on_request(handle)
                -- 给 /api/order/* 打上 biz_sli=order 标签,供 Prometheus 区分
                if string.find(handle:headers():get(":path"), "/api/order/") then
                  handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua",
                    "biz_sli", "order")
                end
              end              

随后在 Prometheus 中用自定义 SLI 计算预算燃烧率(SRE Workbook 多窗口法):

# 1 小时窗口燃烧率(目标 99.9%)
(
  (1 - (sum(rate(http_requests_total{biz_sli="order",code!~"5.."}[1h]))
        / sum(rate(http_requests_total{biz_sli="order"}[1h]))))
  / (1 - 0.999)
) > 14.4

三、日志:Loki 的高性能与分流

直觉:日志系统像"快递分拣中心”

Loki 的设计哲学是"只索引标签(labels),不索引全文",像快递中心只按"省市区"分拣、不打开包裹看内容——所以便宜、能存海量。但带来两个工程问题:(1) 怎么查得快(索引与对象存储的配合);(2) 海量不同等级日志怎么不互相拖累(分流与背压)。

3.1 boltdb-shipper + S3 实现 30 秒以下查询

Loki 的索引原本存在本地 BoltDB,查询范围一大就慢。Loki 2.x 引入 boltdb-shipper:把本地 BoltDB 文件定期上传到 S3,查询时直接从 S3 拉取索引分片,无需各自维护独立索引库,既省资源又能做近实时查询。

# loki.yaml —— 使用 boltdb-shipper + S3 存储
schema_config:
  configs:
    - from: 2022-01-01
      store: boltdb-shipper
      object_store: s3
      schema: v12
      index:
        prefix: index_
        period: 24h          # 每 24h 一个索引分片

storage_config:
  boltdb_shipper:
    active_index_directory: /loki/boltdb-index   # 本地活跃索引
    cache_location: /loki/boltdb-cache
    shared_store: s3                            # 上传到 S3 共享
  aws:
    s3: s3://loki-bucket/index
    region: ap-east-1

ingester:
  chunk_idle_period: 5m       # chunk 空闲 5 分钟即落盘,缩短查询延迟
  chunk_target_size: 1572864  # 1.5MB 左右,利于对象存储小文件

为什么能"30 秒以下":索引分片周期短(24h)、chunk 较小、查询并行拉取 S3 上的索引与 chunk,单租户小范围查询通常亚分钟级。查询慢多半是 label 基数过高(等价于 Prometheus 的高基数问题)。

3.2 基于 Vector + Kafka 将日志按业务等级分流到不同 Loki 租户

不同业务等级(如 critical / info / debug)对查询 SLA 与保留期要求不同。用 Vector 在采集端做路由,经 Kafka 缓冲后,把不同等级写到 Loki 的不同租户(tenant),实现资源与成本隔离。

# vector.toml —— 按 level 字段分流
[sources.k8s_logs]
type = "kubernetes_logs"

[transforms.route_by_level]
type = "route"
inputs = ["k8s_logs"]
route.critical = '.level == "critical"'     # 核心链路日志
route.normal   = '.level != "critical"'     # 普通日志

[sinks.kafka_critical]
type = "kafka"
inputs = ["route_by_level.critical"]
bootstrap_servers = "kafka:9092"
topic = "logs-critical"
encoding.codec = "json"

[sinks.kafka_normal]
type = "kafka"
inputs = ["route_by_level.normal"]
bootstrap_servers = "kafka:9092"
topic = "logs-normal"
encoding.codec = "json"

Kafka 下游再由 Vector 消费并打上 Loki 租户头(X-Scope-OrgID):

# vector-loki.toml —— 消费 Kafka 写入对应 Loki 租户
[sources.kafka_critical]
type = "kafka"
bootstrap_servers = "kafka:9092"
topics = ["logs-critical"]

[sinks.loki_critical]
type = "loki"
inputs = ["kafka_critical"]
endpoint = "http://loki:3100"
# 关键:用租户头隔离,critical 走高配租户
headers."X-Scope-OrgID" = "tenant-critical"
labels = { level = "level", app = "app" }
flowchart LR
    P[Pod 日志] --> V[Vector 采集]
    V -->|critical| KC[(Kafka\nlogs-critical)]
    V -->|normal| KN[(Kafka\nlogs-normal)]
    KC --> LC[Loki 租户 tenant-critical]
    KN --> LN[Loki 租户 tenant-normal]
    LC --> G[Grafana]
    LN --> G

3.3 日志写入出现 backpressure,动态扩容 Ingester 并保证无重复

Loki 写入链路是 Distributor → Ingester → 对象存储。当 Ingester 处理不过来,上游会反压(backpressure),严重时丢日志。应对:

  1. HPA 基于队列深度动态扩容 Ingester
  2. 用 Kafka 的 offset + Loki 的"至少一次"语义配合幂等,保证"不重复"——Loki 本身对同 chunk 去重,Vector 端用 idempotency_key 保证重发可去重。

Ingester 的 HPA(按待处理请求数扩容):

# ingester-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: loki-ingester
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: StatefulSet
    name: loki-ingester
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Pods
      pods:
        metric:
          name: loki_ingester_memory_chunks          # chunk 数逼近上限即扩容
        target:
          type: AverageValue
          averageValue: "1500000"                     # 单副本 150 万 chunk 为阈值
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30                  # 快速扩容
    scaleDown:
      stabilizationWindowSeconds: 300                 # 谨慎缩容,避免抖动

“保证无重复"的工程要点:Loki Ingester 以 (tenant, fingerprint, chunk_start) 作为 chunk 主键,同一份数据重复推送会被覆盖而非新增;配合 Kafka 消费组的 at-least-once + 去重,实现"不丢、不重”。


四、持续 Profiling(性能剖析)

直觉:Profiling 是"给程序做 CT 扫描"

Metrics 告诉你"系统慢了",Traces 告诉你"哪个请求慢了",而 Profiling 告诉你"CPU/内存花在哪一行代码"。持续 Profiling 就是让程序在生产环境一直被采样,随时能拉出火焰图,不用等问题复现才临时挂载 pprof。

4.1 Parca 对 eBPF 采样频率做自适应调整,避免 CPU 扰动

Parca 支持基于 eBPF 的连续采样。固定高频率采样(如 100Hz)本身会吃 CPU,扰动业务。做法是自适应频率:系统闲时高频、忙时降频,始终让采样开销控制在预算内(如 < 1% CPU)。

# parca-agent-config.yaml —— 自适应采样
sampling:
  # 目标:采样对业务 CPU 占用不超过 1%
  max_cpu_overhead_percent: 1.0
  # 初始频率,运行时由 agent 根据 CPU 余量动态调整
  initial_frequency: 50
  # eBPF 方式采集栈,无需改业务代码、无需重新编译
  method: ebpf

原理类比:像空调按室温自动调压缩机功率。采样频率不是越大越好——超过一定频率后,额外样本对"定位热点"边际收益递减,反而拖累宿主。

4.2 基于 Pyroscope + Go pprof 定位内存泄漏,生成火焰图 CI 脚本

Pyroscope 与 Go 的 net/http/pprof 天然契合。在 CI 里跑压测,把 heap profile 推到 Pyroscope,比对"压测前/后"的差异火焰图,内存泄漏一眼可见。

业务侧启用 pprof(标准库,无需第三方):

// main.go —— 暴露 pprof,供 Pyroscope 采集
package main

import (
    _ "net/http/pprof" // 匿名导入即注册 /debug/pprof 路由
    "net/http"
)

func main() {
    go func() {
        // 注意:生产应绑定内网地址并加鉴权
        _ = http.ListenAndServe("0.0.0.0:6060", nil)
    }()
    // ... 你的业务
}

CI 脚本:压测时持续采集 heap 并上传 Pyroscope:

#!/usr/bin/env bash
# ci-profile.sh —— 在 CI 中捕获内存火焰图
set -euo pipefail

APP="payment-service"
PYRO="http://pyroscope:4040"

# 1) 启动压测(示例用 hey)
hey -z 5m -c 50 http://$APP:8080/api/order &

# 2) 每 30s 抓一次 heap profile 并推给 Pyroscope
for i in $(seq 1 10); do
  curl -s "http://$APP:6060/debug/pprof/heap" -o /tmp/heap.pb.gz
  # pyroscope 的 CLI 把 profile 打上 tag 上传,便于按"压测轮次"对比
  pyroscope upload \
    --server-address=$PYRO \
    --application=$APP \
    --tag "ci_run=${CI_PIPELINE_ID}" \
    --tag "phase=loadtest" \
    /tmp/heap.pb.gz
  sleep 30
done

echo "已生成内存火焰图,请在 Pyroscope 对比 phase=loadtest 的 inuse_space 趋势"
flowchart LR
    A[CI 触发] --> B[起压测 hey]
    B --> C[App /debug/pprof/heap]
    C --> D[pyroscope upload]
    D --> E[(Pyroscope 存储)]
    E --> F[火焰图对比\ninuse_space 趋势]
    F --> G{内存是否持续上涨?}
    G -->|是| H[定位泄漏代码行]
    G -->|否| I[通过]

4.3 生产不允许 Privileged 容器时,改用 system-wide perf 收集

Parca/Pyroscope 的 eBPF 模式通常需要 CAP_BPF/CAP_SYS_ADMIN 特权,而很多生产集群禁止 privileged 容器(安全合规)。退路是用宿主机的 system-wide perf,由特权在节点侧一次性采集,再把 profile 文件喂给剖析平台,业务容器无需特权。

在节点上用 perf 采集(节点级,不进容器):

# 在宿主机对目标进程采样(无需容器特权)
# -p 指定进程号,-F 99 表示 99Hz,-g 抓调用栈,持续 60s
sudo perf record -F 99 -p $(pgrep -f payment-service) -g -- sleep 60

# 转换成剖析平台可识别的格式
sudo perf script > /tmp/perf.out
sudo chown ci:ci /tmp/perf.out

# 转成 pprof 格式后上传(可用 perf_data_converter 工具链)
pprof --proto /tmp/perf.out > /tmp/cpu.pb.gz
pyroscope upload --application=payment-service --tag=source=perf /tmp/cpu.pb.gz

权衡:eBPF 方案零侵入、持续;perf 方案需要节点权限但与业务容器解耦,适合"容器不可特权、但节点可运维"的合规场景。面试常考"特权被禁怎么办"——这就是标准答案。


五、多云统一监控与 GTM

直觉:多云监控像"把两个国家的人名翻译成同一套身份证号"

阿里云叫 instanceId,AWS 叫 InstanceId;阿里云地域是 cn-hangzhou,AWS 是 ap-southeast-1。直接把两边指标灌进同一个 Grafana,标签对不上、同名不同义,图表就乱套了。这一章解决标签冲突归一化跨云告警去重/补发

5.1 Grafana Cloud 对阿里云与 AWS 指标做标签冲突归一化

用 Prometheus 的 remote_write + metric_relabel_configs 在写入前就把两家云的标签重命名 / 统一取值,做到"同一语义、同一标签名"。

# prometheus-aliyun.yml —— 阿里云侧,统一标签
remote_write:
  - url: https://prometheus-prod.grafana.net/api/prom/push
    basic_auth:
      username: <grafana-id>
      password: <grafana-token>
    write_relabel_configs:
      - source_labels: [instanceId]
        target_label:  instance_id     # 阿里云的 instanceId -> 统一 instance_id
      - source_labels: [region]
        target_label:  cloud_region
        regex: "cn-(.*)"
        replacement: "aliyun-\\1"        # cn-hangzhou -> aliyun-hangzhou
      - target_label:  cloud_provider
        replacement:   "aliyun"          # 打上来源云标记
# prometheus-aws.yml —— AWS 侧,统一标签
remote_write:
  - url: https://prometheus-prod.grafana.net/api/prom/push
    basic_auth:
      username: <grafana-id>
      password: <grafana-token>
    write_relabel_configs:
      - source_labels: [InstanceId]
        target_label:  instance_id     # AWS 的 InstanceId -> 统一 instance_id
      - source_labels: [region]
        target_label:  cloud_region
        regex: "ap-(.*)"
        replacement: "aws-\\1"          # ap-southeast-1 -> aws-southeast-1
      - target_label:  cloud_provider
        replacement:   "aws"
flowchart LR
    A[(阿里云 Prometheus)] -->|relabel: instanceId->instance_id| G[Grafana Cloud]
    B[(AWS Prometheus)] -->|relabel: InstanceId->instance_id| G
    G --> Q[统一查询\ninstance_id, cloud_region]
    Q --> D[Grafana 跨云大盘]

归一化后,一份查询就能跨云对比:sum by (cloud_provider) (up{cloud_region=~"aliyun-hangzhou|aws-southeast-1"})

5.2 Prometheus Remote Write + Alertmanager 实现跨云去重

多云各自有 Alertmanager,同一个根因(比如"骨干网抖动")会在阿里云、AWS 同时报警,值班收到一堆重复告警。用全局 Alertmanager 集群 + 去重,让同一 fingerprint(标签集合哈希)只报一次。

# alertmanager-global.yml —— 跨云汇聚的去重配置
route:
  receiver: 'global-slack'
  group_by: ['alertname', 'service', 'severity']   # 同组只发一条
  group_wait: 30s
  repeat_interval: 4h
  routes:
    - matchers:
        - severity = "critical"
      receiver: 'pager'
receivers:
  - name: 'global-slack'
    slack_configs:
      - api_url: <slack>
        send_resolved: true
  - name: 'pager'
    pagerduty_configs:
      - routing_key: <pd-key>

各云把告警 remote_write 到同一个全局 Alertmanager,由它按 group_by 去重。本地 Alertmanager 只负责"本云内的快反",全局负责"去重后的汇总"。

5.3 跨云专线中断时本地缓存告警并在恢复后补发

专线断了,告警发不到全局 Alertmanager,等于"失明"。兜底设计:本地 Alertmanager 持久化告警队列,专线恢复后自动重发。

# alertmanager-local.yml —— 本地侧:断线缓存、恢复补发
global:
  resolve_timeout: 5m

route:
  receiver: 'global-am'
  # 断线时不丢弃,等恢复后继续投递
  repeat_interval: 1h

receivers:
  - name: 'global-am'
    # 指向全局 Alertmanager;网络中断时本地队列堆积,恢复后自动重发
    alertmanager_configs:
      - static_configs:
          - targets: ['global-am.global.svc:9093']
        # 关键:开启发送队列的本地持久化(磁盘),避免进程重启丢告警
        timeout: 10s
        send_resolved: true

工程细节:Alertmanager 的 notification log 默认在内存,重启即丢。生产上应挂载 PersistentVolume/alertmanager,让"已发/未发"状态落盘,专线抖动期间也不漏告警。


自测题与动手练习

概念理解题

  1. Prometheus 完整模式与 Agent 模式在内存占用上的本质差异是什么?为什么高基数(>50 万 series)场景必须用 Agent?
  2. Thanos Receive 的 HashRing 实现的是"软隔离"还是"硬隔离"?它靠哪个 label 决定数据落点?
  3. 错误预算(Error Budget)是怎么从 SLO 目标算出来的?为什么"预算消耗 80%“常作为冻结发布的阈值?
  4. Loki 的 boltdb-shipper 解决了传统 BoltDB 模式的什么痛点?为什么它能把查询压到 30 秒以内?
  5. 生产环境禁用 privileged 容器时,持续 Profiling 还有哪条路子?它与 eBPF 方案各有什么取舍?

动手练习

  1. 在本地用 Docker 起一个 Prometheus Agent(开 agent.enabled: true),把数据 remote_write 到一个 Thanos Receive 容器,观察 Agent 进程内存是否明显低于完整模式。
  2. 用 Pyrra 写一个"登录接口 99.5% 成功率"的 SLO,部署后用 kubectl port-forward 打开它的燃尽图面板,手动制造一些 5xx 看预算曲线如何下跌。
  3. 用 Vector 起一个本地 demo:读 /var/log/*.log,按 level=critical 分流到 stdout 的红色标签、其余到普通输出,理解路由配置。
  4. 在任意一个 Go 程序里引入 net/http/pprof,用 hey 压测并用 Pyroscope 上传 heap profile,对比压测前后的 inuse_space 火焰图。
  5. 用一份 metric_relabel_configs 把"阿里云 instanceId“和"AWS InstanceId“统一成 instance_id,写一条 PromQL 同时统计两朵云的 up 数量。

本章小结

  • 降本与隔离:高基数下用 Prometheus Agent(只采只转、不落盘)砍掉 TSDB 内存;用 Thanos Receive HashRing 按租户把数据分散到不同实例做软隔离;用 WAL 按租户限速 防止 noisy neighbor 拖垮整体。
  • SLO 真正管的是预算:用 Pyrra 把 SLO 编译成多窗口燃烧率规则与燃尽图;预算消耗 80% 时自动化建 Jira、@值班、冻结变更;在 Istio 用自定义标签定义 SLI,套用 Google SRE Workbook 公式量化燃烧率。
  • 日志 Lokiboltdb-shipper + S3 让索引可共享、查询亚分钟级;Vector + Kafka 按等级分流到不同 Loki 租户;背压时用 HPA + chunk 去重 动态扩容且保证不重复。
  • 持续 Profiling:Parca 用 eBPF 自适应频率 把采样开销压在预算内;Pyroscope + Go pprof 在 CI 里跑压测抓内存火焰图定位泄漏;容器不可特权时退回 节点级 perf 采集。
  • 多云统一:通过 metric_relabel_configs标签冲突归一化(统一 instance_id/cloud_region);全局 Alertmanager 做跨云去重;专线中断时靠本地持久化队列缓存并在恢复后补发。

可观测性的终极目标不是"堆工具”,而是让正确的信号在正确的时间以正确的成本到达正确的人——降本、隔离、去噪、归一,正是这条主线的四个支点。

复习提示:
  • Prometheus Agent + Thanos Receive:Agent 只采不存(省内存),Thanos 按租户 HashRing 软隔离;两者配合解决高基数场景的内存雪崩。
  • SLO 的核心是预算:Pyrra 把 SLO 编译成燃尽图,80% 消耗时自动建 Jira + @值班 + 冻结变更,让团队 proactive 而非 reactive。
  • Loki 零索引设计:用 label 粗索引 + 内容细扫描,查询亚分钟级;Vector + Kafka 做等级分流,避免告警风暴。
  • 持续 Profiling 不侵入代码:Parca 用 eBPF 采样,Pyroscope 配合 Go pprof 在 CI 里跑压测抓火焰图,定位内存泄漏。
  • 下一章讲服务网格 Istio——它是流量治理、mTLS 安全、熔断限流的统一入口,和可观测性天然互补。
About Me

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

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

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

目标

学AI,加油!加油!