Spring Cloud中基于Sleuth的参数透传功能探索

2024-02-12 14:40

本文主要是介绍Spring Cloud中基于Sleuth的参数透传功能探索,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

一.需求

微服务环境,有A,B,C,D四个服务,调用关系为:A->B->C->D。用户在A的页面选择当前“语言”环境为“英文”,在某些业务场景下,其它几个服务需获取到这个“语言”信息。

二.分析

这个需求还是很简单的,类似于“击鼓传花”:当前服务从上一个服务中获取参数,并传给下一个服务。个人感觉基本上所有的RPC框架都会遇到这个问题,只是以前SOA架构下,服务层级比较少,将“语言”、“登陆”等附加信息放在参数列表中并不会带来太多工作量,所以这个问题并不是太突出。而引入了微服务架构思想后,服务调用层级急剧增长,这就需要一个更加优雅的方式来解决附加信息的传递问题。

三.方案探索

3.1 方案一:参数放在接口参数列表中

优点:思路简单,开发没有学习成本

缺点

  • 代码高度耦合:附加信息却要每个接口都显式维护
  • 升级困难:如果将来再加一个参数,所有层级的接都要改动
  • 引起迷惑:如果B服务的逻辑不需要“语言“参数,但是因为D需要,它也必须维护
  • 太傻了,Big不够

思考:微服务之间绝大多数情况是通过HTTP调用的,HTTP的header中也可以放参数信息。这样,接口参数中就不用维护这些附加信了。


3.2 方案二:参数放在httpRequest的header中

实现
1.自定义一个Filter,获取Request中自己需要的附加信息,
2.将这些信息放入ThreadLocal中,
3.实现feign.Client(这里先忽略RestTemplate)的execute()方法,将附件信息在调用下一层服务前塞入request的header中

优点:参数解耦

缺点:如果B在获取到附加信息后,新起了一个线程”T1“来调用服务C,这时T1就无法从HhreaLocal拿到附加信息了

思考:

  1. 如果我知道怎么用无侵入的方式,在当前线程”T”创建子孙线程”T1”、”T1-1”时,将数据传给后代,就能解决这个问题了
  2. 微服务调用链框架Sleuth的核心功能即是跟踪一次请求从A到D的全过程,它肯定支持多线程调用下的traceId的传递。因此,我可以复用Sleuth的相关功能夹带私货

3.3 方案三:修改Sleuth源码,将附加信息跟着TraceId一起往后传递

优点

  • 原理简单,不用考虑底层实现
  • 不用考虑兼容性等问题,Sleuth都已经实现好
  • 快(对,就是这一个字)
    缺点
  • 维护困难,很容易忘记以前修改了哪些地方,更别提移交给别人维护了
  • 升级困难,以后每次Spring或者Sleuth升级,都要重新下载源码修改

思考:
目前获取参数的问题解决了,用Filter,只剩下保存并传给下一层的问题
既然Sleuth已经解决了多线程下traceId的传递问题,那我就直接用traceId来解决我的问题

3.4 方案四:充分利用traceId

实现

  • 自定义Filter(优先级要低于TraceFilter,因为你要获取TraceFilter里的traceId),拿到traceId和附加信息后,将它们存在本地缓存中,traceId为key,附加信息为value
  • 参考方案二的实现3。重写execute()方法,获取当前线程的traceId(这个Sleuth有接口,不再介绍),然后再通过traceId去本地缓存中拿到附加信息,放进Request的header中

优点:拥有上述方案所有的优点,解决上述方案所有缺点

缺点:看着很完美,但是你忽略了一件事:Sleuth要想传递自己的traceId,想必它已经重写了execute()方法(肯定的,那就是TraceFeignClient),你要想用,那就要想办法在复用TraceFeignClient.execute()的同时,将自己的私货带进去

3.5 方案五:重写TraceFeignClient

实现:有时候,改动源码并不需要直接在原有包里修改。比如:A->B->C->D,如果你要修改C的源码,那就将AB源码也copy出,作为A1,B1,C#,然后重写组件的入口,将组件加载顺序变为:A1->B1->C#->D,即可达到重写源码的目的。这时候注意的是,加载A1的条件必须跟加载A的相反。具体可参考我之前重写Consul的入口例子,示例代码如下

@ConditionalOnExpression("${spring.cloud.consul.ribbon.enabled:true}==false")
public class MyRibbonConsulAutoConfiguration {}// 原有入口:
@ConditionalOnProperty(value = "spring.cloud.consul.ribbon.enabled", matchIfMissing = true)
public class RibbonConsulAutoConfiguration {}

综上,可以重写TraceFeigClient的入口 TraceFeignClientAutoConfiguration->TraceFeignObjectWrapper>TraceFeignClient,即可达到自己的目的.

优点:感觉事儿基本就成了

缺点:配置为false生效,使用者会觉得比较怪,Sleuth仿佛知道别人会这么干似的,它的类的访问权限基本都是default,为了copy过来的几个类能正常编译通过,你还要再copy九个它们的依赖类,程序太丑

思考:突然想起来,还有一种改代码的方式叫字节码替换,如果我能在程序启动的时,将我的execute()直接替换掉Sleuth的execute(),就一劳永逸了

3.6 方案六:字节码替换代源码修改

优点:高大上,不在源码级替换,却在字节码级替换,虚虚实实

缺点:没这么干过,总觉得说着容易做着难

思考:基本上觉得方案五已经能解决问题了。本着精益求精的态度,去技术群里问了下,很快有大神发来Demo,看过代码后顿觉惭愧:我一直在想怎么重写TraceFeignClient的execute(),其实这个execute()真正做http请求时,调用的是feign.Client的另外一个实现类,注意那句”this.delegate.execute”,只要想办法用自己的Client替换掉delegate即可


    private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass());private final Client delegate;@Overridepublic Response execute(Request request, Request.Options options) throws IOException {String spanName = getSpanName(request);Span span = getTracer().createSpan(spanName);if (log.isDebugEnabled()) {log.debug("Created new Feign span " + span);}try {AtomicReference<Request> feignRequest = new AtomicReference<>(request);spanInjector().inject(span, new FeignRequestTextMap(feignRequest));span.logEvent(Span.CLIENT_SEND);addRequestTags(request);Request modifiedRequest = feignRequest.get();if (log.isDebugEnabled()) {log.debug("The modified request equals " + modifiedRequest);}Response response = this.delegate.execute(modifiedRequest, options);logCr();return response;} catch (RuntimeException | IOException e) {logCr();logError(e);throw e;} finally {closeSpan(span);}}

3.7 方案七:替换掉TraceFeigClient的delegate即可

实现:通过再次认真Debug源码知道,TraceFeignClient默认会加载你的Client实现类作为delegate(汗!),因此你只要直接实现feign.Client接口即可。我偷懒了一把,自己写个实现类,直接复用了LoadBalancerFeignClient.execute()
优点:基本什么都有了吧
缺点:如果你以为只是简单地重写个execute()就行,那就大错特了。因为TraceFeignClient直接用了你的方法post过去,因此你要想办法把ribbon手动集成进来。如果不觉得麻烦的话,可以好好看下TraceFeignClient怎么生成Client的实例:TraceFeignObjectWrapper.wrap(Object bean)

思考:既然你可以在程序里获取到trace和span,那为何不将你的信息放到span里呢。如果span中能放点额外信息就好了,就不用自己写这么多东西。经大神提醒,Sleuth中有个baggage可以试试

3.8 方案八:使用baggage

实现:获取参数的方式不变,取得的参数放在baggage中

优点:简单,支持RestTemplate调用的情况,跟其他组件兼容性好

缺点:Sleuth的缺点

四.项目源码

4.1 基于slueth的参数透传插件

Github地址:https://github.com/bishion/sleuth-plugin

简介:微服务下使用,调用过程中用户信息,页面语言信息的透传
使用方式

bizi:sleuth: config:headers: lang_info #如果由多个,逗号隔开.这里配置从filter里需要获取的headerName

调用方式

@Service
public class SessionInfoService {@Resourceprivate SessionInfoOperator sessionInfoOperator;public String getLangInfo(){return sessionInfoOperator.getSessionInfo("lang_info");}public void setUserId(){sessionInfoOperator.setSessionInfo("user_id","bishion");}
}

五.留下的坑

  1. Sleuth通过LazyTraceExecutor解决多线程下的问题,但是它并没有解决给手动创建的Thread传递信息的问题
  2. 有机会试试java字节码替换怎么操作
  3. Sleuth如何重写RestTemplate的
  4. TraceFeignClient怎么生成Client的实例

六.后记

因为附加信息的传递在RPC中扮演了很重要的角色,我潜意识里觉得,肯定会有更加简洁的方法或者框架我还没有了解到。希望各位各位读者老师能不吝珠玉,批评指正

转载于:https://my.oschina.net/xiaominmin/blog/3049732

这篇关于Spring Cloud中基于Sleuth的参数透传功能探索的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/702797

相关文章

SpringBoot中HTTP连接池的配置与优化

《SpringBoot中HTTP连接池的配置与优化》这篇文章主要为大家详细介绍了SpringBoot中HTTP连接池的配置与优化的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录一、HTTP连接池的核心价值二、Spring Boot集成方案方案1:Apache HttpCl

Spring Boot项目打包和运行的操作方法

《SpringBoot项目打包和运行的操作方法》SpringBoot应用内嵌了Web服务器,所以基于SpringBoot开发的web应用也可以独立运行,无须部署到其他Web服务器中,下面以打包dem... 目录一、打包为JAR包并运行1.打包为可执行的 JAR 包2.运行 JAR 包二、打包为WAR包并运行

Java进行日期解析与格式化的实现代码

《Java进行日期解析与格式化的实现代码》使用Java搭配ApacheCommonsLang3和Natty库,可以实现灵活高效的日期解析与格式化,本文将通过相关示例为大家讲讲具体的实践操作,需要的可以... 目录一、背景二、依赖介绍1. Apache Commons Lang32. Natty三、核心实现代

Spring Boot 常用注解整理(最全收藏版)

《SpringBoot常用注解整理(最全收藏版)》本文系统整理了常用的Spring/SpringBoot注解,按照功能分类进行介绍,每个注解都会涵盖其含义、提供来源、应用场景以及代码示例,帮助开发... 目录Spring & Spring Boot 常用注解整理一、Spring Boot 核心注解二、Spr

SpringBoot实现接口数据加解密的三种实战方案

《SpringBoot实现接口数据加解密的三种实战方案》在金融支付、用户隐私信息传输等场景中,接口数据若以明文传输,极易被中间人攻击窃取,SpringBoot提供了多种优雅的加解密实现方案,本文将从原... 目录一、为什么需要接口数据加解密?二、核心加解密算法选择1. 对称加密(AES)2. 非对称加密(R

详解如何在SpringBoot控制器中处理用户数据

《详解如何在SpringBoot控制器中处理用户数据》在SpringBoot应用开发中,控制器(Controller)扮演着至关重要的角色,它负责接收用户请求、处理数据并返回响应,本文将深入浅出地讲解... 目录一、获取请求参数1.1 获取查询参数1.2 获取路径参数二、处理表单提交2.1 处理表单数据三、

java变量内存中存储的使用方式

《java变量内存中存储的使用方式》:本文主要介绍java变量内存中存储的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、介绍2、变量的定义3、 变量的类型4、 变量的作用域5、 内存中的存储方式总结1、介绍在 Java 中,变量是用于存储程序中数据

macOS Sequoia 15.5 发布: 改进邮件和屏幕使用时间功能

《macOSSequoia15.5发布:改进邮件和屏幕使用时间功能》经过常规Beta测试后,新的macOSSequoia15.5现已公开发布,但重要的新功能将被保留到WWDC和... MACOS Sequoia 15.5 正式发布!本次更新为 Mac 用户带来了一系列功能强化、错误修复和安全性提升,进一步增

如何合理管控Java语言的异常

《如何合理管控Java语言的异常》:本文主要介绍如何合理管控Java语言的异常问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、介绍2、Thorwable类3、Error4、Exception类4.1、检查异常4.2、运行时异常5、处理方式5.1. 捕获异常

Spring Boot集成SLF4j从基础到高级实践(最新推荐)

《SpringBoot集成SLF4j从基础到高级实践(最新推荐)》SLF4j(SimpleLoggingFacadeforJava)是一个日志门面(Facade),不是具体的日志实现,这篇文章主要介... 目录一、日志框架概述与SLF4j简介1.1 为什么需要日志框架1.2 主流日志框架对比1.3 SLF4