云原生可观测性: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 选主/复制.-> FReceive 的 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 --> G3.3 日志写入出现 backpressure,动态扩容 Ingester 并保证无重复
Loki 写入链路是 Distributor → Ingester → 对象存储。当 Ingester 处理不过来,上游会反压(backpressure),严重时丢日志。应对:
- HPA 基于队列深度动态扩容 Ingester;
- 用 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,让"已发/未发"状态落盘,专线抖动期间也不漏告警。
自测题与动手练习
概念理解题
- Prometheus 完整模式与 Agent 模式在内存占用上的本质差异是什么?为什么高基数(>50 万 series)场景必须用 Agent?
- Thanos Receive 的 HashRing 实现的是"软隔离"还是"硬隔离"?它靠哪个 label 决定数据落点?
- 错误预算(Error Budget)是怎么从 SLO 目标算出来的?为什么"预算消耗 80%“常作为冻结发布的阈值?
- Loki 的
boltdb-shipper解决了传统 BoltDB 模式的什么痛点?为什么它能把查询压到 30 秒以内? - 生产环境禁用 privileged 容器时,持续 Profiling 还有哪条路子?它与 eBPF 方案各有什么取舍?
动手练习
- 在本地用 Docker 起一个 Prometheus Agent(开
agent.enabled: true),把数据remote_write到一个 Thanos Receive 容器,观察 Agent 进程内存是否明显低于完整模式。 - 用 Pyrra 写一个"登录接口 99.5% 成功率"的 SLO,部署后用
kubectl port-forward打开它的燃尽图面板,手动制造一些 5xx 看预算曲线如何下跌。 - 用 Vector 起一个本地 demo:读
/var/log/*.log,按level=critical分流到stdout的红色标签、其余到普通输出,理解路由配置。 - 在任意一个 Go 程序里引入
net/http/pprof,用hey压测并用 Pyroscope 上传 heap profile,对比压测前后的inuse_space火焰图。 - 用一份
metric_relabel_configs把"阿里云instanceId“和"AWSInstanceId“统一成instance_id,写一条 PromQL 同时统计两朵云的up数量。
本章小结
- 降本与隔离:高基数下用 Prometheus Agent(只采只转、不落盘)砍掉 TSDB 内存;用 Thanos Receive HashRing 按租户把数据分散到不同实例做软隔离;用 WAL 按租户限速 防止 noisy neighbor 拖垮整体。
- SLO 真正管的是预算:用 Pyrra 把 SLO 编译成多窗口燃烧率规则与燃尽图;预算消耗 80% 时自动化建 Jira、@值班、冻结变更;在 Istio 用自定义标签定义 SLI,套用 Google SRE Workbook 公式量化燃烧率。
- 日志 Loki:
boltdb-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 安全、熔断限流的统一入口,和可观测性天然互补。