分布式链路追踪系统设计
在大型项目中往往是多个服务并行运行的,比如我曾经呆过的一家公司将一个系统就拆成了这几个服务
- 后台服务
- 用户中心鉴权服务
- 客户端服务
- 数据统计服务
- 第三方平台: 比如微信公众号,Ai 相关服务
一些中间服务之间的通信采用的是 RPC, 同时这些服务都是互相调用的,往往 A 依赖于 B,B也会去依赖于 A,这也造成了这个系统出了问题其实是很难排查出问题的 因为我们不确定是什么服务引起的,缺乏一套系统的链路追踪。
那么一套系统的链路追踪应该是怎么样子的呢? 我认为它一定具备下面几点特性
- 它一定是有一个 trace id 是可以串联起所有的 服务的
- 系统核心位置加上对应的观测指标,比如当前系统接口吞吐是多少,这个慢 SQL 耗时多长时间,任务平均执行时长是多长
- 它的日志系统一定是规范的,关键地方都打上了日志,报错的地方都打上日志
- 日志聚合指标聚合,这些日志和指标都是需要去收集起来方便可以快速追踪,比如我们可以很快的根据一个 traceid 去查询起整个链路
日志 SDK 设计
为此我们设计了一个 trace SDK, 使用 Go 语言,这个 sdk 主要有两个核心的设计:
- 提供了一个强大的链路 id 生成器, 这个生成器不仅可以生成一个 trace id, 还提供了 层次 id,可以展示当前的服务位于第几个层次第几次调用,比如 1.2
- 将这个 trace id 注入到了每一个 log, 中,同时提供了统一的 业务log api,比如打印出当前 SQL 执行的耗时,这样方便我们在查看这个链路的时候可以快速定位瓶颈
l, _ := gotrace.NewLogger("user-svc", "/var/log/app", gotrace.Config{Stdout: true})
defer l.Close()
// 结构化 trace 日志
gotrace.LogDBEntry(l, tracer, "db", "users", "SELECT 1", 5)
// 标准 slog API,同一条输出链路
l.Slog().Info("hello", "key", "value")
日志存储方式设计
这种 trace 日志的存储,也是非常值得考量的一个地方,因为考虑到这种日志其实是写多读少的特性,所以我们可以借助 rocksdb 来完成设计,在go 语言中我更倾向于使用 pebble
- 上游:系统产生的链路日志(Trace Logs)推送到 Kafka
- 消费者组:部署多个“日志存储服务”实例,以消费者组方式接入 Kafka。实例数不超过 Kafka Partition 数,保证并行消费
- 存储层:每个实例在本地磁盘上管理多个按日期命名的 Pebble 数据库
在使用 pebble 的时候呢,每一个 key 就是 trace id, value 是当前这个 id 下面所有日志的拼接 写入时候使用 merge 操作,可以把对应的数据加到 value 的末尾上面去
日志聚合计算
日志聚合其实非常简单,直接广播所有的节点即可,各个节点按照对应的 trace id 去查询对应的 value,如果查询到即可返回,通常只会有一个节点返回对应的数据,这是因为日志是按照时间 存储的,所以只会存储在一个库上面。