16 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-04-29 12:04 |
可观测性
概述
微服务架构下,一个请求可能穿越十几个甚至上百个服务。排查问题就像在大海捞针。可观测性通过三大支柱(Metrics、Logging、Tracing)让系统行为变得"可见"。
[!question] 核心思考 单体应用出问题,SSH 到服务器上
tail -f日志就能定位。但当你有 50 个服务、每个 3 个副本,日志散落在不同容器里——你还能用同样的方式排障吗?如果不能,什么体系能替代它?
为什么需要独立的"可观测性"体系?
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 示例:定义并注册 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 中的典型视图:
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 串联
{
"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 格式,统一时区 UTClevel— TRACE / DEBUG / INFO / WARN / ERROR / FATALservice— 微服务名称trace_id— 链路追踪 ID,用于跨服务关联msg— 人类可读的消息描述
[!warning] 日志最佳实践
- 不要记录敏感信息:密码、Token、身份证号等绝不能出现在日志中
- INFO 级记录关键业务事件:下单成功、支付回调——这些是排查业务问题的主线
- WARN 级记录异常但不致命:重试了一次、降级走了备用方案
- ERROR 级必须有 trace_id 和错误堆栈,否则毫无价值
日志采集管线
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 回答的问题是:请求在哪一步慢了 / 失败了?——通过完整的调用链路定位瓶颈和故障。
一次请求在多个服务中的完整调用路径:
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 如何从上游服务传递到下游?
// 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)
})
}
// 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% 就告警,而是用户感知到延迟增加时才告
- 降噪:同一个根因可能触发连锁告警,需要抑制机制
告警层级设计
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] 告警规则编写清单
每条告警规则都应该能回答以下问题:
- 什么问题? — 清晰描述故障现象
- 谁负责? — 指定明确的责任人 / On-Call
- 怎么修复? — 链接到 Runbook / 处置文档
- 多久升级? — P1 无人响应则自动升级到上级
OpenTelemetry:统一的可观测性标准
过去,Metrics、Logging、Tracing 各用各的工具链,数据割裂在完全不同的系统中。OpenTelemetry (OTel) 试图解决这个问题。
[!summary] OTel 的核心思想
一套 SDK → 三种信号(Metrics、Logs、Traces)→ 任意后端(Jaeger、Prometheus、ELK...)
这意味着:你不需要在代码里为不同后端写多套采集逻辑。换一个后端只需要改配置,不改代码。
OTel Architecture
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 是一个独立的服务进程,承担三个职责:
- 接收:从应用的 SDK 接收数据
- 处理:过滤、采样、富化(添加额外标签)
- 导出:将数据转发到不同的后端存储
这样你的应用只关心"发送数据",Collector 负责"怎么存、存到哪里"。两者解耦。
Go 中接入 OpenTelemetry
// 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] 渐进式落地路线
- 第一步:先接 Tracing(价值最大、投入最小),覆盖核心链路
- 第二步:加 Metrics(Prometheus + Grafana),建立基础监控
- 第三步:集中 Logging(Loki / ELK),与 trace_id 关联
- 第四步:全量上 OTel Collector,统一管理三件套
不要试图一步到位——Tracing 的 ROI 最高,先从这里开始。
Dashboard 与 On-Call 实践
收集了这么多数据,下一步是怎么用。
Service Health Dashboard
一个优秀的 Dashboard 应该让任何团队成员在 30 秒内了解服务的整体状态:
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 + 一键处置工具。