技术博客

全链路日志:一笔委托的 trace 怎么串起来

第 95 / 141 篇 · 2025-04-19 · secfin · 可观测性日志故障定位

客户报单卡了 3 秒,火线上你要回答:卡在网关、柜台、风控还是撮合?日志散在五个进程的文件里,靠人肉对时间戳拼链路,时间还对不齐(上一篇的时钟问题)。trace 体系的第一性原理其实很简单:给每笔业务一个唯一 ID,全程透传,所有日志按 ID 聚合。难的是透传过程里每一个会丢 ID 的缝。

ID 从哪来,怎么传

request_id 在协议网关生成——客户端请求的最早入口——由时间戳、节点号、随机数拼成,全局唯一且大致有序。生成后塞进上下文对象,随调用链往下传:

  • 同步 RPC:框架的隐式传参机制(Java 的 ThreadLocal、C++ 的 TLS),业务代码无感知;
  • 异步线程池:最大的缝。线程切换时 ThreadLocal 不跟着走,trace 断在半截。修法是包装任务对象,提交时把当前上下文快照进去,执行时恢复。每个异步改造点都要检查这一层;
  • MQ 消息:把 request_id 放进消息头,消费方取出来续上(《消息中间件在交易链路的使用》篇的消费端同样要做这件事);
  • 批量链路:清算任务没有 request_id 的概念,用"任务 ID + 业务键"双键关联——任务日志里记任务 ID,任务处理的具体业务(比如某笔资金流水)记业务键,两边都能串到《清算任务编排》那条依赖链上。

日志规范:格式统一,关键节点必打

格式先统一,否则聚合无从谈起:时间戳 | 级别 | 节点 | request_id | 模块 | 内容,一行一条,结构化字段固定位置。能不能 JSON 化看采集体系,字段约定比格式选择重要。

埋点的纪律:入口打全量报文,风控打结果和原因码,撮合打回报序号,出口打响应。这四个点是事后复盘的完整骨架,缺一个环节,trace 就有盲区。另外两类日志必须分文件:交易流水型日志(每笔固定一条,按日轮转,审计要查的)和调试日志(量多、随意)。混在一个文件里,审计提取和排障搜索互相干扰。

生产排障的顺序

反过来从单条 trace 捞是最低效的做法——日志量面前会先被淹没。正确顺序是先粗后细:

  1. 看错误码直方图:哪一层、哪种错误码在涨?八成的问题在这一步就定位到层了;
  2. 看关键计数器:委托到达率、风控拒绝率、回报发送量,和基线比;
  3. 最后才捞单条 trace:带着 request_id 还原这条委托的完整路径,看断在哪一拍。

这套顺序对应的是一个朴素事实:生产事故大多是面状的(某层挂了、某类单全废),不是点状的。点状问题(单笔卡死)留到第三步。

边界:敏感数据与日志级别

两条红线。第一条是敏感数据不进日志:证件号、银行卡号、密码、通讯地址的明细,日志系统的权限面比核心库宽得多,谁都能 grep 的东西不能放明文。脱敏必须在日志 SDK 里强制执行(比如证件号只留后四位),靠开发自觉写代码时记得掩码,迟早漏一个。第二条是日志级别运行时可调:排障需要临时开 debug,改配置重启进程在生产环境是不可接受的(《变更管理》篇的窗口约束),要用配置中心或 JMX 动态下发,并且"开了记得关"——debug 常驻的生产节点,磁盘和 IO 会被教做人。

保留期对齐审计要求(《审计日志》篇的 WORM 与哈希链),交易流水型日志的保存年限不是运维决定,是合规决定。

一笔委托的 trace:ID 在哪生成,断点在哪出现 客户端 请求发起 协议网关 生成 request_id 柜台 协议解析 事前风控 结果 + 原因码 撮合 / 回报 回报序号 线程池异步 ID 要手动传递 回报落库 流水序号 MQ 发布 ID 放消息头 订阅方消费 取出 ID 续上 格式统一:时间戳 | level | 节点 | request_id | 模块 | 内容,按 ID 聚合即还原全链 埋点纪律:入口全量报文、风控结果、回报序号、出口必打;流水与 debug 日志分文件 排障顺序:先看错误码直方图与计数器(面状问题),再捞单条 trace(点状问题) 红线:证件号、银行卡、密码不进日志(SDK 内强制脱敏);日志级别动态可调、用完即关

trace 体系的价值在事故里才兑现:平时觉得每个请求多带几十字节是浪费,出一次大事就知道,能把定位时间从小时压到分钟的东西,没有替代品。