This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hzh/MS/04-可观测性.md
T

447 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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-基础概念]] — 微服务的整体架构认知