核心问题

系统上线后,我们怎么知道它发生了什么?

测试回答的是:

在已知场景里,系统是否符合承诺?

可观察性回答的是:

在真实世界里,当系统出现未知问题时,我们能不能理解它?

生产环境永远比测试环境复杂:

  • 用户行为更多样
  • 数据状态更脏
  • 网络更不稳定
  • 第三方服务会失败
  • 并发和时序问题会出现
  • 需求边界会被真实使用撞出来

所以系统不能只在测试里“正确”,还必须在运行时“可被理解”。

可观察性不是多打日志

很多人把 observability 理解成:

出问题了,多加点 log。

这太晚了。

真正的可观察性是:

系统在设计时就留下足够证据,让未来的你能回答问题。

例如课程平台里,用户反馈:

我付了钱,但看不了课程。

没有可观察性的系统只能猜:

  • 支付成功了吗?
  • purchase 创建了吗?
  • access grant 创建了吗?
  • grant 被撤销了吗?
  • 账号被封禁了吗?
  • 课程下架了吗?
  • 缓存是不是没刷新?

有可观察性的系统应该能沿着一条证据链查清楚:

payment.succeeded
  -> purchase.created
  -> course_access_grant.created
  -> canAccessCourse=false
  -> reason: account_suspended

三根支柱:Logs, Metrics, Traces

传统上,可观察性有三根支柱:

  • Logs:发生了什么
  • Metrics:整体表现如何
  • Traces:一次请求经过了哪里

它们不是互相替代,而是回答不同问题。

Logs:记录关键事实

日志适合记录具体事件和上下文。

例如:

logger.info("course_access_grant_created", {
  accountId,
  courseId,
  source: "purchase",
  purchaseId,
  requestId,
})

好日志不是自然语言作文,而是结构化事实。

坏日志

User access failed

问题是它没有告诉我们:

  • 哪个用户?
  • 哪门课?
  • 为什么失败?
  • 发生在哪个请求里?
  • 和哪个 payment 或 purchase 有关?

好日志

logger.warn("course_access_denied", {
  accountId,
  courseId,
  reason: "account_suspended",
  requestId,
})

日志名应该像事件名,而不是随手写一句话。

Metrics:观察系统健康

指标适合回答整体趋势。

例如:

payment_success_count
purchase_created_count
course_access_grant_created_count
course_access_denied_count
refund_count
outbox_event_lag_seconds

指标能发现“局部日志看不出来”的问题。

例如:

payment_success_count 正常
purchase_created_count 正常
course_access_grant_created_count 突然下降

这说明支付和购买流程还在跑,但访问权发放出了问题。

好的指标应该围绕业务承诺,而不只是机器状态。

机器指标重要:

  • CPU
  • memory
  • latency
  • error rate

但业务指标也重要:

  • 支付成功后访问权发放成功率
  • 退款后访问权撤销成功率
  • 企业课程分配成功率
  • 课程播放失败率
  • 登录失败率

Traces:理解一次请求的路径

Trace 适合回答:

这一次请求到底经过了哪些步骤,慢在哪里,失败在哪里?

例如购买流程:

POST /checkout
  -> create_payment
  -> payment_provider.confirm
  -> handle_payment_succeeded
  -> purchase.create
  -> course_access_grant.create
  -> outbox_event.create

如果用户说“支付成功但不能看课”,trace 可以告诉我们:

  • 请求有没有到达服务
  • 哪一步耗时最长
  • 哪一步失败
  • 是否写入数据库
  • 是否创建 outbox event

Trace 的关键是 correlation id:

requestId
paymentId
purchaseId
accountId
courseId

没有关联 ID,日志、指标和 trace 就很难串起来。

可观察性要围绕问题设计

不要先问:

我们要接哪个监控工具?

先问:

生产出问题时,我们最需要回答哪些问题?

课程平台至少要能回答:

  1. 用户为什么不能访问课程?
  2. 支付成功后访问权是否发放?
  3. 退款后访问权是否撤销?
  4. 企业管理员为什么分配失败?
  5. 邮件为什么没有发送?
  6. 推荐系统延迟多久?
  7. 哪些课程播放失败最多?
  8. 哪些接口错误率突然升高?

每个问题都应该对应证据:

问题证据
为什么不能访问课程access decision log + reason
支付后是否发放访问权payment/purchase/grant 指标和日志
退款后是否撤销访问权refund/revoke event
邮件为什么没发送outbox event + worker result
哪些接口变慢latency metrics + traces

记录决策原因,而不只是结果

权限和访问判断尤其要记录原因。

坏做法:

return false

更好的做法:

return {
  allowed: false,
  reason: "account_suspended",
}

然后在边界处记录:

logger.warn("course_access_denied", {
  accountId,
  courseId,
  reason: decision.reason,
  requestId,
})

这会极大提高排查效率。

当然,不是所有内部函数都要返回复杂对象。关键是:在重要业务边界上,系统要能解释自己的决定。

Alert:告警不是噪音机器

告警的目标不是“任何异常都通知人”,而是:

当系统承诺正在被破坏时,通知能采取行动的人。

坏告警:

CPU > 80%

这不一定是坏事,也不一定需要人处理。

更好的告警:

payment_success_count 正常,但 course_access_grant_created_count 低于预期

这说明用户可能付了钱但拿不到访问权,是业务承诺被破坏。

好的告警应该满足:

  1. 明确影响什么承诺。
  2. 有明确负责人。
  3. 有可执行的排查入口。
  4. 噪音足够低。

如果告警经常没人理,它就已经失效了。

可观察性和隐私

可观察性不是把所有数据都打进日志。

不要记录:

  • 密码
  • token
  • 完整支付卡信息
  • 隐私文本
  • 不必要的个人身份信息

可以记录:

  • 内部 ID
  • 脱敏后的邮箱或手机号
  • 枚举化 reason
  • 业务对象 ID
  • 请求 ID

原则:

记录足够排查问题的证据,不记录不必要的私人信息。

可观察性也是产品能力

可观察性不只是给工程师看的。

后台系统里,很多“客服查询页”“订单详情页”“访问权历史页”,其实都是面向运营和客服的可观察性。

例如客服需要回答:

用户为什么不能看课?

一个好的内部页面应该展示:

账号状态:suspended
购买记录:paid
退款记录:none
访问权:created, currently inactive
拒绝原因:account_suspended

这比让客服找工程师查数据库成熟得多。

小结

  1. 测试验证已知场景,可观察性帮助理解未知问题。
  2. 可观察性不是多打日志,而是设计证据链。
  3. Logs 记录关键事实,Metrics 观察整体趋势,Traces 理解单次请求路径。
  4. 好日志应该结构化,像事件名,而不是自然语言碎片。
  5. 指标应该围绕业务承诺,不只是机器状态。
  6. 权限和访问判断要记录原因。
  7. 告警应该在系统承诺被破坏时触发。
  8. 可观察性必须尊重隐私和数据最小化。
  9. 内部查询页也是一种产品化的可观察性。