你的系统真的可靠吗?

你有没有想过一个问题:系统上线之后,才是真正难点的开始

在系统还只是一个雏形的时候,没有历史技术债,没有脏数据,没有复杂的依赖关系,我们可以尽情选择自己喜欢的技术,有着极大的自由度。

但是系统一旦上线,就迎来了第一个约束——业务不可中断

我们一切的改动,必须是向后兼容的。每一次故障,都是对用户信任的一次消耗。你的信任钱包里,还有几张钞票呢?

"$1000 中,改掉一个 bug 的时间只值 $1,但知道问题在哪,值 $999。"

也许你是在项目已经完成从 0-1 的时候,加入团队去完成从 1-10、从 10-100 的过程。对于历史的情况,我们是难以完全了解的——有哪些坑?历史的权衡有哪些?哪些地方没有收口?

系统对于我们来说,就是一个黑盒子。

我们要想对它进行升级,首先就需要想办法去了解它。


上游的数据可能是脏的,怎么办?

到系统观察,有个很重要的问题你可能没想过:上游传给你的数据,可能是"脏"的

什么是脏数据呢?比如:

  • 字段缺失
  • 格式不对
  • 业务逻辑不符合你的预期

那怎么应对呢?

我见过一个挺好的设计是这样的:

JSON
复制代码
{ "from": "order", "input": {} }

每个数据都带着它的来源。这样当系统出问题的时候,你就能快速定位:哦,原来是 order 服务传的数据有问题。不是我的问题,是上游的问题! 快速甩锅(呃不对,是快速定位问题)的能力,就是可观测性的一部分。


下游访问频率很高,你敢随便改接口吗?

说完上游,再说说下游。

作为服务提供方,你是不是经常遇到这种情况:你的接口响应是这样的

json
复制代码
{ "code": "5000", "msg": "设备不存在" }

然后某天架构师说:"为了解决中文终端乱码问题,我们统一把 msg 改成英文吧。"

你啪一下就改了,然后上线... 咦?怎么有些下游调用方出问题了?

原来人家是这么用的:

javascript
复制代码
if (msg === "设备不存在") { // 做一些特殊的错误处理 }

啊这... msg 改了,逻辑就崩了。

所以作为服务提供方,我们一定要保证接口向后兼容,不能随意变更响应格式。

有时候你甚至不知道有谁在访问你,这就更头疼了。所以我在想:下游的访问频率高,实际上是在提醒我们——变更要谨慎,兼容性要放在第一位。

可以考虑给每个系统颁发一个 AccessKey,每次访问必须携带。这样当系统需要升级的时候,可以将改动点同步给下游,确认无误再升级,也就不粘锅了


你真的需要 ELK 吗?

说到可观测性,或许我们能够立刻想到 ELK(ElasticSearch、Logstash、Kibana、Filebeat),或者再加上 Kafka。

是的没错,这些技术能够让我们构建出一个好的可观测性体系。

但是,这些技术栈带来便利的同时,也引入了新的复杂性:

  • 海量数据的存储
  • 组件的稳定性
  • 数据一致性和延迟
  • 学习这套技术栈所投入的成本

也许当你成功搭建出这一套体系去解决问题的时候,用户已经流失了。

又或者,你只是说话权重不高的一名工程师,团队不会为你协调这些资源。资源是与业务增长挂钩的。

所以今天想聊聊:如何打造一个极简的可观测性体系?


系统架构设计,是克制的艺术

抛开华丽的技术名词之后,我们需要重新思考:什么才是真正的可观测性?

可以想象警察办案。办案不能靠某个人的主观说辞,而是要不断基于假设去验证,然后构建一段完整的证据链。

比如常见的:"你好,请问事发当晚9点你在做什么?谁来证明?人证、物证?"

所以关键是:某个组件在某个时间点,做了什么?结果怎么样?

这就是一个简易的事件模型:

JSON
复制代码
{ "actor": "order", // 谁 "timestamp": 1776420525400, // 什么时候 "action": "create-order" // 做了什么 }

换到系统里,我们描述一个组件做的事情,其实就是一个 I/O 的过程:

JSON
复制代码
{ "actor": "order", "timestamp": 1776420525400, "action": "create-order", "input": {}, // 外部提供了什么 "output": {} // 组件交付了什么 }

最后,为了描述一件被多个组件协同完成的事情,我们补充上链路 ID,对任务进行关联:

JSON
复制代码
{ "actor": "order", "timestamp": 1776420525400, "action": "create-order", "input": {}, "output": {}, "trace_id": "uuid" // 链路ID }

记录日志这件事,到底该怎么考虑?

好,现在说回到日志。

我之前一直在思考:加日志的代价是什么?

磁盘压力

日志文件增长起来是真的恐怖。

你想啊,一个日活 10 万的系统,每个请求打一条日志,一秒钟就是 100+ 条日志。一天下来就是上千万条。

一个月呢?一年呢?

  • 存储成本:假设一条日志 200 字节,1000 万条日志就是 2GB。一年下来就是 700+G。这还只是一个系统。
  • IO 压力:频繁写入会导致磁盘 IO 升高,尤其是机械硬盘。
  • SSD 寿命:企业级 SSD 贵,消费级 SSD 有写入寿命限制。

"你以为加日志不花钱?存储是要钱的,运维是要钱的,到时候磁盘满了报警,你得半夜起来处理。"

CPU 开销

很多人觉得日志就是"写个字符串",能有多少开销?

还真不少:

  • 序列化开销:把对象序列化成 JSON 或者其他格式,CPU 得算吧?
  • 字符串拼接:日志框架的 {} 占位符,每次拼接都会创建新字符串。
  • IO 阻塞:同步日志写入会阻塞业务线程,影响接口响应时间。
  • GC 压力:大量字符串对象会加重垃圾回收的负担。

💡 "你以为 logger.info('xxx') 是免费的?每一行日志,都是有成本的。"

网络开销

如果你的日志还要发送到远程服务器(比如 Kafka、Elasticsearch),那就还有网络开销:

  • 带宽占用:海量日志会占用网络带宽
  • 延迟增加:日志传输会引入额外的延迟
  • 序列化/反序列化:数据在网络上传输前需要序列化,接收端需要反序列化

成本考量

综合来看,日志的成本包括:

成本类型说明
存储成本磁盘空间、备份、归档
运维成本日志清理、磁盘扩容、报警处理
性能成本CPU 计算、IO 阻塞、GC 压力
网络成本带宽占用、传输延迟
人员成本日志分析和问题排查时间

所以日志不是你想加就能加的,得有个策略


那应该记什么呢?

我的想法是:日志记录的应当是事实源,而非决策源。

什么意思呢?

事实源(应该记)决策源(不应该记)
用户登录时间用户是好人
订单金额订单金额过高需审核
API 调用参数参数校验失败原因判断

说白了,决策是人做的,日志只提供证据链

你不能指望日志告诉你"这是一个异常订单",你只能指望日志告诉你"这个订单的金额是 100 万,时间是凌晨 3 点,调用 IP 是 xxx"。


日志的权衡策略

说了这么多代价,那应该怎么权衡呢?

1. 根据环境调整日志级别

  • 生产环境:ERROR 和 WARN 为主,业务关键节点记录 INFO
  • 测试环境:DEBUG 随便打,方便排查问题
  • 预发环境:根据问题需要,动态调整

2. 异步日志 vs 同步日志

  • 同步日志:可靠性高,但影响性能
  • 异步日志:性能好,但可能丢失(机器宕机时)
  • 核心交易链路:用同步日志;一般业务:用异步日志

3. 日志采样

  • 高并发场景下,不可能记录每一条日志
  • 可以按比例采样(比如 1/100),或者只采样异常请求

4. 日志轮转

  • 按大小轮转(1GB 一个文件)
  • 按时间轮转(每天一个文件)
  • 定期清理过期日志(比如保留 30 天)

5. 精简日志格式

  • 避免记录大对象、大数组
  • 只记录必要的字段
  • 考虑使用二进制格式(更紧凑)

"加日志之前,先问自己三个问题:这个日志以后会看吗?这个日志能帮我定位问题吗?这个日志的价值,值得我付出这些成本吗?"


记日志的正确姿势

说了这么多,最后总结一下记日志的正确姿势:

1. 链路要完整

JSON
复制代码
{ "trace_id": "abc123", "timestamp": "2024-01-01 10:00:00", "actor": "order-service", "action": "create-order", "input": {"user_id": 123, "amount": 100}, "output": {"order_id": "xxx", "status": "success"} }

2.上下文要足够

JSON
复制代码
{ "trace_id": "abc123", "user_id": 123, "order_id": "xxx", "action": "payment-callback", "status": "failed", "reason": "balance_insufficient", "balance": 50, "amount": 100 }

3. 日志要记录堆栈

Java
复制代码
try { // 业务代码 } catch (Exception e) { logger.error("支付失败", Map.of( "order_id", orderId, "error", e.getMessage(), "stack", ExceptionUtils.getStackTrace(e) )); }

4. 敏感信息要脱敏

JSON
复制代码
logger.info("用户登录成功", Map.of( "user_id", userId, "phone", phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2") ));

怎么形成一条完整证据链

光有日志还不够,还得能让日志串联起来。

比如一个请求过来,经过了 A 系统、B 系统、C 系统,最后出问题了。怎么把这个请求在三个系统里的日志串起来呢?

答案是:trace_id

同一个请求,所有日志都带着同一个 trace_id,这样就能串起来了。

在 Java 里,通常用 MDC (Mapped Diagnostic Context) 来实现,基于 ThreadLocal 的线程隔离,很方便。


思考问题的方法

最后想聊聊思考问题的方法。

我最大的感悟是:不要太快出结论

正确的做法应该是:

  1. 先收集事实
  2. 再绘制证据链的地图
  3. 逐步扩散
  4. 当证据链闭环的时候,再出结论

如果你一开始就有了一个结论,然后带着结论去找证据,那很容易陷入确认偏误——只看到支持你结论的证据,忽略不支持的。

证据链的方法,能保证我们客观地分析问题。


发散一下

说到可观测性,其实业界有个经典的说法:可观测性的三大支柱

  1. Logging - 离散的事件记录
  2. Metrics - 聚合的数值统计
  3. Tracing - 请求在分布式系统中的完整路径

这三个结合起来,基本上就能覆盖大部分的可观测性场景了。

还有一些可以想的:

  • 错误处理分层:网络层超时、应用层校验失败、数据层约束违反
  • 降级与熔断:当下游不可用时怎么办
  • 容量规划:基于日志分析接口调用频率、数据增长趋势
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
Issie
下载 APP