乐趣区

调用链系列1解读UAVStack中的贪吃蛇

一、背景

对于分布式在线服务,一个请求需要经过多个系统中多个模块,可能多达上百台机器的协作才能完成单次请求。这种场景下单靠人力无法掌握整个请求中各个阶段的性能开销,更无法快速的定位系统中性能瓶颈。当发生故障时通常需要查看大量日志跨越多个团队来确认问题。

二、举个栗子

程序猿小亮作为一个在职场摸爬滚打多年资深工程师,他可能面临的系统设计是这个样子的,如下图。

(图片来自于网络)

借助良好的系统设计和编码规范,对于一般有问题的请求处理,小亮依据自己对多个系统的了解通过翻阅大量的日志文件(前提是日志输出也需要规范)花费两个小时来定位到问题。随着用户的不断增长系统复杂度也呈现指数增长,小亮的大部分时间都浪费在了团队沟通之类的工作上。小亮的幸福指数也像系统复杂度一样呈现指数下降。

小亮这时可能会想,要是有一个东西能把每次请求经过的系统都记录下来,要是能把每个节点消耗时间、处理类神马的信息也抓出来那这个世界得多么美好。

一个偶然的机会小亮知道了 UAVStack 其中一个叫做调用链的神奇功能,在对业务代码没有任何侵入的前提下轻松解决了他的难题。下面就让我们一起来开启一段探索 UAVStack 的神奇之旅。

UAVStack 调用链技术栈支持

三、效果展示

轻型调用链展示详情:

重调用链开启以后请求报文体抓取视图:

更多使用技巧和说明请参阅官网:https://uavorg.github.io/docu…_useroperation/91.html(用户指南中调用链部分)。

四、具体实现

UAVStack 调用链实现分为模型设计、服务端信息收集(轻 / 重)、方法级信息收集(轻 / 重)、客户端信息收集(轻 / 重)、调用链协议设计(轻 / 重)、调用链上下文传递、调用信息记录及传递、调用数据统计处理等。由于篇幅限制,本期暂时只分享其中的模型设计及实现调用链模型时序图。

五、模型设计

借鉴前人经验并揉合具体业务场景需求,抽象出了如下调用链模型:

调用链元数据:

1)SpanEndpointType:调用类型(Root(“E”),Service(“S”), Client(“C”), Method(“M”));

Root 指本条调用链中的第一个节点,即一条调用链的开始位置,可以是一个服务请求,一次 httpclient 调用等;

Service 指当前调用链中非第一个节点且是系统中对外提供的服务,如用户登录服务;

Client 指当前调用链中非第一个节点且是当前系统与外部沟通的一种途径,如 httpclient、mongoclient 等;

Method 值当前调用链中非第一个节点且是系统中的一个函数,如日志数出函数等。

2)traceId:调用链唯一标识符;

3)spanId:一条调用链中当前节点的调用顺序(与 SpanEndpointType 结合唯一);spanId 采用分层设计,形如 1.2.1,既能表示调用顺序同时又能反应所才调用链层级;

4)parentId:一条调用链中当前节点的父调用节点。

调用链绘制规则:

1)调用者(服务、web)最初调用(无父调用)记为开始节点 E,并生成唯一调用链 ID,traceID;

2)系统内应用组件调用(如 httpclient,方法调用等),spanId 末尾数字加 1(若为第一个则末尾加.1);

3)系统间调用(如 A 服务调用 B 服务),A 服务与 B 服务 span 信息只有 SpanEndpointType 不同(分别对应 span 的两个端)。

举个栗子

背景介绍:用户小明想通过网络获取一些知识,通过网络他进入了系统 O。服务 O 中部署了服务 A 和 B,A 服务使用 httpclient 与 B 通讯,B 服务先会与 redis 交互然后和 myql 交互,最后系统 O 将小明感兴趣的内容返回给小明;

完成此次请求 UAV 抽象出如下调用链模型:

1)小明(下图中的调用方)通过门户访问了 A 服务,此时调用链生成唯一 traceId 并将当前节点的 SpanEndpointType 置为 N(第一个节点的意思),spanId 置为 1(当前调用层中的第一个节点),parentId 置为 E(没有父节点的意思);

2)A 服务通过 httpclient 向 B 服务发起一次 http 请求,此时调用链元数据如下 traceId(沿用父节点 id);1.1(spanId 末尾加.1, 因为为第一次调用);1(parentId 父节点的 spanId);C(调用类型记录为 C 客户端调用);

3)B 服务接收到来自于 A 服务通过 httpclient 的调用,此时调用链元数据如下 traceId(沿用初始调用时 id);1.1(spanId 沿用传递过来的 spanId);1(parentId 沿用传递过来的 parentId);S(调用类型记录为 S 服务端处理请求);

4)B 服务先查询 redis,此时调用链元数据如下 traceId(沿用初始调用时 id);1.1.1(spanId 末尾加.1, 因为为第一次调用);1.1(parentId 父节点的 spanId);C(调用类型记录为 C 客户端调用);

5)B 服务又发起对 mysql 的查询,此时调用链元数据如下 traceId(沿用初始调用时 id);1.1.2(spanId 末尾数字加 1,因为为非第一次调用);1.1(parentId 父节点的 spanId);C(调用类型记录为 C 客户端调用);

6)处理结束,调用链将记录的信息进行记录。

调用链时序图

UAVServer:中间件增强框架,提供在中间件的不同生命周期进行劫持的能力,即中间件劫持技术,如 tomcat webcontainer 启动开始时刻等;

JEEServiceRunGlobalFilterHandler:借助中间件劫持技术延伸出的全局 filter,能够拦截所有经过中间件(tomcat 等)的请求;

ServiceSpanInvokeChainHandler:调用链中专注处理归为 Service 类型节点的 handler;

ClientSpanInvokeChainHandler:调用链中专注处理归为 Client 类型节点的 handler;

XXAdapter:泛指调用链中所有的 adapter,提供在 handler(分为 Service、Client、Method 三种 handler,图中省略了 Method 类型)执行动作 before 和 after 时刻操作数据的能力。

实现对用户代码无任何”侵入”的前提下完成调用链的生成,过程大致分为如下几个过程:

1)在 JEEServiceRunGlobalFilterHandler 的 doRepuest 中包装解析请求;

2)xxAdapter 中的 before 对数据进行适配;

3)xxHandler 处理对应范围内(Service、Client 和 Method)内请求数据;

4)xxAdapter 中的 after 对数据进行整理或记录;

5)在 JEEServiceRunGlobalFilterHandler 的 doResponse 中返回处理过后请求。

六、总结

本文主要目的是让读者对 UAVStack 的调用链有一个整体的认识,初步了解一条调用链绘制的大致生命周期,具体的实现将在以后的分享中详细介绍。

作者:李崇

退出移动版