code.7z
一、问题描述
在使用 open-feign 作为 RPC 调用组件,并开启 hystrix 支持(feign.hystrix.enabled=true)时,会出现 请求头丢失的问题。
1.1、问题原因
假设当前存在两个服务 feign-client 和 feign-server ,现在有一次请求如下:
本次请求由 request1 + request2 组成。request1 和 request2 属于两个不同的请求,在某些业务需求中需要将 request1 header 头部中部分信息通过 request2 进行传递。
此时进行传递的方式有两种
- 直接通过方法参数进行传递(不优雅,甚至是繁琐)
- 通过统一的方式进行设置,借助 Feign 提供了过滤器
RequestInterceptor。在发起 HTTP 请求前, Feign 会先执行所有的
RequestInterceptor#apply方法
使用 RequestInterceptor 进行 header 参数统一透传处理相关代码
/*** <p> Feign Interceptor </p>** @Author 彳失口亍*/public class FeignInterceptor implements RequestInterceptor {private Logger logger = LoggerFactory.getLogger(FeignInterceptor.class);@Overridepublic void apply(RequestTemplate requestTemplate) {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes == null) {logger.info("[新的线程查询不到上下文信息]");return;}// 从 ThreadLocal 中获取 Request 信息HttpServletRequest request = attributes.getRequest();String token= request.getHeader("token");if (!StringUtils.isEmpty(token)) {requestTemplate.header("token", token);}}}
在 feign 开启了 hystrix 支持,request2 执行时序图如下
① HystrixInvocationHandler#invoke 相关代码如下
final class HystrixInvocationHandler implements InvocationHandler {@Overridepublic Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setterMethodMap.get(method)) {@Overrideprotected Object run() throws Exception {// 调用 SynchronousMethodHandler#invokereturn HystrixInvocationHandler.this.dispatch.get(method).invoke(args);}}return hystrixCommand.execute();}}
HystrixCommand中任务的执行由 Future 模式实现的线程池中的线程来完成。
上述代码中 HystrixCommand 中传入的执行任务会被线程池的某个线程执行。
换句话说,request1 和 request2 的请求是在不同线程进行处理的,request2 是一个新发起的请求,并且在一个新的线程中,无法共享 reqeust1 ThreadLocal 中相关的数据。
所以在使用 feign 进行 RPC 调用的时候, request1 中 head 信息无法通过 RequestInterceptor 的方式统一进行传递。
1.2、扩展:为什么开启 feign hystrix 支持 链路追踪信息能够通过 header 进行透传。
在测试过程中,发现 head 参数无法透传,但是 sleuth 链路信息却能够进行传递。
通过源码 TracingFeignClient#client 能够看出端疑
final class TracingFeignClient implements Client {@Overridepublic Response execute(Request request, Request.Options options) throws IOException {Map<String, Collection<String>> headers = new HashMap<>(request.headers());// span 创建Span span = handleSend(headers, request, null);Response response = null;Throwable error = null;try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) {// modifiedRequest(request, headers) 构建新的请求参数response = this.delegate.execute(modifiedRequest(request, headers), options);return response;}catch (IOException | RuntimeException | Error e) { ...... }finally {handleReceive(span, response, error);if (log.isDebugEnabled()) {log.debug("Handled receive of " + span);}}}}
span 相关 内容是在 Feign Client 中才进行构建的,并不是从上一个请求 ThreadLocal 中获取的。
二、解决 RequestInterceptor 无法统一透传
问题本质:feign 开启 hystrix 功能,而 hystrix 默认使用线程隔离,所以feign 发起信息的请求必定是一个新的线程来发起请求处理。
方案一:修改 hystrix 隔离策略-信号量(不推荐)
直接修改配置文件增加配置
#hystrix 隔离策略-信号量隔离hystrix.command.default.execution.isolation.strategy = SEMAPHORE
方案二:自定义 HystrixConcurrencyStrategy
Hystrix 线程隔离为:线程隔离
分析
Feign 开启 Hystrix 发起 RPC 调用时,Hystrix 通过 HystrixCommand 包装了 RPC 调用,然后从其中的线程池中获取一个线程进行执行操作,线程池的从 HystrixConcurrencyStrategy 中获取,
Feign RPC 调用逻辑由 HystrixConcurrencyStrategy#wrapCallable 封装成 Callable<Void>,然后借由 HystrixContextRunnable#run 执行调用,HystrixContextRunnable 相关代码如下:
public class HystrixContextRunnable implements Runnable {private final Callable<Void> actual;private final HystrixRequestContext parentThreadState;public HystrixContextRunnable(Runnable actual) {this(HystrixPlugins.getInstance().getConcurrencyStrategy(), actual);}public HystrixContextRunnable(HystrixConcurrencyStrategy concurrencyStrategy, final Runnable actual) {this.actual = concurrencyStrategy.wrapCallable(new Callable<Void>() {@Overridepublic Void call() throws Exception {actual.run();return null;}});this.parentThreadState = HystrixRequestContext.getContextForCurrentThread();}@Overridepublic void run() {HystrixRequestContext existingState = HystrixRequestContext.getContextForCurrentThread();try {// set the state of this thread to that of its parentHystrixRequestContext.setContextOnCurrentThread(parentThreadState);// execute actual Callable with the state of the parenttry {actual.call();} catch (Exception e) {throw new RuntimeException(e);}} finally {// restore this thread back to its original stateHystrixRequestContext.setContextOnCurrentThread(existingState);}}}
通过自定义 HystrixConcurrencyStrategy 解决透传的问题。
参考:Spring Cloud Sleuth 实现 SleuthHystrixConcurrencyStrategy。 SleuthHystrixConcurrencyStrategy 相关代码如下:
public class SleuthHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {private final Tracing tracing;private final SpanNamer spanNamer;// 被装饰者private HystrixConcurrencyStrategy delegate;@Overridepublic <T> Callable<T> wrapCallable(Callable<T> callable) {if (callable instanceof TraceCallable) {return callable;}Callable<T> wrappedCallable = this.delegate != null? this.delegate.wrapCallable(callable) : callable;if (wrappedCallable instanceof TraceCallable) {return wrappedCallable;}return new TraceCallable<>(this.tracing, this.spanNamer, wrappedCallable,HYSTRIX_COMPONENT);}}
上述方法 SleuthHystrixConcurrencyStrategy#wrapCallable 用来构建线程执行的 Runnable(HystrixContextRunnable) 逻辑模块的。所以在执行该操作时,并未切换线程,此时就可以在这里作文章,
在线程执行逻辑单元中,把 request1 中需要被传递的信息,直接赋值到即将发起 request2 的线程的逻辑执行单元中。
SleuthHystrixConcurrencyStrategy使用了装饰者模式,SleuthHystrixConcurrencyStrategy中构建的逻辑执行单元包裹了一层被装饰者的执行逻辑单元
实现
一个新的类 CustomerFeignHystrixConcurrencyStrategy ,参考SleuthHystrixConcurrencyStrategy 代码,修改其中 的 wrapCallable 方法相关代码如下:
/*** <p> 自定义 Hystrix 策略 </p>** @Author 彳失口亍*/public class CustomerFeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {......@Overridepublic <T> Callable<T> wrapCallable(Callable<T> callable) {RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();return new WrappedCallable<>(callable, requestAttributes);}static class WrappedCallable<T> implements Callable<T> {private final Callable<T> target;private final RequestAttributes requestAttributes;public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) {this.target = target;this.requestAttributes = requestAttributes;}@Overridepublic T call() throws Exception {try {RequestContextHolder.setRequestAttributes(requestAttributes);return target.call();} finally {RequestContextHolder.resetRequestAttributes();}}}}
关键在于构建的 WrappedCallable 对象中存放了 request1 线程上下文中的 RequestAttributes 对象,使得 request2 开启的新线程获取到的 RequestAttributes 为 request1 中的值,达到参数传递的效果。
上述代码与 SleuthHystrixConcurrencyStrategy 组成的线程逻辑单元结构如下:
SleuthHystrixConcurrencyStrategy中的 Callable 和CustomerFeignHystrixConcurrencyStrategy中的WrappedCallable也是装饰者模式。
使用

在 feign client 进行配置即可。
