写点什么

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

  • 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:251652

评论

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

秒懂 Java 的三种代理模式

看完这篇文章,你也可以手写MyBatis部分源码(JDBC)

不收藏你就后悔吧!费了三天才从GitHub上扒下的阿里Java优化笔记

保洁阿姨分享:腾讯架构师JDK源码笔记,13万字,带你飙向实战

来自网络资源资产管理的灵魂拷问

鲸品堂

网络 资源 运营商

东京奥运会与网络安全背后的速度博弈!

郑州埃文科技

【云洲智造】直播间下午4:30准时开播!

工业互联网

文档内容结构化在百度文库的技术探索

百度Geek说

百度 大前端

以1敌10不是梦,Spring Boot企业级真实应用案例

博文视点Broadview

还在用Jenkins?试试Gitlab的CI/CD功能吧,贼带劲!

保安小王分享:四面字节跳动,终拿Offer,只有努力,方能成功

小透明学弟的华为上岸之路

程序员鱼皮

Java c++ Python 大前端 后端

如何基于磁盘 KV 实现 Bitmap

Kvrocks

redis BitMap storage KV存储引擎

模块三作业

NewBranSTONE

架构实战营

Lazada首届技术开放日开麦在即 共享技术创新最佳实践

EMQ X Cloud 正式支持 Microsoft Azure 平台,助力企业出海业务

EMQ映云科技

azure 云端 云上数据 emq

手把手教你实现Android编译期注解

vivo互联网技术

android 注解 sdk

2021,你还在写“赤裸裸”的API吗?

聊一聊在阿里做了 8 年研发后,我对打造大型工程研发团队的再思考

尔达Erda

开源 云原生 研发管理 PaaS 研发

科技监管能源运作?智慧能源从光热发电技术开始描述

一只数据鲸鱼

数据可视化 智慧能源 光热发电

Cypress 自动化测试

admin

自动化测试 Cypress 测试 单元测试 UI测试

电脑里的视频被误删了可以用EasyRecovery恢复吗?

淋雨

EasyRecovery 文件恢复 硬盘数据恢复

最壕逆天改命:18名Java程序员凭阿里P8笔记,同时斩获一线大厂offer

Java架构师迁哥

Unity ML-agents 参数设置解明

行者AI

银行4.0的AI世界——开启算法力的时代

索信达控股

哔哩哔哩B站视频下载器推荐(简单又好用)

资源君

工具 分享 哔哩哔哩 b站视频下载 教程分享

架构训练营模块三作业

晨晨

架构训练营

Abp太重了?轻量化Abp框架

Patronum

学习 程序员 架构 框架 Abp

财务或类财务系统数值精度设计

路边水果摊

数字 财务 精度 数值

Java虚拟机之CMS垃圾收集器

基于 Golang 构建高可扩展的云原生 PaaS(附 PPT 下载)

尔达Erda

开源 云原生 数字化转型 PaaS 数字化

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