介绍
淘宝现在是一个由很多个应用集群组成的非常复杂的分布式系统。这些应用里面主要有处理用户请求的前端系统和有提供服务的后端系统等。这些应用之间一般有RPC调用和异步消息通讯两种手段,RPC 调用会产生一层调一层的嵌套,一个消息发布出来更会被多个应用消费,另外,应用还会访问分库分表的数据库、缓存、存储等后端,以及调用其他外部系统如支付、物流、机彩票等。
请试想一下,现在淘宝一个买家点击下单按钮所产生的网络请求到达淘宝服务器之后,就会触发淘宝内网数百次的网络调用。这些调用中有哪些出问题会影响这次交易,有哪些步骤会拖慢整个处理流程,双十一的交易高峰需要给应用集群分配多少台机器,这些都是需要考虑的。但是调用环境的复杂度,已经很难用人力去做准确的分析和评估了,这时候 EagleEye 就派上了用场。
EagleEye (鹰眼)通过收集和分析在不同的网络调用中间件上的日志埋点,可以得到同一次请求上的各个系统的调用链关系,有助于梳理应用的请求入口与服务的调用来源、依赖关系,同时,也对分析系统调用瓶颈、估算链路容量、快速定位异常有很大帮助。另外,业务方也可以在调用链上添加自己的业务埋点,使网络调用和实际业务内容得到关联。
异步场景:EagleEye基于ThreadLocal实现上下文的存储,所以当业务方使用同步的方式时对使用者透明,但是该方式无法支持异步线程的场景,所以在使用异步线程时需要手动传递上下文,当业务逻辑转移到异步线程时,需要先备份 EagleEye 的调用上下文到异步任务中,保证链路的正确性。
对异步场景的修改方案一(推荐)
提交异步线程前,需要做 EagleEye 上下文备份
Object ctx = EagleEye.getRpcContext(); // 从当前 ThreadLocal 备份
MyAsyncTask task = new MyAsyncTask(); // 这里的 MyAsyncTask 是一个业务自定义的 Runnable
task.setRpcContext(ctx); // 将 ctx 保存到 task 中
Future future = bizThreadPoolExecutor.submit(task); // 提交任务
// 后面继续执行其他逻辑,或者用 future.get() 等待任务的结果,都没有问题
// 如果 submit 多个 task,每个 task 都需要保存一份 ctx
真正执行任务时,还原 EagleEye 上下文
class MyAsyncTask implements Runnable {private Object ctx; // 用于存放之前保存的 EagleEye 上下文public void setRpcContext(Object ctx) { this.ctx = ctx; }public void run() {EagleEye.setRpcContext(ctx); // 还原到 ThreadLocaltry {// 开始做异步逻辑,如调用 HSF、Notify、Tair 之类// ...} finally {EagleEye.clearRpcContext(); // 务必清理 ThreadLocal 的上下文,避免异步线程复用时出现上下文互串的问题}}
}
核心
TraceId
在复杂的分布式系统环境下,EagleEye是一个有广泛用途的调用分析和问题排查工具。与一般的调用信息埋点日志相比,EagleEye埋点的一个显著的不同点在于它的每条日志都有与每次请求关联的上下文ID,我们称为TraceId。通过TraceId,后期的日志处理时可以把一次前端请求在不同服务器记录的调用日志关联起来,重新组合成当时这个请求的调用链。因此,EagleEye不仅可以分析到应用之间的直接调用关系,还可以得到他们的间接调用关系、以及上下游的业务处理信息;对于调用链的底层系统,可以追溯到它的最上层请求来源以及中间经过的所有节点;对于调用链的上层入口,可以收集到它的整棵调用树,从而定位下游系统的处理瓶颈,当下游某个应用有异常发生时,能迅速定位到问题发生的位置。
rpcId
为了区别同一个调用链下多个网络调用的顺序和嵌套层次,EagleEye还需要传输和记录RpcId。
RpcId用0.X1.X2.X3…Xi来表示,Xi都是非负整数,根节点的RpcId固定从0开始,第一层网络调用的RpcId是0.X1,第二层的则为0.X1.X2,依次类推。例如,从根节点发出的调用的RpcId是0.1、0.2、0.3,RpcId是0.1的节点发出的RpcId则为0.1.1、0.1.2、0.1.3。
EagleEye 本身具备透明传输 TraceId、RpcId 的能力,这个能力通用可以用来传输业务的一些数据。例如,如果希望追加自己的业务信息到调用链上,应用可以通过 EagleEye 提供的 API 进行链路分组、链路打标等,EagleEye 会把这些信息一层层的透明往后端系统传递,而且信息也同样记录在每条调用日志里面。
EagleEye 数据透传的功能,在全链路压测、子帐号信息传递、二套环境项目隔离、强弱依赖检测、敏感操作的前端来源跟踪等场景都起到了不可缺少的重要作用。
用户数据(UserData)
EagleEye日志中的UserData字段为业务自定义的数据,该字段包含了RPC层的业务数据,该数据分为两种: 1.本地数据:记录在本地日志中; 2.链路数据:记录在本地日志中,并会跟随调用链传递至下游。
UserData是以"@“开头的字符串数据,可能包含多对业务数据,每对业务数据以KV格式记录,分隔符为不可见字符,两个KV之间的分隔符是0x12,K和V的分隔符是0x14,以K的格式区分本地数据还是链路数据,本地数据的K以”@"开头。 例子: K(本地)=,V=RPC; K(链路)=i,V=7325abff。其中|是EagleEye的日志字段分隔符,第一个@标识该字段为UserData,第二个@标识rpcName这个K是本地数据
|@@rpcName0x14RPC0x12i0x147325abff0x12
使用:
在业务代码中调用EagleEye-Core的API即可 本地数据:
EagleEye.attribute(“rpcName”, “RPC”);
链路数据:
EagleEye.putUserData(“i”, “7325abff”);
一般情况下,用户需要在客户端将TraceID、RpcID和UserData附在请求中,发送给服务端,服务端收到请求后将该信息放回EagleEye的上下文中。
客户端
RpcRequest createRpcRequest() {// 构造请求对象RpcRequest request = new RpcRequest();// 把 EagleEye 的当前上下文作为“附件”传送出去// 常见的 RPC 可以看看是否支持使用 header、attachment、attributes、properties 之类的手段传递request.addAttachment("EagleEye-TraceId", EagleEye.getTraceId());request.addAttachment("EagleEye-RpcId", EagleEye.getRpcId());request.addAttachment("EagleEye-UserData", EagleEye.exportUserData());return request;
}
服务端
// 从请求获取TraceId、RpcId、UserData,还原EagleEye调用链上下文
String traceId = request.getAttachment("EagleEye-TraceId");
String rpcId = request.getAttachment("EagleEye-RpcId");
String userData = request.getAttachment("EagleEye-UserData");
// 重新构建上下文
Map<String, String> context = new HashMap<String, String>();
context.put(EagleEye.TRACE_ID_KEY, traceId);
context.put(EagleEye.RPC_ID_KEY, rpcId);
context.put(EagleEye.USER_DATA_KEY, userData);
EagleEye.setRpcContext(ctx);
如何开启一次链路追踪:EagleEye.startTrace,这个方法会生成一个唯一的追踪标识,并将其存储在当前线程的上下文中,以便在当前线程中进行传递。同时,EagleEye.startTrace方法还会记录当前请求的开始时间,并将其存储在追踪上下文中,用于计算各个节点和服务的执行时间。
EagleEye.startTrace(traceId, traceName, 90); //标识一次trace调用的开始,traceName为用户设置,EagleEye在展示时会以该参数为分组条件(注意不要发散)
如果第一个参数为null,表示当前调用链没有父调用链,即当前调用链是根调用链,也就是整个调用链的起点。因为在分布式系统中,一个请求通常会经过多个进程和服务,每个服务都可能包含多个调用链,如果没有根调用链作为起点,那么就无法将这些调用链关联起来,从而无法准确地追踪和分析整个请求的调用链,影响了分布式系统的调试和性能分析。如果当前调用链有父调用链,则需要将第一个参数设置为父调用链的traceID,从而将当前调用链与父调用链关联起来,形成完整的调用链。
链路数据表
如下:
链路数据:
K 含义
i 入口标识符,最多8个十六进制字符
i2 口的别名标识符,最多8个十六进制字符(别名标识符)
s 链路签名,最多8个十六进制字符
r 入口签名,入口 IP 地址,在 TraceId 前缀不是 Java 入口 IP 的时候生成
t 线上全链路压测使用,值为 1 时表示这个链路是压测链路,使用了压测数据
tchain 线上全链路压测使用,记录压测的业务链路
u 在 tbsession 的埋点,由业务在前端传入的 UserId
w 前端染色埋点,由无线前端生成的ID
o 由业务在前端传入的用户子帐号信息
em 在 BUC Filter 的埋点,内部系统的操作员工号
ip 在 BUC Filter 的埋点,客户端IP
scm_project 二套环境项目标识,HSF、Notify 等都需要识别