监控与可观测 — 三大支柱、OpenTelemetry 与追踪瀑布图
监控与可观测 — 三大支柱、OpenTelemetry 与追踪瀑布图
这一章讲什么: 健康检查只能告诉你「死了没」,不能告诉你「为什么慢」。 这一章给应用装上「内窥镜」:OpenTelemetry 自动给 Express、HTTP、 Prisma 的每次调用生成追踪,汇进 Grafana,让你在一个瀑布图里 看见一次请求在每一层花的时间。 读完你能分清监控与可观测,并能亲手用追踪定位一个慢请求的根因。
1. 监控 vs 可观测:能预知的和不能预知的
监控回答:「系统在工作吗?」——预先决定量什么、阈值定多少,
越过预设阈值(界限值)就报警。第 16 章的 /healthz、/readyz、CPU 面板、
「错误率超 5% 就告警」全是监控。它擅长已知问题:库挂了→
readiness 失败→报警响→你知道发生了什么1。
可观测回答:「为什么不行了?」——不部署新代码、不加新检查, 光靠应用已产出的数据,就能回答你没预想过的问题。对比一针见血: 监控告诉你「API 响应从 50ms 跳到 800ms,发生在 14:32」; 可观测让你追问「哪个端点慢了、哪条数据库查询该负责」—— 你不需要预测这个故障场景,数据早就在那里等你问1。
| 监控 | 可观测 | |
|---|---|---|
| 问题 | 在工作吗? | 为什么不在? |
| 手法 | 预定检查与阈值 | 探索数据找未知问题 |
| 数据 | 指标、健康检查 | 日志+指标+追踪协同 |
| 适用 | 已知故障模式 | 未知/意外故障 |
两个都要:监控抓得住你预知的,可观测救得了你预想不到的1。
2. 三大支柱与它们的乘法
日志(logs):事件记录。结构化 JSON 日志里那个 traceId 字段,
就是把一条日志接到某次请求追踪上的钩子——相关性不会自动发生,
要_logger_主动从 OTel 上下文里取 traceId 写进每条日志。
原则:足够重建现场,但不泄敏感数据(原始 email、令牌、完整请求体
都不进日志;span 属性同理)2。
指标(metrics,随时间收集的数字测量)。只记数字,所以能存百万级数据点; RED 方法一个框架吃全:Rate(每秒请求数)、Errors(失败率)、 Duration(延迟分布,看 p50/p95/p99)。「只记一个框架就记 RED」3。
追踪(traces):一次请求的完整旅程,拆成一串 span—— 每段 span 有开始时间、时长、父 span(树状结构由此而来)和 属性(HTTP 方法、SQL 语句)。书里那张示例图一眼读完全部:
[Trace 4bf92f3577b34da6]
|-- HTTP POST /api/orders (245ms)
|-- express.middleware (2ms)
|-- prisma:client:operation - order.create (198ms)
| |-- prisma:engine:query - INSERT INTO "Order"... (185ms)
|-- express.response (3ms)
总耗时 245ms,其中 185ms 在那条 INSERT 上 —— 慢,也知道慢在哪。乘法在相关性:日志带 traceId → 点进去就是整条追踪; 追踪里看到慢 span → 跳到同时段指标看是孤例还是趋势。 OpenTelemetry 的全部价值,就是一个框架产数据、把三种信号缝起来3。
3. OpenTelemetry:插一次队,换所有后端
OTel 是 CNCF 毕业项目,核心卖点是厂商中立:在它之前, 发给 Jaeger 要用 Jaeger 的库,换 Datadog 就得拆掉重写; 用了 OTel,插桩一次,后端随便换——Grafana、Jaeger、Honeycomb、 Datadog,凡是说 OTLP 协议的都能接。换后端=改配置,不改代码4。
三层架构:应用(装几个 npm 包 + 一个配置文件;auto-instrumentation
自动捕获 HTTP 请求、Express 路由、数据库查询——追踪代码一行不用写)
→ Collector(独立进程:接收、批处理、过滤、采样、转发;可加可不加,
加了就多一层解耦——后端挂了它缓冲重试,还能不改应用就换目的地)
→ 后端(本地开发用 grafana/otel-lgtm 一个镜像打包
Loki+Grafana+Tempo+Prometheus+Collector)4。
包清单里值得知道:auto-instrumentations-node 是元包,
检测到 Express/pg/fetch 就自动建 span;Prisma 不在其列——
Prisma 团队自己维护 @prisma/instrumentation,要单独注册5。
4. 落地:一个文件 + 加载顺序的军规
src/instrumentation.ts 三件事:NodeSDK 配好 OTLP 导出器
( traces/metrics 都走 OTEL_EXPORTER_OTLP_ENDPOINT 环境变量,
代码里不写死 serviceName——配置全走环境变量,同一份代码
跨环境复用);instrumentations 关掉文件系统噪音、滤掉 /healthz 和
/readyz(健康检查的重复 span 只会淹没信号);SIGTERM/SIGINT 时
sdk.shutdown() 把缓冲的遥测冲刷出去(用 process.once,
第二次信号直接快速退出,与第 16 章优雅关停协作)6。
军规只有一条,却是「装了 OTel 却看不到 trace」的头号原因:
instrumentation 必须在应用代码之前加载。OTel 靠 import 时打补丁;
如果你的应用先 import 了 Express,补丁没打上,一切白搭。
ESM 项目用 --import ./src/instrumentation.ts +
--experimental-loader=@opentelemetry/instrumentation/hook.mjs;
CommonJS 项目用 --require 即可。连 dev 脚本(tsx)都要带上
--import,否则「生产有 trace、开发没有」7。
Compose 里加一个 otel-stack 服务(grafana/otel-lgtm,版本钉死);
OTLP 端口用 expose 而非 ports(容器网内部通信,不暴露宿主);
API 侧两个环境变量 OTEL_EXPORTER_OTLP_ENDPOINT /
OTEL_SERVICE_NAME。路由处理器一行没改——这就是 auto-instrumentation
的语义:代码不动,全栈有图8。
5. 主线例子:故障注入——瀑布图上一眼定位 800ms
教程的高光设计:先看健康 trace(52ms,Prisma 查询占大头),
然后人为制造故障——给 users 路由加
await new Promise(r => setTimeout(r, 800)),再打几次请求,
回 Tempo 按时长降序排9:
正常: [handler 52ms]──[prisma 40ms] ← DB 是主耗时
注入后: [handler 850ms] ←─────大段空白─────→ [prisma 40ms]
瀑布图把「时间丢在哪」画成了形状:
850ms 的处理器 span 里,Prisma 缩在最右还是 40ms ——
800ms 消失在处理器内部、数据库调用之前。
没有加一行调试日志,没有重现环境。反过来若 Prisma span 自己变成 750ms,瀑布图立刻指认数据库, span 属性里还躺着那条确切的 SQL。这就是开篇那句话的兑现: 「你不需要预知这个故障,数据早已在那里」9。
跨服务的追踪靠 W3C Trace Context:API 对外发 HTTP 请求时,
OTel 自动注入 traceparent 头,接收方取出来接续同一条 trace——
单机只见 API 内部,加了后台 worker/通知服务,图自动连起来。
浏览器到 API 则要双边配置:前端 @opentelemetry/sdk-trace-web +
propagateTraceHeaderCorsUrls 白名单,后端 CORS allowedHeaders
显式放行 traceparent/tracestate/baggage——缺一边,断层10。
6. 生产化:采样、性能、告警与排障表
采样——生产别追踪一切。1000 QPS 全量导出,数据量大到全是噪声。
头部采样(请求开头按比率掷骰子)起步:ParentBasedSampler 包
TraceIdRatioBasedSampler(0.1)——父 span 已采样的强制跟随
(否则下游给已丢弃的上游续命,产生孤儿 trace),根 span 按 10% 来;
环境变量 OTEL_TRACES_SAMPLER=parentbased_traceidratio 同样生效。
进阶是尾部采样(Collector 看完全部 span 再定夺:错误和慢 trace
全留,正常流量低比率)——配置在 Collector,应用代码不用动11。
开销:默认的 BatchSpanProcessor 在内存里缓冲、攒批一起发送,中小流量 基本无感;真出现延迟,先查 Collector 是否可达(导出器阻塞是首因)11。 lgtm 一体化镜像是开发用的;生产把 Loki/Grafana/Tempo/Mimir 拆成独立服务,Collector 单独部署(sidecar 或独立服务)当缓冲12。
自定义 span:tracer.startActiveSpan('order.create', …) 给
「验证+多写+发邮件」这类跨多个调用的业务操作包一层——回调内的子 span
自动认它当父级,finally 里必须 span.end()。属性只放低基数
值(条目数、是/否标记);用户 ID、订单号这类高基数标识会撑爆存储,
还可能在可观测后台泄露隐私——「遥测数据的访问面往往比生产库还宽」13。
告警回到监控那一半:Grafana 统一告警三件套——alert rule
(查询+条件+定时评估)、contact point(邮件/Slack/PagerDuty)、
notification policy(按标签路由)。第一个该建的规则:p95 延迟
(IS ABOVE 0.5)或错误率——用户真能感到的指标;for 持续几分钟
再响,免得一次尖刺半夜把人叫醒14。
排障表(装完看不到 trace 的对号入座):没有任何 trace →
--import 缺失(最常见);生产有开发无 → dev 脚本漏了 --import;
只有 HTTP span 没有 Prisma → @prisma/instrumentation 没注册;
导出器连接错 → Docker 网内端点写错15。
7. 作者的判断与证据
- 「监控与可观测都要」+ 两者的「预知/未知」分界,是本章的纲领性判断1。
- 「RED 方法只记一个就够」是明确的取舍建议3。
- 「lgtm 一体镜像是开发用的,生产要拆开」是务实的边界声明12。
- 「遥测数据访问面比生产库宽,按日志的标准对待属性」是 2026 年的 隐私口径,值得全文背诵13。
8. 边界与局限
- 前端可观测只有最小示例(sdk-trace-web 一段), hydration、 bundle 分析等大话题明说「够单独一章」;
- 日志支柱只到概念(Node.js 侧的 OTel 日志支持「仍在成熟」, 本章聚焦 traces+metrics);
- tail-based sampling 只在 Collector 层面提及,无落地配置;
- 包名、镜像版本(otel-lgtm 0.28.0)、指标名随版本漂移,书中明说 「以 metric browser 为准」——本节为 2026 年中时点。
判断(我们的,不是书里的): 「故障注入看瀑布图」是全书后段的 教学高光之一:它把可观测从口号变成一次亲眼所见的定位—— 先给你看正常的形状(52ms,DB 占大头),再破坏它(800ms 的空白段), 认知落差自己完成了教学。这比十条「可观测的好处」都有效。 如果错,会错在: 单 API + 单库的拓扑下,追踪的价值被低估的 风险很小、被高估的风险存在——真正的追踪红利在微服务和异步队列里; 本书的单体演示是正确起点,但读者容易把「瀑布图」当成追踪的全部, 而忽略了它在跨服务断点上的真正杀手锏(上下文传播一节只是预告)。
9. 可带走的
- 监控=「在工作吗」(已知故障、阈值);可观测=「为什么不在」(未知故障、探索数据)。
- 三大支柱:日志(事件+traceId 接线)、指标(数字,RED=Rate/Errors/Duration)、追踪(span 树)。
- 相关性是乘法:日志里的 traceId → 点进追踪;慢 span → 跳同时段指标。
- OTel:插桩一次、后端随便换(OTLP);三层=应用/Collector/后端。
- auto-instrumentation 覆盖 Express/HTTP/pg/fetch;Prisma 要单独 @prisma/instrumentation。
- 军规:instrumentation 必须最先加载(--import / --require),dev 脚本也不例外。
- 健康检查的 span 要滤掉,别淹没信号。
- 采样:ParentBased 包 Ratio,10% 起步;错误/慢 trace 全留要 tail-based(Collector 层)。
- 自定义 span 给业务操作包层;属性低基数,遥测当日志一样做隐私审查。
- 第一个告警:p95 延迟 + for 持续几分钟;排障先查 --import。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 监控 vs 可观测 | Chapter 14: Monitoring and Observability | text/167-ch14-chapter-14-monitoring-and-observability.txt:67(搜「whether the system is working」) · text/167-ch14-chapter-14-monitoring-and-observability.txt:117(搜「50ms to 800ms」) |
| 日志与 traceId | The Three Pillars of Observability | text/167-ch14-chapter-14-monitoring-and-observability.txt:220(搜「does not happen automatically」) |
| RED 方法 | The Three Pillars of Observability | text/167-ch14-chapter-14-monitoring-and-observability.txt:310(搜「RED method」) |
| span 示例图 | The Three Pillars of Observability | text/167-ch14-chapter-14-monitoring-and-observability.txt:338(搜「4bf92f3577b34da6」) |
| OTel 厂商中立 | Introducing OpenTelemetry | text/167-ch14-chapter-14-monitoring-and-observability.txt:400(搜「If you later switched to Datadog, you ri」) |
| 三层架构与 auto-instrumentation | Introducing OpenTelemetry | text/167-ch14-chapter-14-monitoring-and-observability.txt:410(搜「three layers」) · text/167-ch14-chapter-14-monitoring-and-observability.txt:546(搜「do not need to add tracing code」) |
| Prisma 单独注册 | Introducing OpenTelemetry | text/167-ch14-chapter-14-monitoring-and-observability.txt:548(搜「not covered」) |
| instrumentation.ts 配置 | Adding OpenTelemetry to Our Project | text/167-ch14-chapter-14-monitoring-and-observability.txt:643(搜「NodeSDK」) · text/167-ch14-chapter-14-monitoring-and-observability.txt:715(搜「ignoreIncomingRequestHook: (req) => {」) |
| 加载顺序军规 | Adding OpenTelemetry to Our Project | text/167-ch14-chapter-14-monitoring-and-observability.txt:811(搜「before your application code」) · text/167-ch14-chapter-14-monitoring-and-observability.txt:825(搜「single most common」) |
| 故障注入瀑布图 | Exploring Traces in Grafana | text/167-ch14-chapter-14-monitoring-and-observability.txt:1267(搜「artificial delay」) · text/167-ch14-chapter-14-monitoring-and-observability.txt:1300(搜「800ms sits」) |
| 上下文传播与浏览器侧 | Exploring Traces in Grafana | text/167-ch14-chapter-14-monitoring-and-observability.txt:1324(搜「traceparent」) · text/167-ch14-chapter-14-monitoring-and-observability.txt:1363(搜「allowedHeaders: [」) |
| 头部采样与 ParentBased | Production actions | text/167-ch14-chapter-14-monitoring-and-observability.txt:1424(搜「head-based」) · text/167-ch14-chapter-14-monitoring-and-observability.txt:1448(搜「orphan traces」) |
| 尾部采样 | Production actions | text/167-ch14-chapter-14-monitoring-and-observability.txt:1482(搜「tail-based」) |
| lgtm 是开发用的 | Production actions | text/167-ch14-chapter-14-monitoring-and-observability.txt:1556(搜「separate service」) |
| 自定义 span 与低基数 | Production actions | text/167-ch14-chapter-14-monitoring-and-observability.txt:1730(搜「startActiveSpan」) · text/167-ch14-chapter-14-monitoring-and-observability.txt:1758(搜「high-cardinality」) |
| 告警三件套 | Production actions | text/167-ch14-chapter-14-monitoring-and-observability.txt:1844(搜「alert」) · text/167-ch14-chapter-14-monitoring-and-observability.txt:1918(搜「p95 latency」) |
| 排障表 | Production actions | text/167-ch14-chapter-14-monitoring-and-observability.txt:1944(搜「No traces in Tempo」) |
Footnotes
-
出处:「Chapter 14: Monitoring and Observability」第 59–171 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:67,搜「whether the system is working」;:117,搜「50ms to 800ms」;:119,搜「which specific database query」)。 ↩ ↩2 ↩3 ↩4 -
出处:同上第 183–286 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:220,搜「does not happen automatically」;:262,搜「reconstruct what happened」)。 ↩ -
出处:同上第 288–382 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:310,搜「RED method」;:338,搜「4bf92f3577b34da6」;:350,搜「185 ms」)。 ↩ ↩2 ↩3 ↩4 -
出处:同上第 384–458 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:400,搜「If you later switched to Datadog, you ri」;:404,搜「vendor-agnostic」;:410,搜「three layers」)。 ↩ ↩2 -
出处:同上第 460–558 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:524,搜「metapackage」;:548,搜「not covered」)。 ↩ -
出处:同上第 560–781 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:643,搜「NodeSDK」;:711,搜「ignoreIncomingRequestHook」;:731,搜「noise」;:767,搜「SIGTERM」)。 ↩ -
出处:同上第 783–968 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:811,搜「before your application code」;:825,搜「single most common」;:851,搜「--experimental-loader」)。 ↩ -
出处:同上第 970–1177 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:1052,搜「expose」;:1114,搜「did not change our route handlers」)。 ↩ -
出处:同上第 1179–1318 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:1267,搜「artificial delay」;:1298,搜「850ms」;:1304,搜「without adding any debug logging」)。 ↩ ↩2 ↩3 -
出处:同上第 1320–1400 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:1324,搜「traceparent」;:1360,搜「allowedHeaders」;:1394,搜「propagateTraceHeaderCorsUrls」)。 ↩ -
出处:同上第 1402–1552 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:1424,搜「head-based」;:1448,搜「orphan traces」;:1482,搜「tail-based」;:1548,搜「Collector is reachable」)。 ↩ ↩2 -
出处:同上第 1554–1606 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:1556,搜「separate service」;:1574,搜「development and testing」)。 ↩ ↩2 -
出处:同上第 1705–1790 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:1730,搜「startActiveSpan」;:1758,搜「high-cardinality」;:1768,搜「broader access」)。 ↩ ↩2 -
出处:同上第 1826–1928 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:1844,搜「alert」;:1898,搜「IS ABOVE 0.5」;:1912,搜「brief spike」)。 ↩ -
出处:同上第 1930–2000 段(
text/167-ch14-chapter-14-monitoring-and-observability.txt:1944,搜「No traces in Tempo」)。 ↩