可观测性的本质是证据链
大多数团队在讨论可观测性时,第一反应是选什么技术栈——ELK、Prometheus、Grafana、Jaeger。这其实搞错了重点。可观测性本质上不是技术选型问题,而是证据链构造问题。 你真正需要回答的只有一个问题:当一个请求经过多个系统后出错了,你能不能沿着一条完整的链路回溯到错误发生的准确位置,并说清楚那一刻发生了什么。
上线之后,技术债才真正开始积累
系统在雏形阶段没有历史负担。没有脏数据,没有复杂的依赖关系,没有必须向后兼容的接口。你可以自由地选择技术方案,架构的改动成本极低。
一旦上线,第一个约束就出现了:业务不可中断。 每一次变更都必须是向后兼容的。每一次故障都是对用户信任的消耗。你未必是从 0 到 1 的那个人,更多时候你接手的是一个已有系统——对你而言,它是一个黑盒子。历史的权衡藏在代码的缝隙里,没有文档,没有标注,只有你上线后踩到坑才能发现。
系统上线不是终点。它意味着你的每一步操作都带着镣铐。
上游的数据不可信
你收到的上游数据可能是不完整的。字段缺失、格式异常、业务逻辑不符合你的预期。这类问题在联调阶段不易察觉,但一旦流量涌入,脏数据就会像石子一样卡在系统的各个缝隙里。
一个简单的防御手段是要求每条数据携带来源标识。比如每次调用附上 source 字段,标明数据来自哪个系统。当问题出现时,你不需要猜测,直接定位到上游。快速定位的能力,本身就是可观测性的一部分。
下游的兼容性陷阱
作为服务提供方,你的接口会被以你完全没想到的方式使用。
一个典型的例子:接口返回错误信息 "设备不存在",下游调用方可能已经依赖这个字符串做判断逻辑。某天你出于规范化的考虑把中文信息改成了英文,然后下游的逻辑就崩了。你甚至不知道是谁在依赖这个字段。
变更前你永远不清楚谁在用你的接口。一个可行的方案是为每个下游系统分配独立的 AccessKey,要求每次调用携带。当接口需要变更时,你可以明确通知到所有已知调用方,而不是上线之后等告警。
你真的需要 ELK 吗
ELK(Elasticsearch、Logstash、Kibana)加上 Kafka,是业界标配的可观测性方案。它能解决的问题很多,但引入的复杂性也同样可观:
- 海量数据的存储和索引成本
- 组件自身的稳定性维护
- 数据一致性和延迟
- 团队学习这一整套技术栈的时间投入
资源的分配总是与业务增长挂钩。如果你在一个资源有限的团队,推动一整套 ELK 体系的部署可能比你真正想解决的问题更耗时。等你终于搭好了这套体系,用户的耐心可能已经耗尽了。
重新理解可观测性
抛开技术名词,回到本质:可观测性是构造证据链的能力。
类比警察办案——你不能靠某个人的主观说辞下结论,你需要不断基于假设去验证,直到形成完整的人证、物证、时间线。关键问题是:某个组件在某个时间点,输入了什么,输出了什么。
一个简易的事件模型足以描述这一切:
1 | { |
actor 表明是谁,timestamp 是时间,action 是行为,input 和 output 构成 I/O,trace_id 将多个组件的行为串联成一条线。这就是可观测性的最小可行模型。 你不需要 ELK 也能做到这一点。
日志的真实成本
加一行日志的代价比你想象的大得多。
磁盘。 一个日活十万的系统,每秒上百条请求,每条请求打一条日志,一天就是上千万条。一条日志按 200 字节算,一天 2GB,一年 700GB。这还只是一个系统。磁盘满了报警,你半夜起来处理——这是运维成本。
CPU。 序列化对象、字符串拼接、IO 阻塞业务线程、大量临时字符串加重 GC 压力。logger.info() 不是免费的。
网络。 日志发送到远程服务器意味着带宽占用、序列化和反序列化的双重开销、传输延迟。
综合下来,日志的成本表大约是:
| 成本类型 | 说明 |
|---|---|
| 存储成本 | 磁盘空间、备份、归档 |
| 性能成本 | CPU 计算、IO 阻塞、GC 压力 |
| 网络成本 | 带宽、延迟 |
| 运维成本 | 清理、扩容、报警 |
| 排查成本 | 海量日志中找到有效信息的时间 |
日志记录的应当是事实源,而非决策源。 你能指望日志告诉你某个订单的金额是 100 万、时间是凌晨 3 点、调用 IP 是 xxx,但你不能指望日志替你判断”这是一个异常订单”。决策是人做的,日志只负责提供证据。
加日志之前问自己三件事:这个日志以后会看吗?它能帮我定位问题吗?它的价值值得我付出这些成本吗?
几条具体的权衡策略:
- 生产环境以 ERROR 和 WARN 为主,业务关键节点记录 INFO
- 核心交易链路用同步日志保证可靠性,一般业务用异步日志降低开销
- 高并发场景做日志采样,比如 1/100 的比例
- 按大小或时间做日志轮转,定期清理过期数据
- 避免记录大对象,只保留必要字段
- 敏感信息必须脱敏
形成一条证据链
单独的日志是孤立的点。你需要让它们串联成一条线。
当一个请求经过 A 系统、B 系统、C 系统后出错,你需要能沿着一个全局唯一的标识——trace_id——把三个系统里的日志全部检索出来。实现方式上,Java 通常用 MDC(Mapped Diagnostic Context),基于 ThreadLocal 做线程隔离,在整个请求生命周期内携带同一个 trace_id。
关于思考方法
排查问题最大的陷阱是确认偏误——你带着一个预设的结论去找证据,然后只看见支持它的部分,忽略不支持的。
正确的方法是先收集事实,再绘制证据链地图,逐步扩散,直到证据链闭环时才出结论。不要太快下结论。
发散
业界通常将可观测性分为三大支柱:Logging(离散的事件记录)、Metrics(聚合的数值统计)、Tracing(请求的分布式路径)。三者组合基本覆盖了大多数场景。但更重要的是理解它们的共同底层——都在构造某种形式的证据。
还有一些延伸方向:错误处理的分层设计(网络层超时、应用层校验、数据层约束各归各)、降级与熔断策略、基于日志做容量规划。但这些的前提,仍然是先把证据链这一层打扎实。