队列的另一头要用链接连,而不是父子关系
一句话总结
越过队列,要用链接而不是父子关系来连接。父子关系的意思是“这件事结束了,那件事才能结束”,而生产者不会等待消费结束。
为什么需要它
给订单 API 加上队列之后,跟踪变得很奇怪。给用户的响应 40ms 就发出去了,画面上的根跨度却显示 3 秒。做埋点的人出于“想把一个订单被处理完的全过程放在一条跟踪里看”的好意,把消费一侧的跨度做成了生产跨度的子级。
父子关系不是这个意思。父跨度要在子级全部结束之后才结束。于是生产者明明已经返回了响应,却关不掉这个跨度,队列积压的日子里,这个跨度会一连开着几分钟。再混进扇出就更糟——一个订单产生五条消息,其中一条又产生三条,一条跟踪就会长成数千个跨度,后端开始对这一条跟踪特殊对待。
工作原理
OpenTelemetry 为这种位置准备了链接(Link)。链接表示的关系是“这个跨度与那个跨度在因果上相连,但那个跨度不会等我”。创建跨度时可以一并给出链接列表,每个链接还可以附上属性。详细定义见 跟踪 API 规范 和 Traces 概念文档。
| 连接方式 | 含义 | 使用场景 |
|---|---|---|
| 父子关系 | 父级等待子级 | 同一请求内的同步调用 |
| 链接 | 有因果关系,但不等待 | 跨越队列、批处理、重新处理 |
| 什么都不做 | 关系不会被记录 | 确实无关的工作 |
所以队列实验的基本形态是这样的。生产一侧创建放入消息的跨度,并把该跨度的 ID 随消息一起送出。消费一侧以新跟踪的根开始跨度,同时用随消息而来的 ID 创建链接并挂上。这样,生产跟踪随用户响应立即结束,消费跟踪只活它自己的那段时间。两条跟踪由链接相连,以后可以顺着找过去。
批量消费一次取出多条消息,要画成带有多个链接的一个跨度。每条消息各建一个跨度,会让处理一批的这一件事散开;不加链接只建一个,又不知道哪些消息属于那一批。消息语义约定在这种情况下规定要写 messaging.batch.message_count——表见 消息跨度约定。
在队列中等待的时间,不会自动记在任何跨度里。要把生产时刻随消息送出,由消费跨度把差值以属性写下来。有了这个值,才能区分“处理慢”和“队列积压”,二者的修复办法完全不同。
跨度种类也由同一份文档规定。创建或发送消息的跨度是 PRODUCER,应用程序处理消息的跨度是 CONSUMER(只负责取回的 receive 是 CLIENT)。这个值不是装饰,而是分析工具解读跟踪之间关系的线索,如果不按规则标,工具就认不出队列。
链接上可以附属性,也应该附。只有链接的话,“连着”这个事实会保留,但为什么连着不会保留。是因为队列消息、因为重新处理,还是因为一起进入了某个批次,写在链接属性里,以后人才能读懂。
要明确本模块不讨论的内容。读取 HTTP 头 traceparent 的片段来接上断开的链条,是传播实验的任务;在一个进程内通过线程或 Task 传递上下文,是上下文边界实验的任务。这里要确定的是识别出用父子关系连接并不正确的位置,并改成链接的判断。
也写一下实验环境无法判定的事项。Pod 里没有 broker、Collector,也没有跟踪后端。所以后端界面如何画出链接、尾部采样是否会把用链接相连的两条跟踪一起保留,在这里都无法确认。队列用一个文件来模拟,判定全部依据 SDK 导出的 JSONL 转储文件的结构和属性。耗时会因机器状况而变化,所以不看绝对数值,只看关系。
在现场相遇的样子
最常见的信号是“根跨度的时间与用户等待的时间不一样”。响应 40ms 就发出了,跟踪却是 3 秒,几乎总是因为把异步作业挂成了子级。用这样的跟踪测不了延迟——用户经历的时间和系统工作的时间被揉成了一个数字。
另一个是“只有某些跟踪打不开”的症状。在把扇出用父子关系连起来的系统里,一个订单长成数千个跨度时,后端会截断那条跟踪,或者界面卡死。改成链接之后,每条跟踪都能保持很小,整个旅程可以顺着链接重新拼接起来。实验的最后一步要亲手做这个拼接。
下一项实验要做什么
把消息放进一个只有一个文件的极小队列,稍后取出处理。先用父子关系连接生产与消费,通过转储文件看根跨度如何膨胀、空隙出在哪里。接着把消费一侧改成新跟踪的根,并用链接连接。把批量消费画成带有多个链接的一个跨度,把在队列中等待的时间留成属性,按约定标上跨度种类和消息属性,在链接上写明原因。最后应用到一个生成多个的扇出,顺着链接跟下去,把一个订单的旅程跨越跟踪拼接起来。