---
tags: [microservice, observability, monitoring, tracing]
create time: 2026-04-29 12:04
---
# 可观测性
## 概述
微服务架构下,一个请求可能穿越十几个甚至上百个服务。**排查问题就像在大海捞针**。可观测性通过三大支柱(Metrics、Logging、Tracing)让系统行为变得"可见"。
> [!question] 核心思考
> 单体应用出问题,SSH 到服务器上 `tail -f` 日志就能定位。但当你有 50 个服务、每个 3 个副本,日志散落在不同容器里——你还能用同样的方式排障吗?如果不能,什么体系能替代它?
### 为什么需要独立的"可观测性"体系?
```mermaid
flowchart LR
subgraph MONO["单体系统"]
S[Service] --> DB[(DB)]
LOG["同一份日志
一目了然"]
S --> LOG
end
subgraph MICRO["微服务系统"]
REQ("Request") --> GW["Gateway"]
GW --> O1["Order Svc"]
GW --> U1["User Svc"]
O1 --> PAY["Payment Svc"]
O1 --> INV["Inventory Svc"]
PAY --> DB1[(DB)]
INV --> DB2[(DB)]
style O1 fill:#faa
style U1 fill:#faa
style PAY fill:#faa
style INV fill:#faa
end
NOTE["问题:日志分散、链路断裂
传统监控无法回答'这个请求经历了什么'"]
MICRO --> NOTE
```
可观测性与监控的本质区别:**监控系统告诉你"出事了"**(已知未知),**可观测性系统让你去探究"为什么会出事"**(未知未知)。
### SLO 与 Error Budget
可观测性的终极目标不是收集数据,而是**支撑决策**。SLO(Service Level Objective)将技术指标映射到用户体验。
> [!summary] SLI / SLO / SLA 三层模型
>
> | 层级 | 含义 | 示例 |
> |------|------|------|
> | **SLI** (Indicator) | 实际测量的指标 | P99 延迟 = 120ms |
> | **SLO** (Objective) | 内部目标值 | P99 延迟 < 200ms(99.9% 的请求) |
> | **SLA** (Agreement) | 对外的契约承诺 | 可用性 ≥ 99.95%,否则赔偿 |
> [!tip] Error Budget(错误预算)
>
> 如果 SLO 是 99.9%,那么每月允许的错误时间 = 30 × 24 × 60 × 0.1% ≈ **43 分钟**。
>
> - **预算充足时**:可以大胆发布新功能、尝试激进方案
> - **预算耗尽时**:冻结非功能性变更,专注稳定性修复
>
> 这构成了"创新"和"稳定"之间的动态平衡机制。
## 三大支柱
| 支柱 | 回答的问题 | 典型工具 |
|------|-----------|---------|
| **Metrics** | 系统现在健康吗? — 趋势、告警 | Prometheus + Grafana |
| **Logging** | 具体发生了什么? — 历史回溯 | ELK / Loki |
| **Tracing** | 请求在哪一步慢了/失败了? — 链路追踪 | Jaeger / Zipkin |
### Metrics:系统的脉搏
Metrics 回答的问题是:**系统现在健康吗?**——通过聚合后的数值揭示趋势。
> [!summary] RED 和 USE 方法
>
> | 方法 | 适用于 | 指标 |
> |------|--------|------|
> | **RED** (Rate, Errors, Duration) | 有状态服务(API) | QPS、错误率、响应时间 P99 |
> | **USE** (Utilization, Saturation, Errors) | 基础设施(CPU/内存/网络) | 使用率、饱和度、错误数 |
核心指标类型:
- **Counter(计数器)**:只增不减,如 `http_requests_total`。适合统计请求总量、错误总数
- **Gauge(仪表盘)**:可增可减,如 `queue_depth`、在线用户数
- **Histogram(直方图)**:样本分布,自动分桶统计,如请求延迟的 P50/P90/P99
- **Summary(摘要)**:类似 Histogram,但客户端计算分位数(Go SDK 默认)
```go
// Go 示例:定义并注册 Prometheus 指标
var (
// Counter: HTTP 请求总数,按 method 和 status 标签分类
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
},
[]string{"method", "status"},
)
// Histogram: API 响应延迟分布
apiLatency = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "api_latency_seconds",
Help: "API latency distribution",
Buckets: prometheus.DefBuckets, // [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
},
[]string{"endpoint"},
)
)
func init() {
prometheus.MustRegister(httpRequestsTotal, apiLatency)
}
```
> [!tip] 解读提示
> Counter 用于统计"发生了多少",Histogram 用于理解"有多慢"。一个完整的告警面板通常同时展示两者:QPS 突降 + 延迟飙升 = 大概率出事了。
在 Grafana 中的典型视图:
```mermaid
graph TB
subgraph GRAFANA["Grafana Dashboard"]
G1["QPS
[Counter]"] -->|关联分析| G2["P99 Latency
[Histogram]"]
G3["错误率
[Counter %]"] -->|关联分析| G2
G2 --> G4["告警规则
阈值触发"]
end
GRAFANA -.->|从 Prometheus 拉取| PROM[(Prometheus)]
```
### Logging:集中化日志
Logging 回答的问题是:**具体发生了什么?**——通过原始事件记录提供上下文。
- **所有服务的日志必须集中收集**,不能散落在各台机器上
- 日志格式推荐 JSON,便于解析
- 每条日志必须携带 `trace_id`,与 Tracing 串联
```json
{
"timestamp": "2026-04-29T12:00:00Z",
"level": "ERROR",
"service": "order-service",
"trace_id": "abc-123-def",
"span_id": "span-456",
"msg": "failed to connect payment service",
"caller": "payment/client.go:42",
"cost_ms": 30000
}
```
#### 结构化日志的核心字段
> [!note] 最小集合(必选)
>
> - `timestamp` — ISO 8601 格式,统一时区 UTC
> - `level` — TRACE / DEBUG / INFO / WARN / ERROR / FATAL
> - `service` — 微服务名称
> - `trace_id` — 链路追踪 ID,用于跨服务关联
> - `msg` — 人类可读的消息描述
> [!warning] 日志最佳实践
>
> - **不要记录敏感信息**:密码、Token、身份证号等绝不能出现在日志中
> - **INFO 级记录关键业务事件**:下单成功、支付回调——这些是排查业务问题的主线
> - **WARN 级记录异常但不致命**:重试了一次、降级走了备用方案
> - **ERROR 级必须有 trace_id 和错误堆栈**,否则毫无价值
#### 日志采集管线
```mermaid
flowchart LR
APP[应用容器] -->|stdout/stderr| LO[FLUENTD / Filebeat]
LO -->|TCP/Lumberjack| LO2[(Logstash / Loki)]
LO2 -->|解析 & 过滤| PIPE[Pipeline]
PIPE -->|写入| ES[(Elasticsearch)]
PIPE -->|跳转| GRAF[Grafana / Kibana]
style PIPE fill:#ff9
```
**常见方案对比**:
| 方案 | 存储 | 查询能力 | 适用场景 |
|------|------|---------|---------|
| ELK (Elasticsearch) | ES 集群 | 全文检索强大,灵活度高 | 日志量大、需要复杂分析的团队 |
| Loki + Grafana | 对象存储 (S3) | 对标 LogQL,轻量高效 | 已经在用 Grafana 的团队 |
| CloudWatch / SLS | 云厂商托管 | 开箱即用,成本较高 | 全云原生环境 |
> [!tip] 日志采样策略
>
> 生产环境中并非所有日志都要采集。**正常路径采 1%,异常路径全量**。这既能大幅降低存储成本,又能在出问题时有足够的调试数据可用。采样逻辑建议写在日志库中,而非业务代码里。
### Tracing:分布式链路追踪
Tracing 回答的问题是:**请求在哪一步慢了 / 失败了?**——通过完整的调用链路定位瓶颈和故障。
一次请求在多个服务中的完整调用路径:
```mermaid
flowchart LR
RID["Request
trace_id: abc-123"]
subgraph SPANS["调用链 (Trace)"]
direction TB
S1["Span #1
API Gateway
5ms"]
S2["Span #2
Order Service
70ms"]
S3["Span #3
Payment RPC
160ms"]
S4["Span #4
Inventory RPC
70ms"]
S1 --> S2 --> S3
S2 --> S4
end
RID --> S1
style S3 fill:#ff9999
note["Span #3 耗时最长 → Payment 是瓶颈"]
S3 -.-> note
```
> [!tip] 关键实践
> - 确保 `trace_id` 在整个调用链中传递(HTTP Header 或 MQ Message Header)
> - 采样策略:**全量采集开发环境**,**生产环境按百分比采样**(避免存储爆炸)
> - 重点关注的 Span 标签:HTTP method、status code、database query
> - **Span 不要嵌套过深**,一个 HTTP 调用或 DB 查询对应一个 Span 即可
#### Context Propagation(上下文传播)
trace_id 如何从上游服务传递到下游?
```go
// 1. 入站:从 HTTP Header 提取 trace context
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// OpenTelemetry SDK 自动从 Header 恢复 span context
ctx := propagator.Extract(r.Context(), headerReader{r.Header})
// 2. 创建新的 span
ctx, span := tracer.Start(ctx, "handleOrder")
defer span.End()
// 3. 将 trace context 注入到新请求的 Header 中
r = r.WithContext(ctx)
propagator.Inject(ctx, headerWriter{r.Header})
next.ServeHTTP(w, r)
})
}
```
```json
// HTTP Header 中实际传递的字段 (W3C Trace Context)
{
"traceparent": "00-abc123def456...-789ghi012jkl-01",
"tracestate": "congo=t61rcWkgMzE"
}
```
> [!note] W3C Trace Context 标准
>
> `traceparent` 格式:`version-trace_id-span_id-flags`,如 `00-{32位trace_id}-{16位span_id}-{2位flags}`。
> 这是 W3C 推荐的标准,被 OpenTelemetry、Jaeger、Zipkin 等主流方案统一支持。**无需再自定义 Header**。
## 告警设计原则
### 告警疲劳是常态
> [!warning] 核心问题
>
> 如果一个团队每天收到 200 条告警,其中 195 条是误报或无需处理,工程师会对剩下的 5 条真正重要的告警产生"脱敏"。这就是 **告警疲劳**。
- **告警要能 actionable**:收到告警后知道该做什么,否则不要告
- **区分级别**:P0(电话叫醒)vs P1(工单处理)vs P2(次日处理)
- **基于 SLO 告警**:不是 CPU > 80% 就告警,而是用户感知到延迟增加时才告
- **降噪**:同一个根因可能触发连锁告警,需要抑制机制
### 告警层级设计
```mermaid
flowchart TD
subgraph L1["L1: 页面上的用户受损"]
A1["HTTP 5xx 错误率 > 1%"]
A2["核心接口 P99 > 2s"]
end
subgraph L2["L2: 系统健康度下降"]
B1["单个服务错误率 > 5%"]
B2["下游依赖超时率升高"]
end
subgraph L3["L3: 资源预警"]
C1["磁盘使用 > 70%"]
C2["内存使用 > 80%"]
end
A1 -->|P0 - 电话 + IM| ONCALL[On-Call 值班]
A2 -->|P0 - 电话 + IM| ONCALL
B1 -->|P1 - IM 通知| OPS[运维群]
B2 -->|P1 - IM 通知| OPS
C1 -->|P2 - 日报汇总| TEAM[团队看板]
C2 -->|P2 - 日报汇总| TEAM
```
> [!tip] 告警规则编写清单
>
> 每条告警规则都应该能回答以下问题:
> 1. **什么问题?** — 清晰描述故障现象
> 2. **谁负责?** — 指定明确的责任人 / On-Call
> 3. **怎么修复?** — 链接到 Runbook / 处置文档
> 4. **多久升级?** — P1 无人响应则自动升级到上级
## OpenTelemetry:统一的可观测性标准
过去,Metrics、Logging、Tracing 各用各的工具链,数据割裂在完全不同的系统中。**OpenTelemetry (OTel)** 试图解决这个问题。
> [!summary] OTel 的核心思想
>
> 一套 SDK → 三种信号(Metrics、Logs、Traces)→ 任意后端(Jaeger、Prometheus、ELK...)
>
> 这意味着:**你不需要在代码里为不同后端写多套采集逻辑**。换一个后端只需要改配置,不改代码。
### OTel Architecture
```mermaid
flowchart LR
subgraph APP["你的应用"]
OTEL_SDK["OpenTelemetry SDK
auto-instrumentation Agent"]
end
OTEL_SDK -->|SDK 自动采集| COLLECTOR[(OpenTelemetry Collector)]
COLLECTOR -->|OTLP| JAEGER[(Jaeger
Tracing)]
COLLECTOR -->|OTLP| PROM[(Prometheus
Metrics)]
COLLECTOR -->|OTLP| LOKI[(Loki
Logging)]
style OTEL_SDK fill:#ff9
```
> [!note] 为什么需要 Collector?
>
> Collector 是一个独立的服务进程,承担三个职责:
> 1. **接收**:从应用的 SDK 接收数据
> 2. **处理**:过滤、采样、富化(添加额外标签)
> 3. **导出**:将数据转发到不同的后端存储
>
> 这样你的应用只关心"发送数据",Collector 负责"怎么存、存到哪里"。两者解耦。
### Go 中接入 OpenTelemetry
```go
// 1. 初始化 TracerProvider(Tracing)
provider := sdktrace.NewTracerProvider(
sdktrace.WithBatcherExporter(exporter), // 异步批量上报,不阻塞业务
)
defer provider.Shutdown(context.Background())
tracer := provider.Tracer("order-service")
// 2. 在 handler 中创建 span
func handleOrder(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "handleOrder")
defer span.End()
// 设置丰富的 Span 属性(供查询和过滤)
span.SetAttributes(
attribute.String("http.method", r.Method),
attribute.Int("http.status_code", 200),
attribute.String("user.id", userID),
)
// 调用下游服务(trace context 自动传播)
resp, err := callPayment(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return
}
}
```
> [!tip] 渐进式落地路线
>
> 1. **第一步**:先接 Tracing(价值最大、投入最小),覆盖核心链路
> 2. **第二步**:加 Metrics(Prometheus + Grafana),建立基础监控
> 3. **第三步**:集中 Logging(Loki / ELK),与 trace_id 关联
> 4. **第四步**:全量上 OTel Collector,统一管理三件套
>
> 不要试图一步到位——Tracing 的 ROI 最高,先从这里开始。
## Dashboard 与 On-Call 实践
收集了这么多数据,下一步是怎么用。
### Service Health Dashboard
一个优秀的 Dashboard 应该让任何团队成员在 **30 秒内**了解服务的整体状态:
```mermaid
flowchart TB
subgraph DASH["Service Health Dashboard"]
ROW1["🔴 可用性 & 错误率 — 第一眼判断"]
ROW2["🟡 性能指标 — P50/P90/P99 趋势"]
ROW3["🔵 基础设施 — CPU/内存/连接数"]
ROW4["⚫ 业务指标 — 订单量/支付成功率"]
end
ROW1 --> JUDGE{是否异常?}
ROW2 --> JUDGE
ROW3 --> JUDGE
ROW4 --> JUDGE
JUDGE --"否" --> NORMAL["一切正常 ✓"]
JUDGE --"是" --> ALERT["触发告警 → On-Call"]
```
### Runbook:告警处置手册
告警来了之后该怎么办?答案不应存在某个资深工程师的脑子里。
> [!summary] Runbook 模板
>
> | 字段 | 内容 |
> |------|------|
> | **标题** | `支付服务 P99 延迟 > 2s 持续 5 分钟` |
> | **影响范围** | 下单接口超时,用户体验受损 |
> | **检查步骤** | ① 看 Grafana 延迟面板确认峰值时间点 → ② 查同期部署记录 → ③ 检查下游 DB 慢查询 |
> | **常见原因** | ① 新上线代码有性能 regression → ② DB 连接池耗尽 → ③ 下游超时风暴 |
> | **快速恢复** | ① 回滚最近一次发布 → ② 扩容支付服务实例 → ③ 开启熔断降低下游压力 |
> | **彻底解决** | 排查根本原因,补充回归测试,完善容量规划 |
> [!question] 反模式自检
>
> 如果你的 On-Call 流程是这样的:收到告警 → 群里问 "有人遇到过吗?" → 等半天有人回复 → SSH 到服务器上翻日志 → ……——这说明你们的可观测性体系还不够成熟。**好的 On-Call 体验 = 清晰的告警 + 完善的 Runbook + 一键处置工具**。
## 关联笔记
- [[02-服务治理]] — 熔断器可以触发告警,与监控系统联动;分布式链路追踪章节也有详细说明
- [[05-部署运维]] — K8s Liveness/Readiness Probe 是可观测性的基础
- [[01-基础概念]] — 微服务的整体架构认知