跳转到内容

Module 15: 可观测性

📖 深度参考手册 — 本模块属于理论参考,非主线必读。 主线学习路径见 README.md。 当你在项目实战中遇到相关问题时,回来查阅。

“你无法改进你无法测量的东西。” 可观测性让你在系统出问题时能快速定位原因,而不是盲人摸象。从”服务挂了”到”知道为什么挂了”之间的差距,就是可观测性的价值。


定义:可观测性(Observability)的三大支柱是:日志(Logs)——离散事件的文本记录,回答”发生了什么”;指标(Metrics)——聚合的数值数据(计数、比率、分位数),回答”多少、多快”;链路追踪(Traces)——单个请求穿过多个服务的完整路径和每一步耗时,回答”在哪里慢了、在哪里断了”。三者互补缺一不可:日志提供细节但缺乏全局视角;指标提供全局趋势但缺乏细节;链路追踪将分散的日志和指标串联成完整的请求故事。

为什么重要:在分布式系统中,一个用户请求可能经过 5-10 个服务,任何一个环节出问题都会导致用户看到”服务异常”。没有可观测性,排查问题就像大海捞针——你知道出了问题,但不知道是哪个服务、哪一步、什么原因。有了可观测性,你可以在几分钟内定位问题根因,而不是花几个小时甚至几天。可观测性也是团队信心的基础——对系统内部越”可见”,上线新功能就越有底气。

案例Hotel Reservation — 预订失败时,三大支柱如何协作定位问题

场景:用户报告"预订失败",页面显示"服务异常,请稍后重试"
═══ 只有日志的世界 ═══
运维看到以下日志(每个服务各自的日志文件):
API Gateway: [ERROR] 500 Internal Server Error - POST /api/reservations
Order Service: [ERROR] Failed to create reservation for user 789
Inventory: [ERROR] Lock timeout for hotel 456, room_type standard
Payment: [INFO] No payment request received ← 没走到支付这一步
问题:能看到出错了,但哪些日志属于同一个请求?
如果每秒有1000个请求,怎么找到这个用户的那一条?
═══ 加上指标的世界 ═══
Grafana Dashboard 显示:
- 预订失败率: 突然从 0.1% 飙升到 15% (过去10分钟)
- 库存服务延迟: P99 从 200ms 飙升到 5000ms
- 库存服务 CPU: 正常 (30%)
- 数据库连接池: 使用率从 40% 飙升到 95%
发现:问题出在库存服务的数据库连接池!但为什么连接池满了?
═══ 加上链路追踪的世界 ═══
Jaeger 中搜索这个用户的 trace:
Trace ID: abc-123-def
├── API Gateway (2ms)
│ └── Order Service (5003ms) ← 总耗时5秒!
│ └── Inventory Service - CheckAvailability (15ms) ✅
│ └── Inventory Service - LockRoom (4980ms) ❌ TIMEOUT
│ └── PostgreSQL Query (4950ms) ❌ 等待连接
│ └── Payment Service ← 未到达
└── 响应: 500 Internal Server Error
根因定位:库存服务的 LockRoom 操作因为数据库连接池耗尽而超时
→ 进一步查日志: 发现有一个慢查询占用了大量连接
→ 修复: 优化慢查询 + 扩大连接池
支柱回答的问题工具数据特点
日志发生了什么?具体错误是什么?ELK, Loki高基数,文本,细节丰富
指标多少?多快?趋势如何?Prometheus + Grafana低基数,数值,适合聚合
链路追踪请求在哪里慢了?卡在哪个服务?Jaeger, Zipkin中等基数,请求路径

先想一想 🤔 如果 Hotel Reservation 只能选两个可观测性支柱(预算有限),你会选哪两个?为什么?

点击查看解析

指标 + 日志

  • 指标是最先需要的——它告诉你”有没有问题”和”问题有多严重”。没有指标,你甚至不知道系统出了问题(用户投诉才知道就太晚了)。指标也是告警的基础。
  • 日志是排查问题的最终依据——指标告诉你”预订失败率上升了”,日志告诉你”具体是什么错误”。
  • 链路追踪可以最后加——在服务数量不多(<5个)时,通过日志中的 correlation_id 手动追踪也能凑合。但当服务数量增多(>10个),链路追踪就变得不可或缺了。

这也是大多数创业公司的演进路径:先加指标和日志(快速搭建),再加链路追踪(系统复杂度上升后)。


定义:结构化日志(Structured Logging)是将日志从人类可读的自由文本格式改为机器可解析的键值对格式(通常是 JSON)。传统日志:"2024-01-15 Error: booking failed for user 123"——人能读懂,但机器很难精确提取”user_id=123”。结构化日志:{"time":"2024-01-15","level":"error","msg":"booking failed","user_id":123,"hotel_id":456,"error":"insufficient inventory"}——机器可以按任何字段索引、搜索、过滤和聚合。关联 ID(Correlation ID / Request ID)是结构化日志的核心实践:每个请求生成一个唯一 ID,贯穿该请求经过的所有服务的日志,方便追踪一个请求的完整链路。

为什么重要:在生产环境中,系统每秒可能产生数千条日志。如果是非结构化的文本日志,你只能用字符串搜索(grep),效率极低且容易遗漏。结构化日志可以被日志系统(ELK Stack / Grafana Loki)索引,让你像查数据库一样查日志——“找出过去 1 小时内 hotel_id=456 且 level=error 的所有日志”,瞬间返回结果。关联 ID 更是分布式系统排查问题的生命线——没有它,你面对 10 个服务的日志根本无法知道哪些日志属于同一个请求。

案例所有系统都需要结构化日志。以 Hotel Reservation 为例——“用户报 bug 说预订失败”的排查过程

═══ 非结构化日志的噩梦 ═══
API Gateway:
2024-01-15 14:23:15 ERROR POST /api/reservations returned 500
2024-01-15 14:23:15 ERROR POST /api/reservations returned 500
2024-01-15 14:23:16 ERROR POST /api/reservations returned 500
... (每秒几百条,哪条是这个用户的?)
Order Service:
2024-01-15 14:23:15 ERROR booking failed for user 789 hotel 456
2024-01-15 14:23:15 ERROR booking failed for user 234 hotel 789
... (多个用户同时失败,混在一起)
排查方式: grep "user 789" order-service.log → 找到一条
但 Inventory Service 的日志里没有 user_id → 无法关联
只能靠时间戳大致匹配 → 不准确,而且费时
═══ 结构化日志 + 关联ID ═══
所有服务使用统一格式,每个请求携带同一个 correlation_id:
API Gateway:
{"time":"2024-01-15T14:23:15Z","level":"error","service":"api-gateway",
"correlation_id":"req-abc-123","method":"POST","path":"/api/reservations",
"status":500,"duration_ms":5003,"user_id":789}
Order Service:
{"time":"2024-01-15T14:23:15Z","level":"error","service":"order-service",
"correlation_id":"req-abc-123","msg":"reservation creation failed",
"user_id":789,"hotel_id":456,"error":"inventory lock timeout"}
Inventory Service:
{"time":"2024-01-15T14:23:15Z","level":"error","service":"inventory",
"correlation_id":"req-abc-123","msg":"lock timeout",
"hotel_id":456,"room_type":"standard","wait_ms":4980,
"error":"connection pool exhausted"}
排查方式:
在 Kibana/Grafana 中搜索: correlation_id = "req-abc-123"
→ 瞬间返回3条日志(分属3个服务)
→ 完整链路一目了然:API → Order → Inventory(连接池耗尽) → 超时
实现关联ID的方式:
请求到达API Gateway
生成 correlation_id = uuid()
放入HTTP Header: X-Correlation-ID: req-abc-123
传递给下游所有服务
每个服务的每条日志都包含这个 correlation_id
日志系统按 correlation_id 索引
// Go中间件示例
func CorrelationMiddleware(c *gin.Context) {
corrID := c.GetHeader("X-Correlation-ID")
if corrID == "" {
corrID = uuid.New().String()
}
c.Set("correlation_id", corrID)
c.Header("X-Correlation-ID", corrID)
c.Next()
}
// 每次写日志时
logger.Info("processing reservation",
"correlation_id", c.GetString("correlation_id"),
"user_id", userId,
"hotel_id", hotelId,
)
日志实践做法效果
结构化格式JSON 格式输出机器可索引、可查询
关联ID每个请求唯一ID贯穿所有服务跨服务追踪一个请求
日志级别DEBUG/INFO/WARN/ERROR/FATAL按级别过滤和告警
上下文字段user_id, hotel_id, order_id等按业务维度查询
采样高流量时只记录10%的DEBUG日志降低存储成本

先想一想 🤔 如果 Hotel Reservation 的日志量太大(每天 100GB),你会如何在”完整性”和”成本”之间取舍?

点击查看解析

分级策略

  1. ERROR/FATAL 日志:100% 保留,保存 90 天。这些是排查问题的关键,绝不能丢。
  2. WARN 日志:100% 保留,保存 30 天。可能是问题的前兆。
  3. INFO 日志:采样保留(比如 10%),保存 14 天。大部分是正常操作记录,不需要全部保留。
  4. DEBUG 日志:生产环境默认关闭,只在排查问题时临时开启特定服务的 DEBUG。

冷热分层

  • 热存储(Elasticsearch/Loki):最近 7 天的日志,支持快速查询
  • 温存储(S3 + Athena):7-30 天的日志,查询慢但成本低
  • 冷存储(S3 Glacier):30-90 天的日志,很少查询,极低成本

关键技巧:即使 INFO 日志被采样,也要确保同一个 correlation_id 的所有日志要么全保留、要么全丢弃——不然你会看到一个请求只有一半的日志,更加困惑。这叫”头部采样”(Head-based Sampling)。


定义:指标(Metrics)是对系统行为的数值化测量,以时间序列的形式存储(每个数据点 = 时间戳 + 数值 + 标签)。Google SRE 团队总结的四大黄金信号是最重要的指标类别:延迟(Latency)——处理请求的耗时;流量(Traffic)——系统承受的请求量;错误率(Error Rate)——失败请求的比例;饱和度(Saturation)——资源的使用程度。指标有三种基本类型:Counter(只增不减的计数器,如请求总数)、Gauge(可增可减的当前值,如当前连接数)、Histogram(分布统计,如响应时间的 P50/P95/P99)。

为什么重要:指标是系统健康状态的”仪表盘”。没有指标,你对系统的状态一无所知——直到用户投诉你才知道出了问题。有了指标,你可以:主动发现问题(错误率上升→告警→立即处理);量化影响(“过去 1 小时有 5% 的请求延迟超过 2 秒”比”系统有点慢”精确 100 倍);指导优化(知道哪里是瓶颈才能优化对的地方);评估容量(CPU 用了 80%→需要扩容)。Prometheus + Grafana 是当前最流行的开源指标采集和可视化组合。

案例YouTube — 视频服务的四大黄金信号

YouTube 的四大黄金信号:
═══ 1. 延迟 (Latency) ═══
需要监控的延迟指标:
- 视频播放首帧时间(Time to First Frame): P50=0.5s, P95=1.5s, P99=3s
- 搜索响应时间: P50=100ms, P95=300ms, P99=800ms
- 视频上传处理时间: P50=2min, P95=10min, P99=30min
- 缩略图加载时间: P50=50ms, P95=200ms
注意:需要区分"成功请求的延迟"和"失败请求的延迟"
→ 失败请求如果很快返回(如400 Bad Request),
会拉低平均延迟,掩盖真实的性能问题
→ 所以应该分开统计
═══ 2. 流量 (Traffic) ═══
- 每秒视频播放请求数 (Requests Per Second)
- 每秒视频上传量
- CDN 带宽使用量 (Gbps)
- 实时并发观看人数
这些指标的价值:
→ 识别流量模式(工作日晚间高峰、周末全天高峰)
→ 容量规划(下个月需要多少CDN带宽?)
→ 异常检测(流量突然下降50%→可能是DNS故障)
═══ 3. 错误率 (Error Rate) ═══
- 视频播放失败率(所有播放请求中返回错误的比例)
- 视频上传失败率
- 转码失败率
- API 5xx 错误率
按错误类型细分:
- 404: 视频不存在(可能是视频被删除)
- 429: 限流(可能需要调整限流阈值)
- 500: 服务端错误(需要立即排查)
- 503: 服务过载(需要扩容)
═══ 4. 饱和度 (Saturation) ═══
- CPU 使用率(转码服务器通常很高)
- 内存使用率
- 磁盘 I/O(视频存储服务器)
- 网络带宽使用率
- 数据库连接池使用率
- 消息队列积压长度(待转码的视频队列)
关键:饱和度预警比告警更有价值
→ "连接池使用率到达80%"(预警)比"连接池耗尽"(告警)更有用
→ 预警给你时间扩容,告警时已经在影响用户了
Prometheus 指标类型示例(以YouTube为例):
// Counter(只增不减)— 用于计算速率
video_play_requests_total{status="200"} 1523456
video_play_requests_total{status="500"} 234
// 错误率 = rate(status="500") / rate(total) = 0.015%
// Gauge(可增可减)— 用于当前值
active_video_streams 85432
transcoding_queue_length 1234
cdn_bandwidth_gbps 450.5
// Histogram(分布)— 用于分位数
video_start_time_seconds_bucket{le="0.5"} 650000 // 65%的视频在0.5秒内开始播放
video_start_time_seconds_bucket{le="1.0"} 850000 // 85%在1秒内
video_start_time_seconds_bucket{le="2.0"} 950000 // 95%在2秒内
video_start_time_seconds_bucket{le="5.0"} 990000 // 99%在5秒内
// P50 ≈ 0.4s, P95 ≈ 2.0s, P99 ≈ 5.0s
黄金信号YouTube 对应指标为什么重要
延迟首帧时间 P50/P95/P99超过3秒用户就会离开
流量每秒播放请求数容量规划、异常检测
错误率播放失败率、转码失败率直接影响用户体验
饱和度CPU/内存/带宽/队列长度提前预警,避免过载

先想一想 🤔 为什么监控延迟时应该用 P99 而不是平均值(Average)?在什么情况下平均值会严重误导你?

点击查看解析

平均值会掩盖”长尾”问题。例如:

100个请求的响应时间:
99个请求: 100ms
1个请求: 10000ms (10秒)
平均值: (99×100 + 1×10000) / 100 = 199ms ← 看起来还行
P99: 10000ms ← 揭示了1%的用户体验极差

在 YouTube 的场景中,如果 1% 的用户视频加载需要 10 秒,而 YouTube 每天有 10 亿次播放——这意味着每天有 1000 万次播放体验极差。但平均值只有 199ms,看起来一切正常。

P99 告诉你”最差的 1% 用户的体验”。对于大规模系统,1% 就是百万级用户。

实际中通常同时监控 P50(中位数,代表”典型用户体验”)、P95(大多数用户的上限)、P99(长尾用户的体验)。如果 P50 和 P99 差距很大,说明系统存在不稳定的长尾延迟,需要排查。


定义:链路追踪(Distributed Tracing)是记录一个请求从进入系统到返回响应的完整路径——它经过了哪些服务、每个服务花了多长时间、是否有错误。核心概念:Trace(一次完整的请求,由一个唯一的 Trace ID 标识)、Span(Trace 中的一个操作步骤,如”调用库存服务”)、SpanContext(跨服务传递的上下文信息,包含 Trace ID 和 Span ID)。每个 Span 记录操作名称、开始时间、持续时间、状态和标签。标准工具栈:OpenTelemetry(采集标准)+ Jaeger 或 Zipkin(存储和可视化)。

为什么重要:在微服务架构中,一个用户请求可能经过 API 网关 → 用户服务 → 订单服务 → 库存服务 → 支付服务 → 通知服务。当请求失败或延迟高时,你需要知道是哪个服务出了问题。没有链路追踪,你只能靠日志的时间戳”猜测”问题出在哪里。有了链路追踪,你可以直观地看到请求在每个服务上花了多长时间,哪一步失败了——就像一张”请求的 X 光片”。

案例Hotel Reservation — 预订请求的完整链路追踪

用户点击"预订"按钮 → 请求经过以下链路:
Trace ID: trace-789-xyz
总耗时: 1250ms
API Gateway [12ms]
└── Auth Middleware [5ms] ✅ JWT验证通过
└── Order Service [1230ms]
├── Validate Request [3ms] ✅ 参数校验
├── User Service - GetUser [45ms] ✅ 获取用户信息
│ └── Redis Cache [2ms] ✅ 缓存命中
├── Inventory Service [850ms] ⚠️ 耗时最长!
│ ├── CheckAvailability [15ms] ✅ 查询可用房间
│ │ └── PostgreSQL Query [12ms]
│ └── LockRoom [830ms] ⚠️ 锁定房间耗时异常
│ └── PostgreSQL Query [825ms] ❌ 慢查询!
│ tag: query="SELECT ... FOR UPDATE"
│ tag: rows_examined=150000
├── Pricing Service [50ms] ✅ 计算价格
│ └── Redis Cache [1ms] ✅ 价格规则缓存命中
├── Payment Service [280ms] ✅ 扣款
│ └── Stripe API [250ms] ✅ 第三方支付
└── Notification Service [35ms] ✅ 发送确认邮件
└── SendGrid API [30ms]
分析:
1. 总耗时 1250ms,其中库存服务占 850ms (68%)
2. 库存服务中,LockRoom 操作的数据库查询扫描了15万行 → 缺少索引
3. 其他所有服务都在正常范围内
优化: 为 rooms 表的 (hotel_id, room_type, date) 添加复合索引
预期效果: LockRoom 从 830ms → 10ms,总耗时从 1250ms → 420ms
Span 的数据结构:
{
"trace_id": "trace-789-xyz",
"span_id": "span-inventory-lock",
"parent_span_id": "span-inventory",
"operation_name": "inventory.LockRoom",
"service_name": "inventory-service",
"start_time": "2024-01-15T14:23:15.100Z",
"duration_ms": 830,
"status": "OK",
"tags": {
"hotel_id": 456,
"room_type": "standard",
"date": "2024-02-14",
"db.type": "postgresql",
"db.statement": "SELECT ... FOR UPDATE",
"db.rows_examined": 150000
},
"logs": [
{"time": "...", "msg": "acquiring row lock"},
{"time": "...", "msg": "lock acquired after 825ms"}
]
}
SpanContext 如何跨服务传递:
Order Service Inventory Service
│ │
│ HTTP请求: │
│ POST /api/inventory/lock │
│ Headers: │
│ traceparent: 00-trace789xyz-spanABC-01
│ tracestate: vendor=value │
│────────────────────────────────────────→│
│ │
│ Inventory Service 收到请求: │
│ 1. 从 Header 中提取 trace_id │
│ 2. 创建新的子 Span │
│ (parent = spanABC) │
│ 3. 所有日志和指标都关联到这个 trace │
│ 4. 调用下游服务时继续传递 traceparent │
标准: W3C Trace Context(traceparent header)
工具: OpenTelemetry SDK 自动处理传递逻辑
概念说明类比
Trace一次完整请求的生命周期一个快递从下单到签收
SpanTrace中的一个操作步骤快递在每个中转站的停留
SpanContext跨服务传递的追踪信息快递单号(贯穿全程)
Trace ID全局唯一请求标识快递单号
Parent Span当前Span的上级上一个中转站

先想一想 🤔 如果 Hotel Reservation 的流量很大(每秒 10000 个请求),记录每个请求的完整链路追踪的成本是否太高?如何解决?

点击查看解析

是的,全量采集的成本很高。解决方案是采样(Sampling)

  1. 固定比率采样:只记录 10% 的请求的链路追踪。简单但可能漏掉罕见的错误请求。

  2. 头部采样(Head-based Sampling):在请求进入系统时就决定是否采集。优点是简单;缺点是在不知道结果的情况下做决定——可能丢弃了一个最终会失败的请求。

  3. 尾部采样(Tail-based Sampling):在请求完成后再决定是否保留——错误的请求 100% 保留,延迟超过阈值的 100% 保留,正常请求只保留 1%。优点是不漏掉重要请求;缺点是需要短暂缓存所有 Span 直到请求完成,架构更复杂。

  4. 自适应采样:根据当前流量动态调整采样率——流量低时 100% 采集,流量高时降到 1%。

推荐做法:尾部采样 + 自适应采样。确保所有错误请求和慢请求都被完整记录,正常请求按比例采样。这样既控制了成本(存储和网络),又不会漏掉需要排查的关键信息。


定义:告警(Alerting)是当系统指标超过预设阈值时,自动通知相关人员采取行动的机制。好的告警有三个特征:可操作(收到告警就知道该做什么)、不频繁(避免告警疲劳导致所有人忽略告警)、分级(P0 紧急→立即处理,P1 高→1 小时内,P2 中→当天,P3 低→下周)。告警的核心原则是按症状告警而非按原因告警——“用户可见的错误率超过 1%“比”某台服务器 CPU 超过 90%“更有意义,因为前者直接影响用户,后者可能完全不影响。

为什么重要:告警是运维团队的”眼睛和耳朵”——没有告警,你只能等用户投诉才知道出了问题。但过多的告警比没有告警更危险——这叫”告警疲劳”(Alert Fatigue)。当团队每天收到 100 条告警,他们会学会忽略所有告警——然后当真正的 P0 事故发生时,也被忽略了。设计好的告警体系是运维能力的核心体现。

案例Gaming Leaderboard — 设计有意义的告警

❌ 反模式:按原因告警(告警泛滥)
Alert: Redis CPU > 80%
→ 每天触发5次(Redis本身在高负载下CPU高是正常的)
→ 团队开始忽略这个告警
→ 某天Redis真的因为CPU过高导致响应变慢 → 但告警已经被忽略了
Alert: 每个5xx错误都发告警
→ 每天几百条(偶发的5xx在大规模系统中是正常的)
→ 团队完全不看告警了
Alert: 磁盘使用 > 70%
→ 持续告警几周(磁盘是慢慢涨的)
→ 变成背景噪音 → 真正快满时没人注意
✅ 正确方式:按症状告警(用户感知)
P0 (立即处理,叫醒值班人员):
"排行榜更新延迟 > 30秒,持续5分钟"
→ 玩家看到的排名是过时的,直接影响游戏体验
→ 操作手册: 检查Redis主从延迟 → 检查消费者积压 → 考虑重启消费者
P1 (1小时内处理):
"分数提交失败率 > 5%,持续10分钟"
→ 部分玩家的分数没有被记录
→ 操作手册: 检查API错误日志 → 检查数据库连接 → 检查消息队列
P2 (当天处理):
"排行榜查询P99延迟 > 500ms,持续30分钟"
→ 排行榜加载变慢但还能用
→ 操作手册: 检查Redis内存使用 → 检查热点Key → 考虑扩容
P3 (下周处理):
"Redis内存使用率 > 70%"
→ 还没影响用户,但趋势在上升
→ 操作手册: 分析Key分布 → 清理过期数据 → 规划扩容
告警设计原则:
1. 按症状告警,不按原因告警
❌ "Redis CPU > 80%"(原因,可能不影响用户)
✅ "排行榜更新延迟 > 30秒"(症状,用户直接受影响)
2. 设置合理的阈值和持续时间
❌ "错误率 > 0%"(任何一个错误就告警 → 噪音)
✅ "错误率 > 1%,持续5分钟"(过滤掉偶发错误)
3. 每条告警都有操作手册(Runbook)
告警内容: "排行榜更新延迟 > 30秒"
Runbook链接: https://wiki/runbooks/leaderboard-delay
步骤1: 检查 Kafka 消费者 lag → 命令: ...
步骤2: 检查 Redis 主从延迟 → 命令: ...
步骤3: 如果以上正常,检查 ... → 命令: ...
4. 定期审查告警
→ 过去30天哪些告警被触发但不需要行动?→ 调整阈值或删除
→ 过去30天哪些事故没有触发告警?→ 添加新的告警规则
告警特征好的告警坏的告警
可操作收到就知道该做什么收到不知道该做什么
频率每周几次每天几十次
针对性按用户影响分级所有告警同级
文档附带Runbook链接只有一行描述
阈值考虑正常波动静态阈值,频繁误报

先想一想 🤔 “排行榜查询 P99 延迟 > 500ms”这个告警的阈值 500ms 是怎么定出来的?定太低或太高分别有什么问题?

点击查看解析

阈值的确定方法:

  1. 基于历史数据:观察过去 30 天的 P99 延迟分布。如果正常值在 50-200ms 之间,偶尔飙到 300ms,那 500ms 是一个合理的告警阈值——足够高以过滤正常波动,又足够低以在用户明显感知之前告警。

  2. 基于用户体验:研究表明超过 1 秒的延迟会明显影响用户体验。500ms 给了你 500ms 的缓冲时间来修复问题。

阈值太低(如 200ms)

  • 正常波动就会频繁触发 → 告警疲劳
  • 团队开始忽略这个告警
  • 真正出问题时也被忽略

阈值太高(如 5000ms)

  • 用户已经在体验糟糕的延迟了才告警
  • 丧失了”提前发现问题”的价值
  • 可能已经有大量用户流失了

最佳实践:设两级阈值——预警(P99 > 300ms,发到 Slack 频道,不叫人)和告警(P99 > 500ms 持续 5 分钟,叫值班人员)。预警让你关注趋势,告警要求立即行动。


定义:Dashboard(仪表盘)是将系统指标以图表形式可视化展示的页面。好的 Dashboard 面向特定角色、回答特定问题:运维 Dashboard(服务是否健康?资源够不够?)、开发 Dashboard(代码上线后错误率有没有上升?API 变快还是变慢?)、业务 Dashboard(今天有多少订单?转化率是多少?)。Dashboard 的核心设计原则:最重要的指标在最上面(一眼看到全局);用颜色区分状态(绿色正常/黄色预警/红色异常);时间范围可调(从最近 1 小时到最近 30 天)。

为什么重要:Dashboard 是团队共享的”系统心跳监视器”。没有 Dashboard,每个人对系统状态的理解是碎片化的——开发只看日志,运维只看服务器,产品只看业务数据,彼此之间的信息不互通。好的 Dashboard 让所有人对系统状态有一致的、实时的理解。在事故处理中,Dashboard 更是核心工具——它帮助团队快速判断问题范围、受影响的用户数、以及修复是否生效。

案例Hotel Reservation — 面向不同角色的 Dashboard 设计

═══ 业务 Dashboard(产品经理/CEO 看)═══
┌─────────────────────────────────────────────────┐
│ 📊 Hotel Reservation - 业务概览 │
│ 时间范围: [今天 ▼] 自动刷新: 每5分钟 │
├─────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 今日订单 │ │ 入住率 │ │ 取消率 │ │
│ │ 1,234 │ │ 78.5% │ │ 4.2% │ │
│ │ ↑12% │ │ ↑2.1% │ │ ↓0.3% │ │
│ │ (vs昨日) │ │ (vs昨日) │ │ (vs昨日) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ [过去7天订单量趋势图 📈] │
│ [热门城市TOP10 柱状图] │
│ [预订→付款 转化漏斗] │
│ 搜索 10,000 → 查看详情 5,000 → │
│ 开始预订 2,000 → 付款成功 1,234 │
│ │
└─────────────────────────────────────────────────┘
═══ 开发 Dashboard(开发团队看)═══
┌─────────────────────────────────────────────────┐
│ 🔧 Hotel Reservation - 开发视图 │
│ 时间范围: [最近1小时 ▼] 自动刷新: 每30秒 │
├─────────────────────────────────────────────────┤
│ │
│ API 健康状态: │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 请求量 │ │ 错误率 │ │ P99延迟 │ │
│ │ 523/sec │ │ 🟢0.1% │ │ 🟢245ms │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ [各API端点响应时间 — 时间序列图] │
│ POST /reservations P99: 450ms 🟡 │
│ GET /hotels/search P99: 120ms 🟢 │
│ GET /reservations P99: 80ms 🟢 │
│ │
│ 最近部署: v2.3.1 (今天 14:30) │
│ 部署后错误率: 🟢 无变化 │
│ │
│ [Sentry 最新错误 TOP5] │
│ 1. NullPointerException in PaymentService (12次) │
│ 2. TimeoutException in InventoryService (5次) │
│ │
└─────────────────────────────────────────────────┘
═══ 运维 Dashboard(SRE/运维看)═══
┌─────────────────────────────────────────────────┐
│ 🖥️ Hotel Reservation - 基础设施 │
│ 时间范围: [最近6小时 ▼] 自动刷新: 每15秒 │
├─────────────────────────────────────────────────┤
│ │
│ 服务状态: │
│ API Gateway 🟢 3/3 healthy │
│ Order Service 🟢 5/5 healthy │
│ Inventory Svc 🟡 4/5 healthy (1 restarting) │
│ Payment Svc 🟢 3/3 healthy │
│ │
│ 资源使用: │
│ ┌──────────────────────────────────────┐ │
│ │ CPU [████████░░] 78% 🟡 │ │
│ │ Memory [██████░░░░] 62% 🟢 │ │
│ │ Disk [████░░░░░░] 43% 🟢 │ │
│ │ DB连接池[████████░░] 85% 🟡 │ │
│ └──────────────────────────────────────┘ │
│ │
│ [CPU/Memory 时间序列图 — 按服务分组] │
│ [数据库慢查询 TOP5] │
│ [K8s Pod 重启次数] │
│ │
└─────────────────────────────────────────────────┘
Dashboard 设计检查清单:
1. ✅ 面向特定角色(不要一个Dashboard塞所有东西)
2. ✅ 最重要的指标在最上面(数字卡片形式)
3. ✅ 与基线对比(vs昨天/vs上周/vs上次部署)
4. ✅ 颜色编码(🟢正常 🟡预警 🔴异常)
5. ✅ 时间范围可调
6. ✅ 自动刷新(业务5分钟、开发30秒、运维15秒)
7. ✅ 可点击深入(从概览钻到明细)
8. ✅ 标注关键事件(部署、配置变更)在时间线上

先想一想 🤔 Hotel Reservation 刚刚做了一次部署(v2.3.1),你应该在 Dashboard 上关注哪些指标来确认部署是否正常?

点击查看解析

部署后的”健康检查”清单(按优先级):

  1. 错误率(最重要):部署后 5 分钟内,5xx 错误率是否上升?与部署前 1 小时的基线对比。如果错误率翻倍 → 考虑立即回滚。

  2. P99 延迟:API 响应时间是否变慢?新代码可能引入性能退化。在 Dashboard 时间线上标注部署时间点,前后对比。

  3. 业务指标:预订成功率是否下降?转化漏斗是否正常?有些 bug 不会导致 5xx 错误,但会导致业务逻辑错误(比如价格计算错误→用户看到异常价格→不预订了)。

  4. 资源使用:CPU/内存是否异常上升?新代码可能有内存泄漏。

  5. 新错误:Sentry 中是否出现了之前没有的新错误类型?

最佳实践:在 Dashboard 的时间线图上,用垂直线标注每次部署的时间和版本号。这样任何指标的异常变化都可以快速与部署关联起来。很多团队还会设置”部署看板”——部署后自动打开,15 分钟内如果关键指标异常就自动回滚。


定义:错误追踪(Error Tracking)是使用专门的工具(如 Sentry、Bugsnag)自动捕获、聚合和分析应用程序中的异常和错误。与直接看日志不同,错误追踪工具会:自动捕获未处理的异常(前端 JS 报错、后端未捕获异常)、聚合相同的错误(同一个 bug 导致的 1000 次错误只显示为 1 条,附带发生次数和受影响用户数)、记录丰富的上下文(用户信息、浏览器版本、请求参数、堆栈跟踪)、在首次出现新错误时高亮通知。

为什么重要:直接看日志来发现错误有两个致命问题:第一,相同的错误重复出现会”淹没”日志,你看不到真正重要的新错误;第二,日志缺少上下文,你看到 NullPointerException 但不知道是哪个用户、什么请求参数触发的。错误追踪工具解决了这两个问题——聚合让你关注”有多少种不同的错误”而不是”有多少条错误日志”,上下文让你能复现和修复错误。对于任何面向用户的系统,错误追踪工具都是必备的。

案例所有面向用户的系统都需要错误追踪。以 News Feed 为例

场景:News Feed 系统上线新版本后,用户反馈"时间线加载不出来"
═══ 没有 Sentry,靠日志排查 ═══
1. 查看后端日志:
ERROR TypeError: Cannot read property 'author' of null
ERROR TypeError: Cannot read property 'author' of null
ERROR TypeError: Cannot read property 'author' of null
... (重复几百条,全是同一个错误)
→ 日志被淹没,其他可能更重要的错误看不到
→ 不知道哪些用户受影响
→ 不知道是什么请求触发的
2. 前端报错?完全不知道
→ 用户浏览器里的JS错误,后端日志里看不到
→ 只能等用户报bug → 用户描述模糊 → 无法复现
═══ 有 Sentry,高效排查 ═══
Sentry Dashboard:
┌──────────────────────────────────────────────────────┐
│ 🔴 NEW TypeError: Cannot read property 'author' │
│ of null │
│ │
│ 首次出现: 今天 14:35 (v2.3.1 部署后5分钟) │
│ 发生次数: 1,523 次 │
│ 受影响用户: 834 人 │
│ 趋势: 📈 持续增长 │
│ │
│ 堆栈跟踪: │
│ at renderPost (Feed.js:45) │
│ at FeedList.map (Feed.js:23) │
│ at Timeline (Timeline.js:15) │
│ │
│ 上下文: │
│ Browser: Chrome 120, macOS │
│ URL: /feed?page=3 │
│ User: user_789 (Premium) │
│ Request: GET /api/feed?page=3&size=20 │
│ │
│ 额外数据: │
│ post_id: 12345 (这条帖子的author字段为null) │
│ post_created: 2024-01-14 (昨天创建) │
│ │
│ 关联Commit: abc123 "refactor: change post schema" │
│ → 原来是重构时改了post表结构,旧数据没迁移 │
└──────────────────────────────────────────────────────┘
立刻知道:
1. 影响范围: 834个用户(其中有Premium用户 → 高优先级)
2. 根因: post_id 12345 的 author 字段为 null(旧数据)
3. 引入时间: v2.3.1 部署后(关联到具体commit)
4. 修复方案: 补一个数据迁移脚本,同时加 null check
Sentry 的核心功能:
1. 自动聚合
相同堆栈跟踪的错误 → 归为一组
显示: 总次数、受影响用户数、趋势(增/减/平)
2. 首次错误高亮
新出现的从未见过的错误 → 🔴 NEW 标签 + 立即通知
→ 通常与最近的代码变更相关
3. Release 关联
每个错误关联到引入它的代码版本
→ 快速定位是哪次部署引入的问题
4. 前端+后端统一
前端JS错误、后端异常都在同一个平台查看
→ 不需要猜"是前端的问题还是后端的问题"
5. 告警集成
新错误 → Slack通知
错误数量突增 → PagerDuty叫人
可按严重程度配置不同的通知策略

先想一想 🤔 如果 News Feed 的 Sentry 中积累了 500 个未解决的错误,你应该如何确定优先级?

点击查看解析

受影响用户数 × 严重程度 排序:

  1. 第一维度:受影响用户数
    • 影响 10000 人的错误 > 影响 10 人的错误
    • Sentry 可以按”受影响用户数”排序
  2. 第二维度:严重程度
    • 导致页面完全白屏(Fatal)> 某个功能不可用(Error)> 功能降级(Warning)
    • 付费用户受影响 > 免费用户受影响
  3. 第三维度:趋势
    • 持续增长的错误(可能会扩大影响)优先于稳定的错误
    • 新出现的错误优先于长期存在的错误(新错误通常与最近变更相关,更容易修复)
  4. 不要试图清零
    • 500 个错误中,可能 80% 是低影响的、长期存在的、修复成本高的
    • 专注于 TOP 20(影响最大的 20 个),解决后再看下一批
    • 对于确认”不修”的错误,标记为”忽略”以减少噪音

实践中的常见节奏:每周花 1 小时做”Sentry 清理”——处理 TOP 5 新错误,忽略已确认不重要的错误。


定义:健康检查(Health Check)是一个简单的端点(通常是 GET /healthGET /healthz),用于报告服务的当前状态。在 Kubernetes 等容器编排平台中,健康检查通过三种**探针(Probe)**实现:Liveness Probe(存活探针)——进程是否还活着?失败→自动重启容器;Readiness Probe(就绪探针)——服务能否处理请求?失败→从负载均衡器中移除(不再接收流量);Startup Probe(启动探针)——服务是否启动完成?用于启动慢的服务,在启动期间不触发存活和就绪检查。

为什么重要:在容器化部署中,服务实例可能随时崩溃、重启、扩缩容。健康检查是自动化管理服务生命周期的基础——没有健康检查,K8s 不知道一个容器是否正常工作,可能把流量发送到一个已经死掉的容器上。健康检查也是零宕机部署(Zero-Downtime Deployment)的关键——新版本的容器必须通过就绪检查后才接收流量,确保用户在部署过程中不会看到错误。

案例所有容器化部署的系统都需要健康检查。以 Hotel Reservation 为例

Hotel Reservation 各服务的健康检查设计:
═══ API Gateway ═══
GET /health
{
"status": "healthy",
"version": "v2.3.1",
"uptime": "72h15m",
"checks": {
"self": "ok"
}
}
// API Gateway是无状态的,只要进程在跑就是健康的
// Liveness: GET /health → 200 ✅
// Readiness: GET /health → 200 ✅
═══ Order Service ═══
GET /health
{
"status": "healthy",
"checks": {
"database": "ok", // 能否连接数据库?
"redis": "ok", // 能否连接缓存?
"inventory_service": "ok" // 下游服务是否可达?
}
}
// Liveness: GET /healthz → 只检查进程存活(不检查依赖)
// → 避免因为数据库临时不可用就杀掉进程
// Readiness: GET /health → 检查所有依赖
// → 如果数据库不可用,从负载均衡摘除,不接收新请求
═══ Inventory Service(启动慢)═══
启动时需要加载房间库存数据到内存(需30秒)
Startup Probe:
GET /health/startup
initialDelaySeconds: 10 // 等10秒后开始检查
periodSeconds: 5 // 每5秒检查一次
failureThreshold: 12 // 最多允许失败12次(60秒)
// 启动期间不触发Liveness和Readiness
Liveness Probe:
GET /healthz
periodSeconds: 10
failureThreshold: 3 // 连续3次失败 → 重启
Readiness Probe:
GET /health
periodSeconds: 5
failureThreshold: 2 // 连续2次失败 → 摘除流量
三种探针的区别和协作:
服务启动中 服务运行中
├────────────┤ ├────────────────────────┤
Startup Probe (不再检查)
├── ❌❌❌✅ ──┤ 启动完成!
Liveness Probe
├── ✅ ✅ ✅ ❌ ❌ ❌ → 重启!
Readiness Probe
├── ✅ ✅ ❌ ❌ → 摘除流量
├────── ✅ ✅ → 恢复流量
关键区别:
- Liveness失败 → 重启容器(适用于进程卡死/死锁)
- Readiness失败 → 停止发送流量(适用于依赖不可用/资源耗尽)
- Startup失败 → 继续等待(适用于启动慢的服务)
常见错误:
❌ Liveness检查数据库连接
→ 数据库临时不可用 → 所有容器被重启
→ 重启后仍然连不上数据库 → 再次重启
→ 雪崩效应!
✅ 正确: Liveness只检查进程存活,Readiness检查依赖
Kubernetes YAML 配置示例:
apiVersion: v1
kind: Pod
spec:
containers:
- name: order-service
livenessProbe:
httpGet:
path: /healthz # 只检查进程存活
port: 8080
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health # 检查所有依赖
port: 8080
periodSeconds: 5
failureThreshold: 2
startupProbe:
httpGet:
path: /health/startup
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12

先想一想 🤔 Hotel Reservation 的 Order Service 依赖 Inventory Service。如果 Inventory Service 不可用,Order Service 的 Liveness Probe 应该返回失败吗?

点击查看解析

绝对不应该

Liveness Probe 失败 = K8s 重启容器。如果 Order Service 的 Liveness 检查 Inventory Service 的可用性:

  1. Inventory Service 宕机 → Order Service 的 Liveness 失败
  2. K8s 重启 Order Service → 重启后 Inventory Service 仍然不可用
  3. Liveness 再次失败 → 再次重启 → 无限循环
  4. 更糟:所有 Order Service 实例同时重启 → 级联故障

正确做法

  • Liveness Probe:只检查 Order Service 自身是否还活着(进程没死、没卡死)。返回 200 就行。
  • Readiness Probe:检查 Order Service 是否能正常处理请求——包括数据库和关键依赖。如果 Inventory Service 不可用,Readiness 返回失败 → K8s 不发送流量 → 但不重启容器 → Inventory Service 恢复后,Readiness 恢复 → 流量自动回来。

核心原则:Liveness 回答”该不该重启”,Readiness 回答”该不该接流量”。依赖不可用不是重启自己能解决的问题。


定义SLI(Service Level Indicator)——衡量服务质量的具体指标,是一个可度量的数值,如”API 请求的 P99 延迟”或”视频播放成功率”。SLO(Service Level Objective)——SLI 的目标值,是团队内部设定的质量目标,如”P99 延迟 < 500ms”或”播放成功率 > 99.95%“。SLA(Service Level Agreement)——对外的法律承诺,通常比 SLO 宽松,如”月可用性 99.9%,不达标赔付 10%“。Error Budget(错误预算)——SLO 允许的错误量,如 99.9% 可用性意味着每月允许 43 分钟不可用。这些概念来自 Google SRE 实践,是平衡”可靠性”和”开发速度”的核心框架。

为什么重要:没有 SLO,关于”系统够不够好”的讨论永远是主观的——产品说”用户觉得慢”,开发说”P99 已经 200ms 了很好了”,运维说”可用性 99.8%“。SLO 给了所有人一个共同的、量化的标准。更重要的是 Error Budget 的概念——它将可靠性从”越高越好”变成了”够用就好”。如果 SLO 是 99.9%,你还有 0.1% 的 Error Budget 可以用来发布新功能(新功能可能引入少量错误)。当 Error Budget 用完时,停止发布新功能,集中修稳定性——这是一种数据驱动的决策方式。

案例YouTube — 视频播放的 SLI/SLO/SLA 设计

YouTube 视频播放服务的 SLI/SLO/SLA:
═══ SLI(怎么衡量?)═══
SLI 1: 可用性
= 成功的播放请求数 / 总播放请求数
测量方式: 服务端统计 HTTP 2xx 响应的比例
排除: 客户端错误(4xx)不计入
SLI 2: 延迟
= 视频首帧显示时间(从点击播放到第一帧画面出现)
测量方式: 客户端 SDK 上报
分位数: P50, P95, P99
SLI 3: 质量
= 播放过程中无缓冲的比例
测量方式: 客户端 SDK 上报
═══ SLO(目标是多少?)═══
SLO 1: 可用性 > 99.95%
→ 每月允许的不可用时间: 43.2秒 × 5 = 21.6分钟
→ 对应每天约 43 秒的不可用
SLO 2: 首帧延迟 P99 < 3秒
→ 99%的用户在3秒内看到第一帧画面
SLO 3: 无缓冲播放率 > 99%
→ 99%的播放session不出现缓冲中断
═══ SLA(对外承诺多少?)═══
SLA: 月可用性 > 99.9%(比SLO宽松)
→ 每月允许 43.2分钟不可用
→ 不达标: 赔付当月费用的10%(对于YouTube Premium用户)
→ 严重不达标(<99%): 赔付30%
═══ Error Budget(还能犯多少错?)═══
本月Error Budget计算(以可用性SLO 99.95%为例):
总请求数(预估): 100亿次
允许失败请求数: 100亿 × 0.05% = 500万次
本月已消耗:
Week 1: 部署bug导致50万次失败 → 消耗10%
Week 2: 正常运行,3万次失败 → 消耗0.6%
Week 3: CDN故障导致200万次失败 → 消耗40%
────────────────────────────
已消耗: 50.6%
剩余: 49.4%(还剩约247万次可失败)
决策:
剩余 > 30%: ✅ 正常迭代,可以发布新功能
剩余 10-30%: ⚠️ 谨慎发布,加强测试
剩余 < 10%: 🔴 冻结新功能,全力修稳定性
如何选择合适的SLO?
❌ 错误: "我们的SLO是100%可用性"
→ 不可能达到(硬件会坏、网络会抖、代码会有bug)
→ 如果你承诺100%,每次任何错误都是"违反SLO"
→ Error Budget = 0,永远不敢发布新功能
❌ 错误: "我们的SLO是90%可用性"
→ 太低了,意味着每天可以不可用2.4小时
→ 用户会流失到竞品
✅ 正确: 基于用户预期和业务需求设定
问自己:
1. 用户能容忍多少不可用?
→ 电商: 99.9%(每月43分钟不可用还可以接受)
→ 支付: 99.99%(每月4.3分钟是上限)
→ 医疗: 99.999%(每月26秒,很难但必须)
2. 竞品的水平?
→ 如果竞品是99.95%,你至少要一样
3. 成本可接受?
→ 从99.9%提到99.99%的成本可能是10倍
→ 从99.99%提到99.999%的成本可能再10倍
概念定义面向谁举例
SLI衡量服务质量的指标工程团队API P99延迟
SLOSLI的目标值工程团队P99延迟 < 500ms
SLA对外的法律承诺客户99.9%可用性,违反赔10%
Error BudgetSLO允许的错误量产品+工程每月43分钟可不可用

先想一想 🤔 Hotel Reservation 的 SLO 应该比 Gaming Leaderboard 更严格还是更宽松?为什么?

点击查看解析

Hotel Reservation 的 SLO 应该更严格,原因:

  1. 涉及金钱:Hotel Reservation 涉及真实的金钱交易——预订失败可能意味着用户错过住宿、行程受影响。Gaming Leaderboard 的分数延迟几秒更新,影响远小于此。

  2. 不可逆性:预订是有时效性的——“今晚的房间”过了今晚就没有意义了。排行榜的数据可以延迟更新后追回。

  3. 竞争环境:酒店预订市场竞争激烈(Booking.com, Airbnb 等),用户对预订失败的容忍度极低——一次失败就可能永久流失。游戏排行榜的用户粘性更多来自游戏本身。

建议 SLO:

  • Hotel Reservation:预订成功率 > 99.95%,支付接口 P99 < 1s,搜索 P99 < 500ms
  • Gaming Leaderboard:分数更新延迟 P99 < 5s,排行榜查询 P99 < 200ms,可用性 > 99.9%

注意 Gaming Leaderboard 虽然可用性 SLO 较低,但延迟 SLO 可能更严格——玩家期望排行榜实时更新,200ms 就能感知到”卡”。


练习 1:Hotel Reservation 可观测性方案

Section titled “练习 1:Hotel Reservation 可观测性方案”

为 Hotel Reservation 设计完整的可观测性方案,回答以下问题:

  1. 日志:每个服务需要记录哪些关键日志?日志格式是什么?如何使用 correlation_id?
  2. 指标:用四大黄金信号定义需要监控的指标(具体的指标名称和含义)。
  3. 告警:设计 P0-P3 的告警规则(包括阈值、持续时间和操作手册概要)。
  4. Dashboard:画出开发团队 Dashboard 的布局草图。

从 11 个系统案例中选 3 个,为每个系统:

  1. 定义 2-3 个 SLI(说明如何测量)
  2. 为每个 SLI 设定 SLO(给出具体数字,并解释为什么选这个值)
  3. 计算 Error Budget(每月允许多少错误/不可用时间)
  4. 定义 Error Budget 策略(剩余多少时该做什么)