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

分布式链路断掉的地方

跨度应该在哪里断开

在 TT Lab 中继续学习

一句话总结

划分跨度不是按代码块来划,而是看下一个打开这条跟踪的人会问什么。跨度太少,就不知道该看哪里;太多,就没有人愿意打开。

为什么需要它

收到“支付很慢”的反馈后打开跟踪,却只有一个跨度。名称是 POST /checkout,时长 211ms。从中只能读出“花了 211ms”。是读购物车花的时间,是支付代理商慢,还是我们的进程里把商品价格算了二十四遍,都无从得知。埋点是开着的,却是回答不了任何问题的埋点。

于是也有团队走向了另一个极端。在循环里每次放一个跨度,一个请求就变成了三十个跨度,商品有两百件的订单则超过两百个。画面上是一模一样的条形无穷无尽地延伸,在故障会议上,没有人把那条跟踪滚动到底。跨度数既是存储成本,也是读者的注意力预算。

两种失败都是跳过了同一个问题的结果——这个请求以后会被问到什么,要回答这个问题,边界应该划在哪里。

工作原理

划定边界时可以用一个数字:父跨度存活期间,没有被任何子跨度覆盖的时间。本文称之为埋点空白。子跨度之间可能重叠,所以要按并集来计算,再从父跨度的时长中减去,剩下的就是它。

POST /checkout  ├────────────────────────────────────────┤  211ms
  cart.load             ├──────┤                             30ms
  payment.charge                      ├────────────┤         60ms
  공백            ·······        ······              ······   121ms

空白很大,不是说慢,而是说这个区间还没有跨度。这个区分很重要。自身耗时和关键路径是在已经创建好的跨度之间询问“什么慢”的性能计算,而空白是指出尚未埋点之处的地图。测量空白的目的不是排名,而是选出下一个跨度该划在哪里。

父跨度 211ms 内,子跨度 cart.load 的 30ms 和 payment.charge 的 60ms 所覆盖的位置,以及没有被任何子跨度覆盖的 121ms 剩成三段的样子

划分边界时用的规则归纳为三条。

规则 含义 违反时
进程边界必须有跨度 进来的请求、出去的调用都要分开 分不清是别人的错还是我们的错
先拆分空白大的区间 没被覆盖的时间就是不知道的时间 增加跨度也得不出答案
循环要折叠,不要做成跨度 用次数、总耗时、最大值 同样形状的条形铺满整条跟踪

第三条在实际工作中最常出错。把循环区间包进一个跨度,再把 count、total_ms、max_ms 作为属性加到这个跨度上,一个跨度就能说明整个循环。只留平均值,“二十四次里有一次慢了十倍”这个事实就消失了,所以要同时留下最大值,而那一次具体是什么,则写成事件。事件是附在跨度内某个时间点上的记录,适合用来记录循环中的特定时刻。

边界的标记使用 SpanKind。处理进来的请求的跨度是 SERVER,发往进程之外的调用是 CLIENT,在进程内部划分出来的区间是 INTERNAL。这个标记不是装饰,而是以后区分“我们等待的时间”和“我们花费的时间”的依据。CLIENT 跨度的总和大,说明是在等别人;INTERNAL 大,说明是我们自己在干活。

最后,这个判断如果留给个人喜好,每次评审都要重新争论一遍。把每个请求的跨度数上限和允许的空白比例写成文件,让机器去读,那么在给新的 handler 埋点时,同样的标准就会自动生效。超过上限并不总是错的,但光是让人写下一行超出的理由,“先把跨度加上再说”的做法就会消失。

也要说清楚在这个实验 Pod 中无法判定的事项。这个 Pod 里既没有 Collector 二进制,也没有把跟踪画出来的后端界面。所以“在界面上这条跟踪好不好读”要靠眼睛看,评分器看的是把跨度导出为 JSONL 的转储文件的结构——跨度数、父子关系、名称、SpanKind、属性键,以及空白比例。毫秒绝对值会因为 Pod 繁忙而变化,所以只按比例判定。

在现场相遇的样子

一个订单服务只开了自动埋点,过了几个月。自动埋点会在 HTTP 入口和数据库驱动上创建跨度,所以跟踪看起来并不空。可是一打开慢请求,空白总是占一半。那一半是应用代码自己运行的区间,不是自动埋点能看到的地方。开始测量空白之后,“应该在哪里手动加跨度”才变成了一份清单。

相反的事故也有。一个批处理作业给每个条目创建一个跨度,平时条目只有十个,没有人察觉。月底条目变成两万个,一条跟踪就成了两万个跨度,采集端被这一条跟踪拖慢了。解决办法不是删掉跨度,而是折叠——去掉条目跨度,在批处理跨度上以属性记下处理条数和最长耗时。跟踪重新变得可读,慢的那一条仍以事件的形式保留了下来。

下一项实验要做什么

拿到一段完全没有埋点的订单处理代码,从只有一个跨度的跟踪出发。测量空白,找到没有埋点的区间,把它拆开,使空白降到 5% 以下。接着把循环区间做成跨度,亲手数一数跨度会变成多少个,再把同样的循环用属性和事件折叠起来,重新减少。用 SpanKind 标出边界之后,把每个请求的跨度数上限和允许的空白写成规则文件,再写一个检查这些规则的小程序,在前面生成的转储文件上运行。最后在同一套规则下,从头给第二个 handler 埋点。