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

16 KiB
Raw Blame History

tags, create time
tags create time
microservice
observability
monitoring
tracing
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 格式,统一时区 UTC
  • level — TRACE / DEBUG / INFO / WARN / ERROR / FATAL
  • service — 微服务名称
  • 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] 告警规则编写清单

每条告警规则都应该能回答以下问题:

  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

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

// 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 秒内了解服务的整体状态:

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