447 lines
16 KiB
Markdown
447 lines
16 KiB
Markdown
---
|
||
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["同一份日志<br/>一目了然"]
|
||
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["问题:日志分散、链路断裂<br/>传统监控无法回答'这个请求经历了什么'"]
|
||
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<br/>[Counter]"] -->|关联分析| G2["P99 Latency<br/>[Histogram]"]
|
||
G3["错误率<br/>[Counter %]"] -->|关联分析| G2
|
||
G2 --> G4["告警规则<br/>阈值触发"]
|
||
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<br/>trace_id: abc-123"]
|
||
|
||
subgraph SPANS["调用链 (Trace)"]
|
||
direction TB
|
||
S1["Span #1<br/>API Gateway<br/>5ms"]
|
||
S2["Span #2<br/>Order Service<br/>70ms"]
|
||
S3["Span #3<br/>Payment RPC<br/>160ms"]
|
||
S4["Span #4<br/>Inventory RPC<br/>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<br/>auto-instrumentation Agent"]
|
||
end
|
||
|
||
OTEL_SDK -->|SDK 自动采集| COLLECTOR[(OpenTelemetry Collector)]
|
||
|
||
COLLECTOR -->|OTLP| JAEGER[(Jaeger<br/>Tracing)]
|
||
COLLECTOR -->|OTLP| PROM[(Prometheus<br/>Metrics)]
|
||
COLLECTOR -->|OTLP| LOKI[(Loki<br/>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-基础概念]] — 微服务的整体架构认知
|