写点什么

调用链系列四:调用链上下文传递

  • 2020-02-12
  • 本文字数:3328 字

    阅读完需:约 11 分钟

调用链系列四:调用链上下文传递

在之前的调用链系列文章中,我们已经对调用链进行了详细介绍,相信大家已经对调用链技术有了基本的了解。


其实,在调用链的绘制过程中,调用链上下文的传递非常值得关注。各个节点在获取上层上下文后生成新的上下文并向后传递。在传递过程中,上下文一旦丢失或出现异常就会导致调用链数据缺失,甚至可能会发生断裂。


本文主要讲述 UAV 中调用链上下文传递过程中的部分实现细节。




前言


在调用链的实现中,主要存在以下几种调用链上下文的传递方式:


  • 请求处理前到请求处理后的上下文传递;

  • 各个客户端调用间的上下文传递;

  • 各个服务间调用时的上下文传递。


在这三种情况中,上下文传递过程中所传递的信息以及遇到的问题会有所不同。


  • 在请求处理前后的上下文传递过程中,需要传递的信息一般包括 traceID、 spanID、请求开始的时间以及部分请求参数等。相关代码可能会因为异步执行导致上下文面临异步线程传递的问题。

  • 在客户端调用间及服务间调用中,需要传递的上下文信息一般只包括 traceID 和 spanID。但客户端调用之间的上下文传递可能会遇到跨线程池传递的问题,服务间调用则存在跨应用传递的问题。


因此,我们把今天所讲的上下文传递划分为以下四种场景进行分析:


1、在同一线程内传递


2、跨线程池传递


3、异步线程传递


4、跨应用传递


为了更好地阐述这四种场景,我们假设存在以下业务调用过程:


1550129281876017593.png


假设某次请求首先进入服务 A,在服务 A 的业务代码中发起了一次 JDBC 请求,访问了一次数据源;然后又通过 httpClient(同步,异步)发起了一次 http 访问并返回相应结果。


数字表示所在点存在调用链上下文信息的获取。在大多数的相邻点之间都会涉及到调用链上下文的传递。


例如,从 2 点到 3 点就是请求前和请求后的上下文传递,从 3 点到 4 点就是两次客户端调用间的上下文传递,从 4 点到 5 点就是服务间的上下文传递。下面我们将在不同的场景下说明各点之间的上下文传递过程。

1.在同一线程内的上下文传递

这种场景比较常见,也是最简单的场景。


假设上述模拟流程中全部为同步操作,业务代码中不涉及任何的线程池(数据库连接池不影响)及异步操作,那么服务 A 中调用链的相关代码均会在同一个线程中执行。


说到这里,想必大家都会想到使用 ThreadLocal 便可以解决。使用 ThreadLocal 的确可以解决同线程中的参数共享传递问题。在 UAV 中,一般两次客户端调用之间的上下文传递都直接使用 ThreadLocal(其实并不是原生的 ThreadLocal,后文会有所介绍),传递过程如下:


1550129297901025915.jpg


但是很多时候,业务代码中经常会涉及到异步或者提交线程池的操作,此时单单使用 ThreadLocal 便无法满足相应的需求。下面我们就来讨论有关含有线程池操作和异步请求的上下文传递问题。



2.跨线程池的上下文传递

首次我们来看一下跨线程池上下文传递问题。


假设上述的业务场景中在进行 JDBC 操作时,当前线程仅负责将 JDBC 操作提交到线程池中,那么此时上下文信息从 1 点传递到 2 点就会遇到跨线程池的问题,此时使用 ThreadLocal 无法上下文信息的传递。


当然有的同学可能会说用 InheritableThreadLocal。但是提交线程和线程池线程本身并不存在父子关系,因此 InheritableThreadLocal 也是无法完成跨线程池的上下文传递的。


为了解决这个问题,我们使用了阿里开源的跨线程池的 ThreadLocal 组件:transmittable-thread-local(以下简称 TTL,具体的实现方式有兴趣的同学可以去了解下https://github.com/alibaba/transmittable-thread-local)


通过该组件可以增强 ThreadLocal 的功能实现跨线程池的传递。以下是 github 中 TTL 的使用示例:


TransmittableThreadLocal


parent.set(“value-set-in-parent”);


Runnable task =new Task(“1”);


// 额外的处理,生成修饰了的对象 ttlRunnable


Runnable ttlRunnable = TtlRunnable.get(task);


executorService.submit(ttlRunnable);


// Task 中可以读取,值是"value-set-in-parent"


String value = parent.get();


可以看到,想要 TTL 起作用,就需要将业务代码中的 runnable 更换为 TtlRunnable。为了实现对业务代码的零入侵,我们借助 javaagent 机制增加了一个针对 ThreadPoolExecutor 等一些 Eexecutor 的 ClassFileTransformer,将提交到线程池中的 Runnable 和 Callable 包装成相应的 TtlRunnable 和 TtlCallable,这样就实现了在不修改业务代码的情况下完成跨线程池的上下文传递。


另外,由于 TTL 具备 ThreadLocal 的所有特性,因此 UAV 的上下文传递过程中用到的 ThreadLocal 均是 TTL。

3.异步线程中上下文传递

看完上面的跨线程池操作,我们再来看一下异步线程的问题。


假设在上述模拟场景中,我们使用异步 HttpClient 发送了一个异步的 Http 请求。由于是异步操作,4 点的代码和 7 点的代码(这里 7 点的上下文是从 4 点中获取的属于请求前后的上下文获取场景)实际上会在不同的线程中执行,导致 7 点无法获取 4 点放入 ThreadLocal 中的上下文数据,进而导致调用链的数据丢失。


为了解决这个问题,在 UAV 中我们同时使用了字节码改写和动态代理技术。关键在于目标劫持函数的选择,需要能够获取到异步线程的回调对象。


下面以异步 HttpClient 为例介绍 UAV 中异步线程上下文的传递过程。


在异步 HttpClient 中,我们劫持的是 InternalHttpAsyncClient 类的 execute()方法,该方法声明如下:


1550129319021009666.png


一般情况下,异步的使用方式为传入一个 callback 接口对象,在 callback 中实现相应的异步逻辑;或者使用返回的 Future 接口对象的 get()方法实现一种异步转同步的操作。


为了能够在相应的地方获取到调用链的上下文,我们首先通过改写字节码的方式,在方法执行前生成调用链的上下文信息;然后对 FutureCallback 接口做动态代理,同时将生成的上下文信息传入到代理对象中,并替换原来的 callback 对象。


这样当异步请求返回调用 callback 接口时,实际上拿到的是我们的代理对象,此时也就完成了异步线程中上下文的传递过程,具体过程如下:


1550129333027072293.jpg


为了支持通过 get()方法的异步转同步操作,在这里我们也对返回的 Future 接口做了动态代理来完成上下文的传递。

4.跨应用上下文传递

说完应用内的上下文传递过程,我们来看一下跨应用的上下文传递问题。


跨应用的场景也是比较常见的。在这种场景下,上下文传递的思路一般是将上下文的信息按照一定的协议反序列化,然后放入到请求的传输报文中;在下游服务中劫持请求,获取其中的信息完成上下文的传递。在整个处理过程中,不对应用报文解析造成任何影响。


常见的传输协议中如 HTTP 协议,Dubbo 的 RPC 协议,RocketMQ 的 MQ 协议等。这些协议一般会含有类似于头信息的结构,用于表示本次请求的额外信息。


我们恰好可以利用这一点,将上下文信息放入其中传输给下游服务,完成上下文的传递。


下面我们仍然以异步 HttpClient 来介绍 UAV 跨应用上下文的传递过程。


之前我们说过,在异步 HttpClient 中,我们劫持的是 execute()方法。在这个方法中,我们可以拿到 HttpAsyncRequestProducer 接口对象,具体接口如下:


1550129417726034155.png


通过其中的 generateRequest()方法,我们就可以拿到本次请求将要发送的 request 对象,利用 request 的 setHeader()方法,将调用链的上下文信息放入 Header 中传入下游。


这里的上下文一般比较简单,基本上都是由 traceID 和 spanID 的字符串构成,传输成本也不高。


至于下游服务中如何解析该上下文,实际上之前的调用链系列中有谈到,就是借助 UAV 的中间件增强框架(MOF),在服务端劫持请求对应的 request 对象,然后直接从其头信息中获取即可。


1550129392978049116.jpg


其他的 RPC 或者 MQ 等协议,在 UAV 中均是采用这种方式完成,只是具体的 API 和劫持点有所不同。


例如,Dubbo 远程调用过程中使用是其中的 RpcContext,而 RocketMQ 则是放入到了 msg 的 UserProperty 中。感兴趣的同学可以到 UAVStack(https://github.com/uavorg/uavstack)中查看相关的源码。


总结


了解这些上下文的传递过程后,大家便可以基于调用链实现更为强大的功能。UAV 中,调用链和日志关联功能就是通过劫持日志输入部分的相关代码,获取调用链上下文,然后将 traceID 输出到业务日志中来实现的。


大家也可以自己在业务代码中尝试获取调用链的上下文,将业务数据与调用链数据打通,方便数据统计和问题排查。


本文转载自宜信技术学院网站。


原文链接:http://college.creditease.cn/detail/221


2020-02-12 15:251462

评论

发布
暂无评论
发现更多内容

【直播回顾】Hello HarmonyOS应用篇第六课——短视频应用开发

HarmonyOS开发者

HarmonyOS

Linux开发_网络编程、网络通信介绍

DS小龙哥

6月月更

TiDB Cloud 上线 Google Cloud Marketplace,以全新一栈式实时 HTAP 数据库赋能全球开发者

PingCAP

TiDB

10 个派上用场的 Flutter 小部件

坚果

6月月更

使用 ViroReact 开发增强实现应用的一个具体例子

汪子熙

AR React 增强现实 6月月更

Flink CDC + Hudi 海量数据入湖在顺丰的实践

Apache Flink

大数据 flink 编程 流计算 实时计算

Nacos配置中心实战,盘古微服务开发标配组件

码农大熊

微服务架构 nacos 盘古开发框架 分布式开发

【高并发】彻底理解Nginx限流机制与实战

冰河

并发编程 多线程 高并发 异步编程 6月月更

明道云上榜2022年中国信创行业办公软件排行榜

明道云

物联网低代码平台如何使用操作日志?

AIRIOT

物联网 低代码开发 低代码平台 物联网关

Plugsched 实战解读:如何在不中断业务时对 Linux 内核调度器热升级? | 龙蜥技术

OpenAnolis小助手

Linux 开源 内核 调度 Plugsched

InfoQ 极客传媒 15 周年庆征文| 迁移 Eureka 到 Nacos 之双注册双订阅模式

4ye

架构 nacos Eureka springcloudAlibaba InfoQ极客传媒15周年庆

JavaScript寄生式组合继承

大熊G

JavaScript 前端 6月月更

使用 KubeKey 搭建 Kubernetes/KubeSphere 环境的“心路(累)历程“

胡说云原生

Kubernetes KubeSphere KubeKey

Linux中有趣的命令:cowsay,会说话的牛!

wljslmz

Linux 6月月更 cowsay

盘点现有开源软件许可合规工具

开源社

GameFi新的启程,AQUANEE将于6.9日登陆Gate以及BitMart

西柚子

Web Service进阶(八)BASE64Decoder小解

No Silver Bullet

6月月更 BASE64Decoder

WordPress 版本更新

海拥(haiyong.site)

WordPress 6月月更

Java培训多线程+List分段解决批量更新太慢

@零度

List 多线程 JAVA开发

Android 产生ANR后的Trace文件的解析

北洋

android 6月月更

保姆级教程:如何成为Apache Linkis文档贡献者

康月牙

Apache GitHub 教程 文档 Linkis

C#入门系列(九) -- 方法使用

陈言必行

C# 6月月更

关于在线帮助中心你需要思考以下几个问题

小炮

苹果称M2比intel i5强26倍 虚假营销的实情揭晓!

科技之家

柴云鹏:创新能力的培养至关重要|OceanBase 数据库大赛访谈

OceanBase 数据库

oceanbase 数据库大赛

IPO,联结一切的桥梁

鼎道智联

【Python技能树共建】lambda 表达式

梦想橡皮擦

6月月更

文档书写规范

甜甜的白桃

文档 6月月更

版式设计三大原则

源字节1号

软件开发 小程序开发

开发一个软件应用程序需要多少钱?

开源直播系统源码

软件开发 定制开发 直播源码

调用链系列四:调用链上下文传递_区块链_朱文强_InfoQ精选文章