--- 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-基础概念]] — 微服务的整体架构认知