TT Lab
开始
学习 学习路径 课程

分布式链路断掉的地方

只看这一个跨度能复现故障吗

在 TT Lab 中继续学习

一句话总结

好的跨度,是只凭这一行就能再次触发同样失败的跨度。属性是那个复现输入,事件是这期间什么时候发生了什么,链接则指明是哪条另外的跟踪让它做了这件事。

为什么需要它

凌晨支付失败集中爆发。打开跟踪,跨度里有 http.route=/checkout、http.response.status_code=502,还有 1842ms。这些都是自动埋点替我们做好的。但拿着这个跨度,什么也做不了。购物车里有几件商品、缓存是不是空的、队列积压了多少、支付重试了几次——复现所需的值一个都没有。

相反,有的团队走向“先全部放进去”,结果出了事故。把请求体整个放进属性,客户邮箱和认证令牌就原样堆积在可观测性后端里,而那个后端是全体开发人员都能看到的。此后,这个团队把属性里放什么列为代码评审的一项内容。

本实验讨论的就是这两者之间的地带。不是 SDK 怎么配置,也不是按什么顺序确定 service.name,而是属性栏要用什么来填。

工作原理

先把问题倒过来。问的不是“放什么”,而是只拿着这个跨度,要想再次触发同样的失败,还缺什么。把答案写下来,就是属性清单。复现输入(商品数、请求体大小)、当时的状态(缓存命中、队列深度),以及我们做过的事(重试次数),通常都放在这里。

然后区分不能放的值。联系方式、卡号、认证令牌这类值不以原文保留。但全部丢掉的话,调查时又会很为难,所以要换成下面三种方式之一留下。

原文 替换后留下的方式 这样仍能回答的问题
邮箱、账号 哈希(前几位) “是不是反复发生在同一个用户身上”
卡号 类别(品牌) “是不是只在特定发卡机构出现”
认证令牌 长度 “令牌是不是被截断后传进来的”

第三,带有时间点的事实不是属性,而是事件。重试了两次是一个数字,所以是属性;但第一次重试是什么时候、因为什么原因发生的,是带有时刻的记录,所以是事件。同一个事实只留成属性,顺序会消失;只留成事件,又难以聚合。两者不是竞争关系,而是分工不同——计数用属性,发生的瞬间用事件。

第四,有些关系不是父子关系。队列里积压的任务以后被处理时,那个任务是在另一条跟踪中产生的,如果把处理跨度挂成那条跟踪的子级,就会出现一个几小时之后才结束的奇怪的父级。这时用的就是链接。链接只说“这个跨度与那个跨度有关系”,父级位置留空。很多团队把关系写成属性字符串(parent_trace_id=...),这样工具无法把它们连起来,只能由人用眼睛去找。这里只是尝试一次“关系是链接,不是属性”,至于如何设计跨越队列的跟踪,留到后面的模块单独讲。

最后是成本。数一数每个属性键有多少种取值,性质就分开了。订单号每个请求都不同才正常(标识符),卡的品牌则应该只有三种(类别)。问题在于本该是类别的位置混进了原文——如果异常消息里嵌着订单号,一个键转眼间就会有数千种值。所以要把每个键分成“不限制唯一值的键”和“属于类别型、唯一值应该很少的键”,写下来,再数转储文件,找出不符的地方。

到此为止确定的内容,如果只留成文字,下一个人不会去读。要做成逐行写明键名、类型、允许值、唯一值性质的规约文件,再配一个能找出违反规约的跨度的小检查器。这样,给新的 handler 埋点时,规约就会自动跟上。

也记下这个 Pod 中无法判定的事项。没有 Collector。实际运行中,还会再用 Collector 的 redaction processor 过滤一遍,但这里无法运行那个环节,所以只能看到应用程序自己过滤后的结果。属性在后端如何建立索引、成本有多高,在这个 Pod 里也无从得知——所以改为数转储文件,用唯一值的数量来代替。另外,SDK 截断属性个数和长度的设置,不在本实验范围内。

在现场相遇的样子

有一个服务,每次出故障,调查都停在“无法复现”。跨度里连请求标识符都没有,没法在日志里找出是哪个请求失败了。加上六个属性(订单号、商品数、请求体大小、缓存命中、队列深度、重试次数)之后,就可以挑出一个失败的跨度,原样把同样的输入再放进去试一遍,调查时间从以小时计缩短到以分钟计。

另一起事故正相反。把异常消息原样放进属性成了惯例,结果一个键有了几万种值,用这个键搜索的界面每次都因超时而挂掉。解决办法不是删掉消息,而是拆成两份——能分类的简短种类(declined、timeout)放进属性,给人读的长句则移到跨度状态消息和事件里。搜索又变快了,给人读的句子也原样保留了下来。

下一项实验要做什么

拿到一个失败请求的一行跨度,先写下复现所需却缺失的值。把这些值作为属性加上,不能放的值换成哈希、类别、长度留下。重试和缓存未命中这类带有时间点的事实移到事件里,处理两百个请求,数出每个属性键的唯一值,做出预算表。根据由此暴露的泄漏键写出团队规约文件,再编写检查这个规约的程序,在自己的转储文件和别人的转储文件上运行。最后,在按规约给由队列触发的退款 handler 埋点的同时,用链接把产生该任务的跟踪连起来。