Theory · Microservice · Observability

可观测性

几十个服务 × 几百个实例 —— 没有 Logging / Metrics / Tracing 三支柱,故障排查就是盲人摸象

一句话本质

通过系统的外部输出理解其内部状态:日志(离散事件细节)、指标(聚合趋势)、追踪(请求全旅程)

技术栈

OpenTelemetry(统一采集 SDK+OTLP 协议)→ Prometheus(指标)· Loki/ELK(日志)· Tempo/Jaeger(追踪)

工程闭环

SLI/SLO 定义目标 → 告警分级 → 排障方法论(指标定位 → 链路收敛 → 日志定因)→ 复盘沉淀

这份 deck 回答"跨几十个服务怎么排障":三支柱各自解决什么 → Logging 结构化实践 → Metrics 的 Prometheus 模型与 RED 方法 → Tracing 的上下文传播机制 → OpenTelemetry 统一架构 → 采样 → SLI/SLO → Go 实践 → 告警与排障方法论 → QA。核心记忆点:trace context 跨服务传播、Prometheus 四类指标、头部 vs 尾部采样。

Why Observability Matters

先看现场:服务拆开之后,排障到底多了什么难题

同一个故障,在单体和微服务里完全是两种难度

阶段用户报“下单失败/很慢”之后发生了什么
单体时代几台机器、一个日志文件、一个数据库。登上去 grep 订单号 → 看到异常栈 → 5 分钟定位到慢 SQL。
拆成微服务下单请求穿过网关 → 订单 → 库存 → 支付 → 风控 → 通知,6 个服务 × 平均 8 个实例 = 48 个进程各自写日志。登哪台机器都只能看到自己那一小段。
结果订单组说“我这边正常”,库存组说“我这边也正常”,支付组说“我返回值是对的”。最后靠逐个服务加日志 → 重新发版 → 等复现3 小时起步。
关键观察:故障难度不是“变复杂了一点”,而是性质变了——单体里“谁能看到全部信息”是默认成立的,微服务里这个前提没了。每个实例只知道自己的一秒钟,没人知道整件事。

拆开之后,多出来的三个新问题

① 认不出“同一次请求”:48 个实例的日志混在一起,哪些行属于用户这一次点击?没有统一 ID 就没法串。
② 不知道慢在“哪一跳”:每个服务只记自己的耗时,加起来对不上用户感受到的 12 秒——中间还有排队、重试、网络。
③ 不知道“是不是真坏了”:没有聚合数字,只能等用户投诉才发现。等发现时,已经坏了一小时。

可观测性换掉了什么

它换掉的不是日志本身,而是那套“靠猜 → 加日志 → 重新发版 → 等复现”的排障方式。装上三支柱之后,同样的问题变成:指标先告诉你“订单服务 P99 从 200ms 涨到 3s”(30 秒发现),trace 再告诉你“慢在支付那一跳”(1 分钟收敛),最后用 trace_id 到那一个实例捞日志看到根因(5 分钟定因)。
总耗时:3 小时 → 约 7 分钟,而且不需要重发版。

本 deck 的路线

先看三支柱各答什么问题(第 3 页)→ 逐一展开日志 / 指标 / 追踪 → 用 OpenTelemetry 把它们接起来 → 处理成本(采样)目标(SLO)→ 最后落到 Go 工具箱与排障五步法。读完你应该能回答:“这条链路到底怎么查”。

动机页:用“单体 5 分钟 vs 微服务 3 小时”的具体对比建立痛感,指出拆服务后丢失的前提是“有人能看到全部信息”。三个新问题(认不出同一次请求/不知慢在哪跳/不知是否真坏)恰好一一对应后面 trace_id / trace / 指标告警三件事。结尾给出 7 分钟 vs 3 小时的量化收益,并列出本 deck 路线。

Three Pillars

可观测性 vs 监控:三支柱各答一个问题

开场那三个问题,正好一人领走一个:认不出请求 → 追踪给 trace_id;不知慢在哪跳 → 追踪画调用树;不知是否真坏 → 指标做聚合告警。

可观测性三支柱与协作关系 三根支柱:指标负责回答哪里有问题(聚合趋势与告警触发),追踪负责回答请求经过了哪里(跨服务链路收敛),日志负责回答到底发生了什么(单点细节与根因)。下方展示排障时三者协作的漏斗顺序:指标发现异常、追踪缩小范围、日志定位原因。 Metrics 指标 哪里有问题?(聚合·趋势·告警) Prometheus · counter/gauge/histogram 成本低 · 可长期保留 · 无个体细节 RED: Rate / Errors / Duration Tracing 追踪 请求经过了哪里?(跨服务旅程) trace_id / span / context 传播 OTel · Jaeger/Tempo · 采样取舍 跨服务因果链还原的唯一手段 Logging 日志 到底发生了什么?(单点细节) 结构化 JSON · level/trace_id 字段 Loki/ELK · 成本最高 · 细节最全 最终定因的"第一现场" 排障漏斗(三者如何协作) ① 指标告警发现异常(错误率↑ P99↑)→ ② Trace 按 trace_id 收敛到慢/错的那一跳 → ③ 到该实例用 trace_id 捞日志看根因 → ④ 修复后回看指标恢复曲线 关联三件套的钥匙 = trace_id 同时出现在指标 exemplar / span / 日志行里 —— 打通才能"一键跳转"
三支柱各答一个问题:指标答"哪里有问题"(聚合、便宜、无细节)、追踪答"经过哪里"(跨服务因果)、日志答"发生了什么"(细节最贵)。可观测性 vs 监控的定义差异:监控面向已知故障模式,可观测性面向未知问题的探索。排障漏斗是三者的协作顺序,trace_id 是打通三支柱的钥匙。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(先记住这个,细节后面展开)
可观测性
Observability
只看系统的外部输出,就能推断它内部发生了什么——强在能回答事先没想到过的问题
监控
Monitoring
针对已知故障模式设阈值并告警(CPU>80%)。功能是可观测性的子集:告警靠监控,定因靠可观测
日志
Log
一条一条的离散事件记录:细节最全、成本最高,用来看"到底发生了什么"
指标
Metric
可聚合的数值时间序列(QPS、错误率、P99):便宜、能长期存,但没有个体细节
追踪 / 跨度
Trace / Span
一次请求走过的全部步骤叫 trace;其中一个步骤(一次 RPC、一次 SQL)叫 span,带起止时间与父子关系
trace_id
上下文传播
整条链路共用的同一个编号,靠 HTTP header / gRPC metadata / MQ 消息头逐跳往下传——打通三支柱的钥匙
标签基数
Cardinality
标签取值能组合出多少种时间序列。user_id 做标签 = 几百万种 = 打爆存储,只能放服务/接口这类可枚举维度
分位数 P99把请求按耗时从快到慢排,第 99% 位置那一笔的耗时。比"平均耗时"更能反映长尾用户的真实感受
SLO / 错误预算SLO = 服务质量目标(如 30 天成功率 ≥99.9%);错误预算 = 1−SLO,即允许"欠"多少失败(99.9% ≈ 43 分钟/30 天)
采样
Sampling
只记录一部分链路以省成本。头部=入口随机掷骰子,尾部=链路跑完按结果决定(错误/慢的全留)

如果这些概念还不熟,先看这三篇

微服务总览 → 为什么要把一个系统拆成一堆服务(本 deck 的一切痛点都源于这次拆分)
服务间通信与 RPC → "一跳"到底是什么、context 是怎么传过去的
GMP 调度 → 后面读 Go 的 goroutine / 调度延迟指标时需要

本 deck 怎么用这些词

统一直觉:一次请求 = 一串 span。给这串 span 编一个号(trace_id),写进每一个服务打出的每一行日志;同时把所有同类 span 的数值聚合成指标。于是"全站趋势"和"某个用户这一次点击"共用同一个编号——你可以从一张趋势图一路钻到具体那一行日志。这就是三支柱唯一要做的事。

一个提前建立的直觉

三支柱不是三套并列的系统,而是一个漏斗的三层:指标最便宜所以全量(发现问题)→ 追踪按 trace_id 收敛(缩小范围)→ 日志最贵所以只在确定的那一处捞(定因)。成本从低到高、细节从粗到细,排障时顺着这个漏斗往下走。

阅读提示:术语不用背,忘了回来查这一页就行。真正需要记住的只有两件事:trace_id 是打通三支柱的钥匙,以及三支柱是一个"由粗到细"的漏斗
前置页:术语按“信号类型 → 关联机制 → 度量方法 → 成本治理”分组,避免一上来堆组件名。右侧给三篇前置 deck 与一个统一心智模型(编号 + 聚合 = 趋势与个体共用一套 ID),并提前建立“三支柱是由粗到细的漏斗”这一组织性直觉。

Pillar 1

Logging:结构化日志是唯一正确的日志

结构化(JSON)vs 文本日志

// ✅ 结构化:字段可检索可聚合
{"ts":"2026-09-02T10:00:00Z",
 "level":"error","msg":"pay failed",
 "trace_id":"a1b2c3",
 "order_id":"ORD-20260902",
 "err":"timeout after 500ms"}

// ❌ 文本:grep 一把梭但无法聚合
"[ERROR] 2026-09-02 pay failed
 for order ORD-2026... timeout"

结构化才能做到:按字段过滤(order_id)、跨实例聚合统计、与 trace 联动(trace_id 字段)。

日志实践清单

  • 级别纪律:ERROR(需处理/告警)· WARN(潜在问题)· INFO(关键业务动作)· DEBUG(默认关闭,按需开)——ERROR 打告警,别把 INFO 打成 ERROR 稀释告警
  • 必带字段:ts/level/msg/trace_id/服务名/实例标识;业务关键 ID(订单号等)
  • 采样与限流:高频错误日志限速(每秒 N 条),防"日志风暴"反噬 IO
  • 不记敏感信息:密码/token/身份证脱敏——合规硬要求
  • Go 库:zap(性能)· slog(标准库 1.21+,结构化原生)

收集链路

应用 → stdout/stderr → sidecar/节点代理采集(Filebeat/Fluent Bit/Promtail)→ Kafka 削峰 → 存储(Loki:只索引标签省成本;ES:全文索引能力强)。选型口诀:标签检索选 Loki,全文检索选 ES,成本敏感 Loki + 对象存储。

Logging 页:结构化 JSON 是唯一正解(对比示例代码)、级别纪律(ERROR 才告警)、必带字段(trace_id 联动)、采样限流防日志风暴、敏感脱敏、Go 库选型(zap/slog)。收集链路:stdout → 代理 → Kafka → Loki/ES,选型口诀按检索需求。

Pillar 2

Metrics:Prometheus 指标模型与 RED 方法

四类指标类型(必背)

类型语义
Counter只增不减的累计值请求总数、错误总数
Gauge瞬时值可增可减goroutine 数、队列深度、CPU
Histogram分桶统计分布(可算分位数)请求延迟 P50/P95/P99
Summary客户端直接算分位数多实例无法聚合,慎用(histogram 替代)

rate()/histogram_quantile() 是两个最常用的 PromQL 函数——延迟分位数必须用 histogram 分桶在服务端算。

RED / USE 方法论

RED(面向服务/请求):Rate(QPS)· Errors(错误率)· Duration(延迟分布)——每个对外接口都要有这三元组;USE(面向资源):Utilization(使用率)· Saturation(饱和度)· Errors(错误)——CPU/内存/连接池/队列。组合:RED 看服务健康,USE 找资源瓶颈。

Pull 模型与标签设计

Prometheus 主动拉取 /metrics(服务发现驱动)——故障时"拉不到"本身也是信号(对比推模型的失联沉默问题);标签基数纪律:user_id/order_id 级别不要做 label(组合爆炸打爆 TSDB)——高基数细节交给日志与 trace,指标只留可枚举维度(服务/实例/接口/状态码)。

指标模型示例:http_request_duration_seconds_bucket{service="order",method="POST",le="0.1"} —— label 定位维度、bucket 定位分位;一切告警 SLO 都建立在正确的指标设计上。Exemplar(OTel 扩展):直方图样本可携带 trace_id——指标图上直接跳转到对应 trace。

Metrics 页:四类指标(Counter/Gauge/Histogram/Summary,Summary 多实例不可聚合的坑)、RED/USE 两方法论、Pull 模型的优势(拉不到即信号)、标签基数纪律(高基数进日志不进指标)。Exemplar 打通指标到 trace 的跳转是进阶点。

Pillar 3

Tracing:trace context 的跨服务传播

分布式链路追踪上下文传播 入口网关生成 trace_id 和根 span,随后每次 RPC 调用把 trace 上下文通过 W3C traceparent 头传给下游:订单服务、库存服务、支付服务各自创建子 span 并继续向下传播,MQ 异步消息同样携带上下文。所有 span 汇聚到追踪后端,按 trace_id 还原完整调用树,定位耗时最长的一跳。 客户端 API 网关 生成 trace_id + root span traceparent: 00-{trace-id}-{parent-span-id}-01 W3C TraceContext 标准 —— 随每一跳 HTTP header / gRPC metadata / MQ 属性传播 订单服务 span① client span → 下游继续传播 库存服务 span② server span + DB 子 span 支付服务 span③ server span(故障跳示例) MQ 异步消息(Kafka) 消息头携带 trace context → 消费侧续链 追踪后端(Tempo/Jaeger) 按 trace_id 还原调用树 · 找到最耗时/出错的一跳 span 四要素:trace_id span_id/parent/时间与属性 传播断了 = 链路断了:自研 RPC/MQ 框架必须显式注入/提取 context(OTel 已覆盖 gRPC/HTTP/Kafka 主流插件);跨"物理进程边界的每一步"都要续链。
Tracing 核心机制:trace_id 全局唯一 + span 树(span_id/parent/时间/属性);W3C TraceContext 的 traceparent 头随每跳传播(HTTP header/gRPC metadata/MQ 消息头),MQ 异步也要续链。传播断链是排障大坑——自研框架必须显式接 OTel 插件。span 树按 trace_id 还原调用树找最耗时一跳。

OTel

OpenTelemetry:统一采集的事实标准

它统一了什么

  • API/SDK 统一:一套代码产出 traces/metrics/logs 三信号(Go SDK v1.46,2026-08 核实)
  • 协议统一:OTLP(gRPC/HTTP)作为标准传输——替换 Jaeger client/StatsD 等碎片协议
  • 厂商中立:应用只接 OTel,后端(Jaeger/Tempo/Datadog/云厂商)随时可换——反锁定
  • 上下文传播统一:W3C TraceContext/B3 等自动兼容,Baggage 支持业务上下文随链路传播
  • 语义约定统一:HTTP/DB/消息的 span 与指标字段命名走 semconv——跨厂商后端的数据可互认、可互换

架构:SDK + Collector

应用 → OTel SDK(插桩)
  → OTLP → Collector(可选但推荐)
      receivers  # 接收
      processors # batch/sampling
      exporters  # 多后端分发

# Collector 价值:
#  - 卸载应用侧开销(批量/重试)
#  - 统一采样/脱敏/富化
#  - 后端解耦(换后端不改应用)

部署形态:Agent(每节点 DaemonSet)或 Gateway(中心集群)。

OTel 页(上):统一的五个层面(API/SDK、OTLP 协议、厂商中立、传播上下文与 Baggage、语义约定)与 SDK+Collector 架构(Collector 三价值:卸载应用侧开销 / 统一采样脱敏富化 / 后端解耦)。

OTel in Practice

OTel 落地:Go 最小接入与插桩三层

上一页回答"为什么用它",这一页回答"到底怎么接进去"。

Go 侧最小接入(gRPC + OTel)

// 初始化一次,全局生效
tp := sdktrace.NewTracerProvider(
  sdktrace.WithBatcher(exporter),
  sdktrace.WithResource(res))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(
  propagation.TraceContext{})

// gRPC 拦截器自动注入/提取
grpc.Dial(addr,
  grpc.WithUnaryInterceptor(
    otelgrpc.UnaryClientInterceptor()))
grpc.NewServer(
  grpc.UnaryInterceptor(
    otelgrpc.UnaryServerInterceptor()))

插桩层次(答"怎么接")

零插桩/eBPF 自动埋点:Grafana Beyla/Otel-Auto-instr——无需改代码,适合存量;② 库级插桩:OTel 官方 Instrumentation(gin/grpc-go/database/sql 桥接 otelsql);③ 手动插桩:业务关键路径自建 span + 自定义属性(业务维度最值钱)。三层叠加,手动只做"值得记"的关键点。

手动 span 的纪律:只给下单/扣款/风控这类业务关键点建 span,其余交给库级自动埋点——span 数 × 属性大小直接决定 trace 存储成本,手动埋点埋的是"排障价值",不是"覆盖率"。

OTel 落地页:Go 最小接入代码(API/SDK、OTLP 协议、厂商中立、传播上下文+Baggage)、SDK+Collector 架构(Collector 三价值:卸载/治理/解耦)、Go 最小接入代码(TracerProvider+TraceContext propagator+gRPC 拦截器)、插桩三层(eBPF 自动/库级/手动)。

Sampling

采样:全量太贵,怎么不丢关键信息

头部采样 vs 尾部采样

  • 头部采样(Head Sampling):请求入口掷骰子决定整条链路是否记录(如 1%)——开销最低,但可能丢弃"后来才出错"的关键链路
  • 尾部采样(Tail Sampling):整条链路完成后按结果决定——错误链路/慢链路(P99 之外)全保留,正常链路抽 1%;Collector 的 tail_sampling processor 实现,代价是 Collector 需缓存整链(内存与延迟成本)
  • 组合:头部粗滤(10%)+ 尾部精筛(错误/慢全留,正常再抽 10%)

采样策略设计(实践口径)

  • 错误全保:error 状态 span 的链路 100% 保留
  • 慢链路全保:超过阈值(如 1s)全保——长尾是排障重点
  • 新接口/灰度期提高采样率:上线初期 10-100%,稳定后降
  • 按业务重要性分层:支付链路采样率高于浏览链路
  • 指标不受采样影响:RED 三元组必须全量——采样只针对 trace(这点常被误答)

trace_id 全量打进日志(保底技巧)

trace 被"不采样"并不代表信息丢失:所有日志行都带 trace_id——出事后即使没有完整 span 树,也能按 trace_id 串起各服务的日志还原时间线。日志是低成本全量底座,trace 是高成本精读层——两者互补而不是替代。

成本视角

trace 存储是三支柱最贵的(每链路几十个 span×属性)。控制手段:采样率、属性精简(不塞大 payload)、TTL 分层(热数据 SSD 7 天 → 冷数据对象存储)。可观测成本是"数据税"——设计时按"单位排障价值/字节"取舍。

采样页:头部采样(入口掷骰子,便宜但丢关键)vs 尾部采样(按结果保留错误/慢链路,Collector 缓存成本)与组合策略;五条实践口径(错误全保、慢全保、新接口提高、业务分层、指标不受采样影响——这条常被误答)。保底技巧:日志带 trace_id 全量兜底;成本视角的分层存储。

SLO

SLI / SLO / 错误预算:可观测的目标语言

概念链(背定义)

  • SLI(Indicator):从指标计算的服务质量比率,如 成功请求率 = 非5xx / 总请求延迟达标率 = P99<200ms 的请求占比
  • SLO(Objective):对 SLI 的目标,如"30 天滑动窗口内成功率 ≥ 99.9%"
  • 错误预算 = 1 − SLO:99.9% → 30 天可"欠"43 分钟——预算内放心发布,预算烧完冻结变更修稳定性
  • SLA:对外合同承诺(违约赔偿),SLO 是内部更严的目标——内部 SLO 严于对外 SLA 留缓冲

错误预算的管理价值(SRE 核心)

  • 把"稳定性 vs 迭代速度"从吵架变成数据决策:预算充足→快速发布;预算 <25% → 提高风险审查;耗尽→冻结非稳定性变更
  • 多窗口多燃烧率告警:快烧(1h 窗口 14.4×速率)立即呼人,慢烧(3d 窗口 6×)工单处理——避免"99.0% 一眼看不出问题"的静态阈值
  • SLO 按用户旅程定义(下单成功率),不是按机器 CPU——面向用户而不是面向资源

下单链路 SLO 示例

下单成功率 ≥ 99.95%(30d)· 下单 P99 < 500ms ≥ 99% · 支付回调处理延迟 P99 < 3s ≥ 99.9%——每个 SLO 都能对应到具体 SLI 查询语句

与告警的衔接

告警 = 预算燃烧速率超阈值;看板 = 预算余量+燃烧趋势。错误预算告警替代"CPU>80% 告警"这类噪音源——用户没受影响就不该呼人

落地顺序

先给 1 条核心链路建 SLO(别贪多)→ 跑一个月校准阈值 → 再铺开。SLO 是治理工具不是 KPI——用 SLO 考核团队会催生"指标造假",Google SRE 明确警告过

SLO 页:概念链(SLI→SLO→错误预算→SLA,内部严于外部)、错误预算的管理价值(数据决策发布节奏、多窗口燃烧率告警、面向用户旅程),示例 SLO 与落地顺序(一条链路先跑一个月,SLO 不是 KPI——Google SRE 的警告值得引用)。

Go Toolkit

Go 可观测工具箱:pprof / runtime/metrics / expvar

pprof:性能剖析四件套(高频考点)

import _ "net/http/pprof"
go http.ListenAndServe(
  ":6060", nil)  // 内网端口!

// CPU 火焰图 (30s 采样)
go tool pprof -http=:8080 \
  http://pod:6060/debug/pprof/profile?seconds=30
// 内存 profile
.../debug/pprof/heap
// goroutine 泄漏定位
.../debug/pprof/goroutine?debug=2
// 阻塞/互斥锁分析
.../debug/pprof/block · mutex

事故现场三板斧:CPU 火焰图找热点、heap 看泄漏、goroutine dump 看谁阻塞——配合 gmp/gc deck 的原理理解采样数据。

运行时指标(Prometheus 暴露)

必暴露的自定义+运行时指标:go_goroutines / go_gc_duration_seconds / process_resident_memory_bytes;业务侧:连接池活跃数(db/stats)、队列深度、缓存命中率。runtime/metrics 包提供全量运行时探针(新版优先于 expvar)。

trace 里的 Go 特有信号

runtime/trace(go tool trace):调度延迟、GC 停顿、网络/syscall 阻塞的时间线视图——pprof 是"谁占 CPU",trace 是"时间线上发生了什么"。GC 抖动排障组合:metrics 看到 GC 频率异常 → pprof heap 看分配热点 → go tool trace 看停顿影响(对照 Go GC deck 的三色标记与写屏障)。

上线检查清单:pprof 端口只绑内网(不是公网!信息泄漏风险)· /healthz 与 /readyz 分离 · 优雅关闭 flush 指标与 trace(before exit)· OTel exporter 失败不阻塞业务(异步批处理)。

Go 工具箱:pprof 四件套(profile/heap/goroutine/block+mutex,代码示例可背)、runtime/metrics 与 Prometheus 暴露清单、go tool trace 时间线视图(与 pprof 的分工:谁占 CPU vs 时间线发生了什么)。上线清单四条,pprof 端口不得公网是安全红线。与 gmp/gc deck 的原理互链。

Troubleshooting

告警分级与排障方法论:从"发现"到"定因"

告警分级(有呼人成本意识)

  • P0 呼人(电话):核心链路不可用、错误预算快烧——要求立即响应
  • P1 IM 推送:非核心降级、慢燃烧率——工作时间处理
  • P2 工单:趋势性劣化、容量预警
  • 纪律:每条告警必须有动作(runbook 链接)——不能"看了也不知道干嘛"的告警一律下线;告警疲劳是最大敌人

五步排障法(面试讲这个)

  • ① 定范围:影响面(哪些接口/用户/比例)——指标看板先行
  • ② 定时间:起点(发布?流量突增?依赖变更?)——变更关联是第一直觉,80% 故障紧跟变更
  • ③ 定位置:trace 收敛到异常跳(哪一跳慢/错、参数是什么)
  • ④ 定原因:该跳的日志(trace_id 捞)+ 资源指标(USE)+ 必要时 pprof 现场剖析
  • ⑤ 止血优先于定因:可回滚先回滚、可降级先降级——恢复业务后再深挖根因(分层:止血/根因/复盘)

高频症状 → 优先怀疑(速查表)

症状先查
P99 涨但 QPS 稳定慢查询/慢依赖(trace 定位)、GC(pprof heap)
错误率突增最近变更、依赖健康、限流熔断是否误触发
内存持续上涨goroutine 泄漏(dump)、heap 对比、缓存无淘汰
全链路超时底座(网络/DNS)、注册中心、配置误发

复盘文化(可观测的最后一环)

无责复盘(blameless):时间线 → 根因(5 why)→ 改进项(可验证、有 deadline)→ 沉淀为监控/告警/runbook。衡量指标:MTTD(发现时长)与 MTTR(恢复时长)——可观测性建设的成果最终体现在这两个数字上。

排障方法论:告警三级(P0 电话/P1 IM/P2 工单)+ 每条告警必须有动作;五步排障法(定范围→定时间→定位置→定原因→止血优先于定因);症状速查表四行(P99 涨/错误率涨/内存涨/全链路超时各自先查什么);无责复盘与 MTTD/MTTR。

Cheat Sheet

一页带走:三支柱 · 选型 · 排障

① 三支柱:一句话分工与成本

指标 Metrics答"哪里有问题":聚合数字、最便宜、可长期存、无个体细节 → 负责告警
追踪 Tracing答"经过了哪里":span 树还原因果,负责收敛到某一跳
日志 Logging答"到底发生了什么":细节最全、最贵,负责最终定因
打通的钥匙trace_id 同时写进 span、日志行、指标 exemplar —— 才能从趋势图一键钻到日志

② 指标选型:四类与两个坑

Counter / Gauge累计只增(请求数)/瞬时可增可减(队列深度、goroutine 数)
Histogram ★服务端分桶聚合 → 多实例可算全局 P99;微服务默认选它
Summary ✗客户端算分位,多实例无法聚合——多副本场景别用
标签基数纪律只放可枚举维度(服务/接口/状态码);user_id 级别交给日志与 trace
RED / USERED 看服务(QPS/错误率/延迟),USE 找资源瓶颈(使用率/饱和度/错误)

③ 采样:怎么省成本又不丢关键

头部采样入口掷骰子决定整条链路留不留 —— 便宜,但会丢掉后来才出错的链路
尾部采样 ★链路跑完按结果决定:错误 / 慢的全保,正常的再抽样;代价是 Collector 要缓存整链
组合(推荐)头部粗滤 10% + 尾部精筛(错误/慢全留,正常再抽 10%)
最易错的一条指标不受采样影响:RED 三元组必须全量,采样只针对 trace
保底技巧trace 被丢也不怕:日志行全量带 trace_id,仍能按 ID 串起时间线

④ SLO 与错误预算

99.9% / 30 天错误预算 ≈ 43 分钟;99.99% ≈ 4.3 分钟(每多一个 9,预算 ÷10)
预算管理充足→放心发布;<25%→提高审查;耗尽→冻结非稳定性变更
告警口径燃烧速率告警(快烧呼人、慢烧工单),不用"CPU>80%"这类噪音源

⑤ 排障五步(顺着漏斗走)

指标定范围 → 变更关联定时间(80% 故障紧跟变更)→ trace 定位置(收敛到异常那一跳)→ 日志 + pprof 定原因止血优先于定因(可回滚先回滚)。
症状速查:P99 涨 / QPS 稳 → 慢查询或 GC;错误率突增 → 变更与依赖;内存涨 → goroutine 泄漏;全链路超时 → 底座(网络 / DNS / 注册中心)。
速查页是“可检索性优于一次性”原则的落点:五块按使用顺序排(先分工、再选型、再成本、再目标、最后处置)。三支柱分工表对应第 3 页,采样与 SLO 对应第 8/9 页,排障五步对应第 11 页。

Interview QA

高频 QA

先自己答一遍再往下看——想不起来比看得顺眼记得牢;答不上就翻回上一页速查表。

Q1 可观测性和监控的区别?

已知 vs 未知

监控面向已知故障模式:预设阈值、触发告警(CPU 高、错误率高);可观测性面向未知问题:通过丰富的外部输出(结构化日志/多维指标/分布式 trace)让工程师能提出任意新问题并得到答案。监控是可观测性的子集——告警靠监控,定因靠可观测。

Q2 一个请求跨服务变慢,怎么定位哪一跳的问题?

trace 收敛span 树

拿到慢请求的 trace_id(从日志/网关响应头/采样系统查):打开 span 树看各 span 的时长与父子关系——最深的耗时叶子节点就是瓶颈(DB 查询/RPC/队列等待);span 属性带 SQL/目标地址等细节;若是概率性变慢,用尾部采样捞慢链路 + 多条 trace 对比找共性。整个流程秒级——这就是 trace 的价值。

Q3 Prometheus 的 Pull 模型有什么优缺点?

自愈信号短任务问题

优点:① 拉不到数据本身就是异常信号(实例挂了不需要心跳超时推断);② 服务端限流可控(拉取频率由 Prometheus 决定,防止被采集打爆);③ 目标发现天然对接服务发现。缺点:短生命周期任务(Job)可能在两次拉取间结束——Pushgateway 补丁;NAT 后的实例不可达需要网关。被问"为什么不用 Push"按这个答。

Q4 Histogram 和 Summary 怎么选?

服务端 vs 客户端分位

默认 Histogram:分桶在服务端聚合,多实例可汇总算全局 P99(histogram_quantile),代价是桶粒度是近似值;Summary 在客户端直接算分位(精确)但多实例无法聚合、且分位数集合固定。多副本微服务一律 Histogram——"单实例精确 vs 全局可聚合"是选择的核心权衡。

Q5 尾部采样为什么需要?代价是什么?

按结果保留

头部采样在入口随机——只采 1% 时,恰好"出错的链路"有 99% 概率被丢掉;而排障最需要的就是错误/慢链路。尾部采样在链路完整后按结果决策:错误/慢全保+正常抽样。代价:Collector 要缓存整条链路(跨服务聚合 span)才能决策——内存/延迟/复杂度都高于头部采样,所以是组合使用。

Q6 线上 goroutine 数暴涨怎么排查?

dump 对比泄漏模式

① 指标确认(go_goroutines 趋势)→ ② /debug/pprof/goroutine?debug=2 拿全量栈 dump,按阻塞点聚类——几千个 goroutine 卡在同一 ch <- / Lock / HTTP 无超时读,就是泄漏点;③ 常见模式:channel 无缓冲无人消费、锁未释放、HTTP/RPC 缺 context 超时、无限 for+time.Sleep;④ 修复后用 heap+goroutine 对比验证。原理支撑见 channel/sync deck。

六题:可观测 vs 监控定义、跨服务慢请求定位(trace 收敛五步)、Pull 模型优缺点、Histogram vs Summary(全局可聚合 vs 客户端精确)、尾部采样的必要性与代价、goroutine 暴涨排查(dump 聚类四步)。

Related & References

相关知识点与参考

本领域相关 deck

跨领域相关 deck

  • Go GC —— GC 指标异常的原理层解读
  • GMP 调度 —— goroutine 泄漏与调度延迟的机制根源
  • Channel 底层 —— 阻塞类泄漏的原理定位
  • Kafka 底层 —— 消息链路观测的消费端延伸

参考链接(一手来源)

收尾链接:领域内网关(trace 起点)、RPC(插桩挂载)、容错(状态可观测)、配置(告警下发);跨领域 Go GC/GMP/Channel(原理层解读指标异常)。参考:OTel 官方文档、W3C TraceContext、Prometheus 文档、Google SRE Book/Workbook、Go pprof 与 runtime/metrics。