4.2 链路追踪:请求 ID 贯穿
场景:一个慢请求,牵出五个服务
有了全量日志还不够。一次接口慢,网关记 800 毫秒,后端 A 记 600 毫秒,数据库不知道多慢——五个服务各自记日志,没有关联键,拼不出完整故事。排查时人肉对时间戳,误差几百毫秒就对不上,尤其在凌晨脑子转不动的时候。链路追踪要解决的就是这根串起所有记录的线:一个请求一个 ID,从网关贯穿到后端再到数据库,任何一段耗时都能单独掐出来。
我们没有上完整的 OpenTelemetry 分布式追踪平台(那又是一个大系统),而是用最轻的方式实现核心价值:网关生成请求 ID,转发时放进自定义 Header 透传,后端日志与数据库慢查询日志都带上它。简单的方案落地快、维护省,而排障百分之九十的需求只是“这次调用慢在哪一段”。
决定走轻量路线前我们算过一笔账:完整的追踪平台要部署采集器、存储后端、查询界面,至少两台机器外加一个人力维护,而八成的排障场景一个 ID 就能覆盖。先把 ID 体系铺满所有服务,等链路数据积累到一定程度再考虑升级到标准协议,是更务实的路径。技术选型最怕一上来就追求大而全,铺不满的追踪系统比不追踪更坑人。
方案:三级 ID 体系
一个 ID 不够用。真正定的体系是三级:全局唯一的 TraceID、每跳一个 SpanID、父子用 ParentID 关联。后端拿到 ParentID 生成自己的 SpanID,层层嵌套就是调用树。虽然比标准协议简化,但自洽够用:
| ID | 生成者 | 生命周期 | 用途 |
|---|---|---|---|
| TraceID | 网关 | 整条链路 | 全局唯一线索 |
| SpanID | 每个服务 | 单跳 | 定位节点 |
| ParentID | 上游传入 | 记录 | 构建调用树 |
三级 ID 体系里最容易被砍掉的是 SpanID,我们一度也想偷懒只留 TraceID。保留它的理由是一次排障:某个接口慢,但网关与后端日志都显示正常,最后发现慢在网关内部两次中间件之间的排队上。只有每跳都有 SpanID,才能把“网关整体 200 毫秒”拆成“鉴权 5 毫秒、限流 3 毫秒、缓存 2 毫秒、转发等等 190 毫秒”,看到真正的病灶。粒度越细,优化越有的放矢。
所有 ID 走 Header 传输(如 X-Trace-Id),日志输出格式统一在最前面打 TraceID,这样 CLS 检索时一条搜索就能拉出全部相关记录。命名上要避免与行业通用 Header 冲突,也避免内部多套系统的 Header 互相覆盖——别小看 Header 命名,这是我们踩过的小坑。
ID 的格式也讨论过几轮。最终定下 32 位以内、全小写、含时间信息但不暴露机器 IP。曾经有方案直接用 UUID,整数化索引困难、肉眼不可读,在日志里密密麻麻一片;而时间戳前缀的格式顺着日志列表就能看出先后,排障时直觉友好。生成函数必须线程安全且足够快——每个请求都调用,慢一点全链路都慢。
ID 的唯一性保障也是想清楚后才做的。早期生成函数只用毫秒时间戳加机器位,某次高峰期同毫秒并发高的机器上撞了 ID,两条不同请求的链路在 CLS 里缠在一起,排查越排越糊涂。修复是引入随机熵位并用 SET NX 做一次冲突检测,极小概率撞上时直接重新生成。链路数据的可信度全靠 ID 唯一,这一层省不得。
配置:生成与透传
网关侧的生成与透传逻辑极简,但有一处必须注意:ID 生成放 TraceMiddleware 第一位(第 1.2 节中间件链),保证所有中间件与后端都能拿到:
<?php
// 追踪中间件:整个链路的起点
public function handle($request, callable $next)
{
$traceId = $request->getHeaderLine('X-Trace-Id');
if ($traceId === '') {
$traceId = sprintf('%s-%08x', date('YmdHis'), random_int(0, 0xffffffff));
}
$request = $request
->withAttribute('trace_id', $traceId)
->withHeader('X-Trace-Id', $traceId)
->withHeader('X-Span-Id', $this->spanId());
$resp = $next($request);
// 响应头回传,客户端也能拿着排查
return $resp->withHeader('X-Trace-Id', $traceId);
}
ID 的生成规则刻意包含时间戳前缀,优点是 CLS 里按时间范围扫时天然有序,缺点是可预测,因此对安全敏感场景(如随机性要求高的场景)可以加 hash 熵值。响应头回传 TraceID 是点睛之笔:客户报障时直接把响应头贴给我们,排障链路从零延迟开始。这件事还有个意外收益:客户的技术同学开始主动把 TraceID 写进他们自己的报障模板,沟通效率又上一层。好的设计会自己长腿走进客户的流程里。
透传还有一处容易漏:异步任务。后端把请求转发给消息队列或投递到定时任务时,TraceID 若不随消息体一起带走,这段链路就成了黑洞。我们的约定是消息载荷里固定携带 trace_id 字段,消费端落到日志后才算闭环。同理,curl 多级跳转、内部回调钩子都要显式带上 Header,这些边角地方漏一个,链路就断一截。
ID 的打点位置也值得克制。我们见过有团队给每个中间件都加 Span 上报,结果链路数据量是业务日志的三倍,存储费用先扛不住。我们的 Span 只覆盖关键跳:网关入口、中间件链总耗时、后端入口、数据库代理。四个点足以回答百分之九十的问题,数据量保持在日志的一成以内。粒度是成本与价值之间的权衡,先粗后细逐步加密,比一上来就全量铺网更可持续。
踩坑:ID 被吞与日志不齐
第一个坑是 ID 在某一跳被吞。某个后端服务用了旧框架,自定义 Header 被当作非法头过滤掉了,它的日志没有 TraceID,链路在那里断掉。修复方式是统一所有服务的 Header 白名单,并把“日志中能看到 TraceID”写入后端接入网关的强制规范,新服务不满足不许上线。规范一旦强制,这坑再没出现。
第五个坑是 TraceID 的对外暴露。响应头回传 TraceID 方便排障,但也意味着外部客户能拿到 ID。所幸我们的 ID 不含机器 IP 等敏感信息,且检索接口只开放给内部。即便如此,安全评审仍要求 ID 不可被枚举遍历——前缀可预测加随机熵位的设计在这里再次发挥了作用。对外可读与可被遍历是两回事,TraceID 的设计要守住这条线。
第二个坑是时钟不齐。不同机器时钟差几十毫秒,两段耗时算出来可能为负数或对不上。所有机器 NTP 同步是前提(限流那节也强调过),同时计算耗时一律用单机单调时钟,跨机只做时间排序不做精确相减。第三个坑是慢查询日志缺 TraceID:MySQL 慢查询日志不带自定义内容,我们改用数据库代理层(后端统一走的一个查询中间件)记录,把 TraceID 注入查询注释里,慢查询自然带上线索。
第四个坑是日志格式不统一。各服务的日志字段名五花八门,有的叫 trace_id 有的叫 requestId,CLS 检索时得写一串 OR 条件,痛苦不堪。后来统一约定:字段名一律小写下划线,TraceID 字段名全村只有一个。统一字段的收益远超实现成本,这也是链路追踪能真正用起来的地基。
链路的日常用法
链路追踪的日常价值集中在三类场景:慢请求定位(按 TraceID 拉全部日志看每跳耗时)、错误归因(失败在哪一跳一目了然)、容量分析(聚合所有 Span 看哪个服务是瓶颈)。每周我们还会抽样慢请求做链路复盘,把耗时最长的链路画成图贴在团队区,谁的责任谁领走。追踪数据让“优化哪里”这个问题永远有数据支撑,而不是靠猜。
一个真实案例:某天订单接口偶发超时,链路数据显示网关到服务 A 这段耗时正常,服务 A 内部处理耗时正常,但服务 A 到 Redis 这跳偶发激增。顺藤摸瓜发现是一台 Redis 从库在同步大 key,最终给业务侧的建议是给大 key 拆分。没有 TraceID,这个结论可能要花一整周。
链路数据的长期价值还能反哺架构决策。我们把三个月的链路数据按调用对聚合,发现某两个服务之间的调用量是其他对的十倍,但其间网络同机房——这个发现让我们在容量规划时优先给这对服务做连接复用与批量调用改造。链路数据指着哪里改,比架构评审拍脑袋靠谱。
链路数据还要有意识地做分层保留。全量 Span 只存 15 天,要做季度优化研究又不够。我们的做法是正常链路 15 天,慢请求链路(P99 之外)自动转存到冷主题保留 90 天——慢请求天然就是样本,是系统行为最值得研究的那一部分。存储成本几乎没增加,季度优化报告的素材却厚了一层。
小结
链路追踪的轻量做法:网关生成 TraceID,Header 透传,后端日志与查询层全部携带。三级 ID 体系够画调用树,响应头回传让客户也能参与排障。落地难点不在代码而在规范——每个后端必须把 TraceID 写进日志,这是接入网关的硬门槛。基础设施的价值往往取决于覆盖率,而不是深度。
再补一条人才视角的经验:TraceID 体系上线后,新同学的成长速度明显变快。以前遇到线上问题要老人带着看日志,现在新人按 TraceID 自己就能顺出完整链路,甚至能独立给出优化建议。可观测性的收益不止是排障速度,它悄悄降低了整个团队理解系统的门槛。
如果只能记住一条:别等“完整的追踪平台”才动手,一个 Header 加一行日志字段,今天就能让八成的排障从痛苦变为享受。
还有一条关于存储:链路日志不要与业务日志混在一个主题。混了之后按 TraceID 检索会把无关的业务日志也带出来,噪音大且费用高。我们给链路 Span 单独建主题,保留 15 天够用,业务日志另留。日志分主题这件事前期多花一小时,后期天天受益。
