可观测性与监控:面向工程团队的完整指南

每个工程团队都在监控生产环境。但很少有团队能在不经历漫长排查的情况下回答「系统为什么会这样」。监控与可观测性的差别不是选工具,而是一种设计取向,它决定了团队理解并应对从未预料过的故障模式有多快。

传统监控假定你知道该盯着什么。你设定 CPU 阈值、检查状态码,在磁盘超过 90% 时叫醒某个人。对已知故障,这套做法管用。但现代分布式系统的崩坏方式,往往是任何看板都没预料到的:某个服务里一次细微的延迟劣化,把队列堵成积压,最终表现为数据库连接池耗尽,而且只在流量高峰才浮出水面。若无法对系统内部状态提出任意问题,你就是在盲飞。

为什么可观测性不止是基础监控

可观测性是系统的一种属性:允许你从外部输出推断其内部状态。监控告诉你出了问题;可观测性让你查清到底哪里出了问题——哪怕你从未预见过那种具体的故障。这个区分很关键:监控由告警驱动、被看板束缚;可观测性由追问驱动、以数据为底。

当你的系统在日志、指标与追踪中输出高基数的结构化数据时,你就能关联事件、下钻到具体用户或请求,并找出那些任何阈值告警都抓不到的事故根因。投入可观测性的团队,平均修复时间往往能降低一个数量级,因为他们把时间从猜测转到了依据证据行动。

「监控告诉你系统是否在工作。可观测性让你追问它为什么不工作。后者是运行那些你并未完全理解的系统的前提——而所有生产系统都是如此。」

三大支柱:日志、指标与追踪

可观测性的生态建立在三类互补的数据之上,各司其职。把它们当作一个统一的信号,而不是彼此割裂的孤岛,正是有效排查的关键。

日志:事件的不可变记录

日志是带时间戳的离散事件记录。它是最细粒度的信号,精确捕捉某一时刻发生了什么。一条结构良好的日志携带足够的上下文——请求标识、服务名、耗时、错误详情——让你无需跨多个来源比对,就能重建执行路径。

团队在日志上最常犯的错,是把它当成无结构文本。只有三台服务器时,在纯文本文件里搜索还行得通;一旦到了规模,无结构日志就是噪音。每一行都必须可解析、带有结构化元数据,并在你架构中的每个服务里遵循一致的模式。

指标:随时间聚合的度量

指标是按时间间隔测得的系统状态的数值表示,为高效存储与快速聚合而设计。它回答「每秒多少请求」「p99 延迟是多少」这类问题。与日志不同,指标丢弃了单个请求的细节——它用粒度换取压缩与速度。

常见的指标类型——计数器、量表、直方图与摘要——各有用武之地。计数器跟踪累计值,比如请求总数。量表记录时点值,比如内存占用。直方图把观测值分到可配置的桶里,适合刻画延迟分布。选对类型,才不会产生误导性的聚合与浪费掉的基数。

追踪:请求端到端的生命周期

分布式追踪跟随一个请求穿越服务边界。每条追踪由若干跨度组成——带起止时间戳的命名操作,记录某个服务或函数所做的工作。在微服务架构里,追踪是唯一能重建请求完整生命周期的信号。

没有追踪,一次缓慢的页面加载可以归咎于参与其中的几十个服务中的任何一个。有了追踪,你能定位到瓶颈是用户服务里那条耗时 800 毫秒的数据库查询,而其余每个跨度都在 50 毫秒内完成。这样的精度,单靠日志或指标做不到。

「日志告诉你发生了什么。指标告诉你发生了多少次。追踪告诉你这些如何串在一起。要在生产事故中不靠猜测地前进,这三样你都需要。」

OpenTelemetry 的搭建与埋点

OpenTelemetry 是生成、采集与导出遥测数据的行业标准。它跨语言提供一套统一的接口与工具包,以与厂商无关的格式输出日志、指标和追踪。采用它可以消除厂商锁定,并确保无论你用商业平台还是自建管道,埋点都照常工作。

自动埋点与手动埋点

大多数工具包支持自动埋点——对流行框架和库的零代码挂钩。一次初始化调用就能覆盖 HTTP 服务器、数据库客户端、消息队列与 gRPC 调用。自动埋点覆盖常见路径,但业务关键的代码路径、自研中间件和领域专有操作,仍需手动埋点。

import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
import { PgInstrumentation } from "@opentelemetry/instrumentation-pg";
import { Resource } from "@opentelemetry/resources";
import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions";

const sdk = new NodeSDK({
  resource: new Resource({
    [SEMRESATTRS_SERVICE_NAME]: "payment-service",
  }),
  traceExporter: new OTLPTraceExporter({
    url: "http://otel-collector:4317",
  }),
  metricExporter: new OTLPMetricExporter({
    url: "http://otel-collector:4317",
  }),
  instrumentations: [
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
    new PgInstrumentation(),
  ],
});

sdk.start();
process.on("SIGTERM", () => sdk.shutdown());

OpenTelemetry 采集器的架构

OpenTelemetry 采集器是一个与厂商无关的中转层,负责接收、处理并导出遥测数据。它是任何生产级可观测性管道的骨干。每台主机部署一个采集器,或在 Kubernetes 中以集群形式部署,可以把埋点与后端选型解耦,让你在数据抵达平台之前先做批处理、过滤、采样与转换。

采集器可以配置处理器来做尾部采样:为高延迟或出错的请求保留完整追踪,同时丢弃绝大部分健康流量。这能大幅降低存储成本,又不牺牲对那些最要紧请求的排查能力。

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
    send_batch_size: 1024
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
  filter:
    error_mode: ignore
    traces:
      span:
        - 'attributes["http.target"] == "/healthz"'
        - 'attributes["http.target"] == "/metrics"'
  attributes:
    actions:
      - key: environment
        value: production
        action: upsert

exporters:
  otlp:
    endpoint: "collector.example.com:443"

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, filter, batch, attributes]
      exporters: [otlp]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch, attributes]
      exporters: [otlp]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, batch, attributes]
      exporters: [otlp]

结构化日志的写法

结构化日志指的是以机器可解析的格式(通常是 JSON)输出日志条目,并在各服务间使用一致的字段名。它把日志从「搜索问题」变成「查询问题」。当所有团队对请求标识、服务名、错误码和耗时都采用同一套字段约定时,你就能在整个基础设施上随手发起查询,而不必编写定制解析器。

日志的模式设计

每条结构化日志事件都应包含一组最低限度的属性:时间戳、严重级别、服务名、追踪标识、跨度标识与消息。在此之上可以加入领域字段,但要守住命名约定。例如始终使用同一种写法,给用户相关字段加统一前缀,并把错误详情放进嵌套对象,而不是拼接进消息字符串。

  • 始终包含 trace_id 与 span_id,以便把日志与追踪关联起来
  • 使用与可行动信号相对应的日志级别(debug、info、warn、error),而不是迁就写代码时的方便
  • 绝不要记录敏感数据——个人信息、密钥或令牌——即使在开发环境也不行
  • 让日志消息保持固定,把可变数据放进结构化字段,这样才能聚合
  • 统一所有服务的时间戳格式,采用 RFC 3339 或 Unix 毫秒

常见的坑

最昂贵的日志错误是记得太多。用户标识或网络地址这类高基数字段,能把日志量撑大好几个数量级。对量大的调试日志使用采样,把详尽记录留给特定追踪标识或错误条件。第二个错误是字段命名不一致——一个服务用 user_id,另一个用 customerId,第三个用 user.id,没有转换层就无法跨服务查询。

「一条查不了的日志,跟不存在没有区别。结构化日志不是为了好读,而是为了让每一条日志都成为你可观测性平台里的一等公民。」

微服务中的分布式追踪

分布式追踪是排查微服务架构最有效的单一工具。一条横跨二十个服务的追踪,能准确告诉你时间花在哪里、哪些请求失败、错误如何传播。没有追踪,排查缓慢的结账流程就意味着翻十几个服务的日志,然后猜测因果关系。

追踪上下文的传递

追踪上下文必须在每一处服务边界传递下去——HTTP 头、消息队列元数据、gRPC 元数据,甚至跨越定时任务或后台工作者这类异步边界。只要你使用它针对 HTTP、gRPC 和消息框架的埋点库,OpenTelemetry 会自动处理这件事。追踪标识从入口网关一路流向每个下游服务,沿途收集跨度。

  • 为所有入口埋点:API 网关、负载均衡器、入口控制器
  • 通过消息头或消息元数据,在消息队列间传递追踪上下文
  • 在日志输出中带上追踪标识,以便日志与追踪相互对照
  • 采用能保留错误或高延迟追踪的采样策略
  • 为跨度添加自定义属性以承载业务上下文——客户等级、商品编号、地域

读懂一条追踪

标注良好的追踪会揭示请求的关键路径。盯住耗时最长的那个跨度——那就是你的瓶颈。寻找带有错误事件或属性特别庞杂的跨度。把成功与失败请求的追踪对照起来,找出规律。如果追踪反复显示某个下游调用在超时,那你面对的是依赖问题,而不是应用缺陷。

告警:该对什么告警,以及如何避免告警疲劳

告警疲劳是事故响应的头号威胁。当一个团队一个班次收到五十条告警时,每一条都会被无视。健康的告警策略,目标不是发现每一处异常,而是产生一小批高信号、需要人来判断的通知。其余的一切,都该留给看板或日志查询。

告警的分级体系

把告警分成三级。一级告警立刻呼叫值班工程师,因为它意味着用户已经受影响——错误率飙高、服务完全不可用,或错误预算的消耗速度超过阈值。二级告警建一张工单,留到下一个工作日分诊——延迟升高、组件劣化但尚未失效。三级告警只是知会——证书快到期、存储空间接近上限。

  • 只对症状告警,不对原因告警。用户看到的是 5xx 错误——就对它告警,而不是对 CPU 使用率
  • 使用多条件告警,要求偏离持续一段时间后才触发(比如超过阈值五分钟)
  • 每人每班次最多三条呼叫级告警,以维持信号质量
  • 每季度复查并裁剪告警规则——过期的告警会侵蚀大家对系统的信任
  • 在每条告警里附上处置手册的链接,好让响应的人知道头三步该做什么

按消耗速率告警

消耗速率告警在错误预算被消耗得比预期更快时触发。与静态阈值告警不同,它与你的服务目标直接挂钩。若目标是 30 天内 99.9% 的可用性,你大约有 43 分钟的停机额度。当预计的消耗速度会让窗口提前耗尽时,告警就会触发,为你留出在目标被突破前反应的时间。

做出真正有用的看板

大多数工程看板都是无人问津的图表坟场。一个看板有用,是因为它回答了一个具体问题,而且不需要额外解读。最好的看板只服务一种角色、一个用途:值班看板用于事故分诊,周度回顾看板用于容量规划,团队看板用于跟踪服务目标的达成。

值班看板

值班看板应当一屏放下,并回答四个问题:服务还活着吗?错误率多少?延迟分布如何?错误预算是否已被击穿?每张图都要有清晰的阈值线,好让人一眼看出当前值是否健康。一个值班看板上不要超过六张图——事故当中,认知负担很要紧。

  • 从 RED 指标开始:速率(每秒请求数)、错误(失败请求)、耗时(延迟分位数)
  • 为基础设施补上 USE 指标:每种资源的利用率、饱和度与错误
  • 把服务目标的达成情况和预算消耗速率作为醒目的状态指示
  • 把每张图都链到它背后的日志或追踪查询,一键下钻
  • 延迟图用对数刻度——线性刻度会掩盖尾部那些重要的波动

常见的反面做法

最常见的反面做法,是把基础设施产出的每个指标都摆上看板。这类「一片绿墙」制造虚假的安全感,也让人在事故中根本找不到信号。其他反面做法还包括:用饼图呈现时间序列、把多个指标堆在不一致的坐标轴上、阈值线不加标注。如果一张图需要一句注释才能看懂,它就不该出现在看板上。

SLI、SLO 与错误预算

服务水平指标、目标与错误预算,构成了你的团队与用户之间的契约。指标是原始度量——请求延迟、错误率、吞吐量。目标是你承诺兑现的水准——99.9% 的请求在 200 毫秒内完成。错误预算是可容许的失败余量——那 0.1% 的空间,让团队可以发布、试验和迭代,而不必担心违背承诺。

选择有意义的指标

好的指标面向用户、可度量、可据以行动。可用性(成功请求的比例)、延迟(低于某阈值的请求比例)与新鲜度(所返回数据的年龄),是 Web 服务最常见的指标。关键在于从用户视角度量——一个在你服务器上 50 毫秒完成、却在缓慢移动网络上要花两秒的请求,从用户角度就是失败。

务实地设定目标

99.999% 的目标听上去很唬人,代价却极其高昂。每多一个九,大约意味着在冗余、测试与运维工具上多投入十倍。多数服务从 99.9% 起步即可,把更高的目标留给面向客户的关键路径。对能力要诚实——一个长期达不到的目标比没有目标更糟,因为它让失败变得理所当然,也磨掉了工程师对指标的信任。

错误预算如何带来速度

错误预算把可靠性从约束变成可度量的风险。预算充裕时,团队可以放手发布,知道自己还有余地。预算耗尽时,团队暂停非关键发布,专心做可靠性工作。这就形成了一条清晰、以数据为依据的决策路径,取代了关于某次发布是否「足够安全」的主观争论。

无服务器与边缘环境中的可观测性

无服务器与边缘计算带来了独有的难题。函数转瞬即逝,基础设施被抽象掉,执行环境分散在全球各个接入点。基于代理的传统监控失效了,因为根本没有可供代理驻留的主机。这类环境需要另一套办法:由代码主动推送遥测、对冷启动做细粒度追踪,以及在全球部署中谨慎管理基数。

无服务器特有的做法

冷启动是无服务器函数中主导延迟的因素。把函数初始化与请求处理分开埋点,才能把冷启动延迟同业务逻辑延迟区分开来。用结构化日志捕捉调用上下文——请求标识、地域、函数版本、执行环境——并以异步方式输出调用次数、耗时与错误率的自定义指标,以免阻塞响应路径。

  • 把冷启动耗时作为自定义指标来量——在云厂商的标准指标里看不到它
  • 选用与你所在平台扩展机制兼容的 OpenTelemetry 上下文传播器
  • 以批量方式导出遥测,避免给函数执行时间再添延迟
  • 在缺少自定义指标接口的环境中,改用从日志派生的指标
  • 给所有遥测打上部署环境、地域与函数版本的标签,以便有效筛选

边缘计算的考量

边缘平台会在全球数十个地点执行代码。这种地理分散使传统的集中采集变得不切实际。请使用面向边缘设计的可观测性工具,在每个接入点就地采集遥测,再以很小的开销汇总到中心。要按地域监控错误率——某地运营商的故障可能让你的服务在一个区域劣化,而全球平均值看上去依旧健康。

培育可观测性文化

工具与管道是必要的,却不充分。可观测性最难的部分在文化。你的团队必须像重视有测试的代码那样,重视有埋点的代码。每个新功能的「完成定义」里都应包含可观测性埋点,正如它包含单元测试和代码评审一样。这需要基础设施支撑:共享库、写下来的约定,以及每个团队里一位推动此事的人。

让可观测性成为头等事

先写一份遥测设计文档,为每个服务定义标准字段、指标命名约定与追踪要求,并把它纳入新服务上线的流程。在持续集成中设置自动检查,强制最低限度的埋点——例如,拒绝那些新增了 HTTP 处理器却没有相应追踪埋点或结构化日志的合并请求。

  • 把可观测性埋点写进每个功能的完成定义
  • 定期举办演练日,让团队只用看板和追踪查询来排查问题
  • 庆祝可观测性带来的胜利——分享那些体现良好遥测如何促成快速定位的事故复盘
  • 设立轮值负责人,每个迭代跨团队检查遥测质量
  • 投资共享的埋点库,让做对的事比做错的事更省力

反馈回路

可观测性不是一次性的落地,而是持续的反馈回路:埋点、发布、观察、学习、改进。当某次事故暴露出盲点——某个没被跟踪的指标、某个缺失的日志字段、某条被丢弃的追踪——就把它当作你可观测性平台的缺陷来修。久而久之,遥测会越来越完整,排查会越来越快,团队也会获得那份来自「知道自己能理解任何故障」的底气,哪怕是从未见过的故障。

「可观测性的目标不是杜绝事故。它的目标是确保事故发生时,团队拥有数据、工具和底气,在用户察觉之前把它解决掉。」

采纳可观测性是一段旅程,而不是一次采购。从小处入手——把一个关键服务端到端埋好点,做一个真正有用的看板,写一条有意义的服务目标。让价值自己说话。一旦团队在事故中体会过「猜」和「知道」的差别,就再也不会想回头了。

See what your own repository can account for.

Thirty minutes on a repository you choose, including the part the record cannot attribute.