Module 15: 可观测性
📖 深度参考手册 — 本模块属于理论参考,非主线必读。 主线学习路径见 README.md。 当你在项目实战中遇到相关问题时,回来查阅。
“你无法改进你无法测量的东西。” 可观测性让你在系统出问题时能快速定位原因,而不是盲人摸象。从”服务挂了”到”知道为什么挂了”之间的差距,就是可观测性的价值。
15.1 可观测性三大支柱
Section titled “15.1 可观测性三大支柱”定义:可观测性(Observability)的三大支柱是:日志(Logs)——离散事件的文本记录,回答”发生了什么”;指标(Metrics)——聚合的数值数据(计数、比率、分位数),回答”多少、多快”;链路追踪(Traces)——单个请求穿过多个服务的完整路径和每一步耗时,回答”在哪里慢了、在哪里断了”。三者互补缺一不可:日志提供细节但缺乏全局视角;指标提供全局趋势但缺乏细节;链路追踪将分散的日志和指标串联成完整的请求故事。
为什么重要:在分布式系统中,一个用户请求可能经过 5-10 个服务,任何一个环节出问题都会导致用户看到”服务异常”。没有可观测性,排查问题就像大海捞针——你知道出了问题,但不知道是哪个服务、哪一步、什么原因。有了可观测性,你可以在几分钟内定位问题根因,而不是花几个小时甚至几天。可观测性也是团队信心的基础——对系统内部越”可见”,上线新功能就越有底气。
案例:Hotel Reservation — 预订失败时,三大支柱如何协作定位问题。
场景:用户报告"预订失败",页面显示"服务异常,请稍后重试"
═══ 只有日志的世界 ═══
运维看到以下日志(每个服务各自的日志文件):
API Gateway: [ERROR] 500 Internal Server Error - POST /api/reservationsOrder Service: [ERROR] Failed to create reservation for user 789Inventory: [ERROR] Lock timeout for hotel 456, room_type standardPayment: [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个),链路追踪就变得不可或缺了。
这也是大多数创业公司的演进路径:先加指标和日志(快速搭建),再加链路追踪(系统复杂度上升后)。
15.2 结构化日志
Section titled “15.2 结构化日志”定义:结构化日志(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 5002024-01-15 14:23:15 ERROR POST /api/reservations returned 5002024-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 4562024-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),你会如何在”完整性”和”成本”之间取舍?
点击查看解析
分级策略:
- ERROR/FATAL 日志:100% 保留,保存 90 天。这些是排查问题的关键,绝不能丢。
- WARN 日志:100% 保留,保存 30 天。可能是问题的前兆。
- INFO 日志:采样保留(比如 10%),保存 14 天。大部分是正常操作记录,不需要全部保留。
- DEBUG 日志:生产环境默认关闭,只在排查问题时临时开启特定服务的 DEBUG。
冷热分层:
- 热存储(Elasticsearch/Loki):最近 7 天的日志,支持快速查询
- 温存储(S3 + Athena):7-30 天的日志,查询慢但成本低
- 冷存储(S3 Glacier):30-90 天的日志,很少查询,极低成本
关键技巧:即使 INFO 日志被采样,也要确保同一个 correlation_id 的所有日志要么全保留、要么全丢弃——不然你会看到一个请求只有一半的日志,更加困惑。这叫”头部采样”(Head-based Sampling)。
15.3 指标与监控
Section titled “15.3 指标与监控”定义:指标(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"} 1523456video_play_requests_total{status="500"} 234// 错误率 = rate(status="500") / rate(total) = 0.015%
// Gauge(可增可减)— 用于当前值active_video_streams 85432transcoding_queue_length 1234cdn_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个请求: 100ms1个请求: 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 差距很大,说明系统存在不稳定的长尾延迟,需要排查。
15.4 链路追踪
Section titled “15.4 链路追踪”定义:链路追踪(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 → 420msSpan 的数据结构:
{ "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 | 一次完整请求的生命周期 | 一个快递从下单到签收 |
| Span | Trace中的一个操作步骤 | 快递在每个中转站的停留 |
| SpanContext | 跨服务传递的追踪信息 | 快递单号(贯穿全程) |
| Trace ID | 全局唯一请求标识 | 快递单号 |
| Parent Span | 当前Span的上级 | 上一个中转站 |
先想一想 🤔 如果 Hotel Reservation 的流量很大(每秒 10000 个请求),记录每个请求的完整链路追踪的成本是否太高?如何解决?
点击查看解析
是的,全量采集的成本很高。解决方案是采样(Sampling):
固定比率采样:只记录 10% 的请求的链路追踪。简单但可能漏掉罕见的错误请求。
头部采样(Head-based Sampling):在请求进入系统时就决定是否采集。优点是简单;缺点是在不知道结果的情况下做决定——可能丢弃了一个最终会失败的请求。
尾部采样(Tail-based Sampling):在请求完成后再决定是否保留——错误的请求 100% 保留,延迟超过阈值的 100% 保留,正常请求只保留 1%。优点是不漏掉重要请求;缺点是需要短暂缓存所有 Span 直到请求完成,架构更复杂。
自适应采样:根据当前流量动态调整采样率——流量低时 100% 采集,流量高时降到 1%。
推荐做法:尾部采样 + 自适应采样。确保所有错误请求和慢请求都被完整记录,正常请求按比例采样。这样既控制了成本(存储和网络),又不会漏掉需要排查的关键信息。
15.5 告警设计
Section titled “15.5 告警设计”定义:告警(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 是怎么定出来的?定太低或太高分别有什么问题?
点击查看解析
阈值的确定方法:
基于历史数据:观察过去 30 天的 P99 延迟分布。如果正常值在 50-200ms 之间,偶尔飙到 300ms,那 500ms 是一个合理的告警阈值——足够高以过滤正常波动,又足够低以在用户明显感知之前告警。
基于用户体验:研究表明超过 1 秒的延迟会明显影响用户体验。500ms 给了你 500ms 的缓冲时间来修复问题。
阈值太低(如 200ms):
- 正常波动就会频繁触发 → 告警疲劳
- 团队开始忽略这个告警
- 真正出问题时也被忽略
阈值太高(如 5000ms):
- 用户已经在体验糟糕的延迟了才告警
- 丧失了”提前发现问题”的价值
- 可能已经有大量用户流失了
最佳实践:设两级阈值——预警(P99 > 300ms,发到 Slack 频道,不叫人)和告警(P99 > 500ms 持续 5 分钟,叫值班人员)。预警让你关注趋势,告警要求立即行动。
15.6 Dashboard 设计
Section titled “15.6 Dashboard 设计”定义: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 上关注哪些指标来确认部署是否正常?
点击查看解析
部署后的”健康检查”清单(按优先级):
错误率(最重要):部署后 5 分钟内,5xx 错误率是否上升?与部署前 1 小时的基线对比。如果错误率翻倍 → 考虑立即回滚。
P99 延迟:API 响应时间是否变慢?新代码可能引入性能退化。在 Dashboard 时间线上标注部署时间点,前后对比。
业务指标:预订成功率是否下降?转化漏斗是否正常?有些 bug 不会导致 5xx 错误,但会导致业务逻辑错误(比如价格计算错误→用户看到异常价格→不预订了)。
资源使用:CPU/内存是否异常上升?新代码可能有内存泄漏。
新错误:Sentry 中是否出现了之前没有的新错误类型?
最佳实践:在 Dashboard 的时间线图上,用垂直线标注每次部署的时间和版本号。这样任何指标的异常变化都可以快速与部署关联起来。很多团队还会设置”部署看板”——部署后自动打开,15 分钟内如果关键指标异常就自动回滚。
15.7 错误追踪
Section titled “15.7 错误追踪”定义:错误追踪(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 checkSentry 的核心功能:
1. 自动聚合 相同堆栈跟踪的错误 → 归为一组 显示: 总次数、受影响用户数、趋势(增/减/平)
2. 首次错误高亮 新出现的从未见过的错误 → 🔴 NEW 标签 + 立即通知 → 通常与最近的代码变更相关
3. Release 关联 每个错误关联到引入它的代码版本 → 快速定位是哪次部署引入的问题
4. 前端+后端统一 前端JS错误、后端异常都在同一个平台查看 → 不需要猜"是前端的问题还是后端的问题"
5. 告警集成 新错误 → Slack通知 错误数量突增 → PagerDuty叫人 可按严重程度配置不同的通知策略先想一想 🤔 如果 News Feed 的 Sentry 中积累了 500 个未解决的错误,你应该如何确定优先级?
点击查看解析
按 受影响用户数 × 严重程度 排序:
- 第一维度:受影响用户数
- 影响 10000 人的错误 > 影响 10 人的错误
- Sentry 可以按”受影响用户数”排序
- 第二维度:严重程度
- 导致页面完全白屏(Fatal)> 某个功能不可用(Error)> 功能降级(Warning)
- 付费用户受影响 > 免费用户受影响
- 第三维度:趋势
- 持续增长的错误(可能会扩大影响)优先于稳定的错误
- 新出现的错误优先于长期存在的错误(新错误通常与最近变更相关,更容易修复)
- 不要试图清零
- 500 个错误中,可能 80% 是低影响的、长期存在的、修复成本高的
- 专注于 TOP 20(影响最大的 20 个),解决后再看下一批
- 对于确认”不修”的错误,标记为”忽略”以减少噪音
实践中的常见节奏:每周花 1 小时做”Sentry 清理”——处理 TOP 5 新错误,忽略已确认不重要的错误。
15.8 健康检查与探针
Section titled “15.8 健康检查与探针”定义:健康检查(Health Check)是一个简单的端点(通常是 GET /health 或 GET /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: v1kind: Podspec: 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 的可用性:
- Inventory Service 宕机 → Order Service 的 Liveness 失败
- K8s 重启 Order Service → 重启后 Inventory Service 仍然不可用
- Liveness 再次失败 → 再次重启 → 无限循环
- 更糟:所有 Order Service 实例同时重启 → 级联故障
正确做法:
- Liveness Probe:只检查 Order Service 自身是否还活着(进程没死、没卡死)。返回 200 就行。
- Readiness Probe:检查 Order Service 是否能正常处理请求——包括数据库和关键依赖。如果 Inventory Service 不可用,Readiness 返回失败 → K8s 不发送流量 → 但不重启容器 → Inventory Service 恢复后,Readiness 恢复 → 流量自动回来。
核心原则:Liveness 回答”该不该重启”,Readiness 回答”该不该接流量”。依赖不可用不是重启自己能解决的问题。
15.9 SLI / SLO / SLA
Section titled “15.9 SLI / SLO / SLA”定义: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延迟 |
| SLO | SLI的目标值 | 工程团队 | P99延迟 < 500ms |
| SLA | 对外的法律承诺 | 客户 | 99.9%可用性,违反赔10% |
| Error Budget | SLO允许的错误量 | 产品+工程 | 每月43分钟可不可用 |
先想一想 🤔 Hotel Reservation 的 SLO 应该比 Gaming Leaderboard 更严格还是更宽松?为什么?
点击查看解析
Hotel Reservation 的 SLO 应该更严格,原因:
涉及金钱:Hotel Reservation 涉及真实的金钱交易——预订失败可能意味着用户错过住宿、行程受影响。Gaming Leaderboard 的分数延迟几秒更新,影响远小于此。
不可逆性:预订是有时效性的——“今晚的房间”过了今晚就没有意义了。排行榜的数据可以延迟更新后追回。
竞争环境:酒店预订市场竞争激烈(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 设计完整的可观测性方案,回答以下问题:
- 日志:每个服务需要记录哪些关键日志?日志格式是什么?如何使用 correlation_id?
- 指标:用四大黄金信号定义需要监控的指标(具体的指标名称和含义)。
- 告警:设计 P0-P3 的告警规则(包括阈值、持续时间和操作手册概要)。
- Dashboard:画出开发团队 Dashboard 的布局草图。
练习 2:SLI/SLO 设计
Section titled “练习 2:SLI/SLO 设计”从 11 个系统案例中选 3 个,为每个系统:
- 定义 2-3 个 SLI(说明如何测量)
- 为每个 SLI 设定 SLO(给出具体数字,并解释为什么选这个值)
- 计算 Error Budget(每月允许多少错误/不可用时间)
- 定义 Error Budget 策略(剩余多少时该做什么)