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);
@Override
public 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 {
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setterMethodMap.get(method)) {
@Override
protected Object run() throws Exception {
// 调用 SynchronousMethodHandler#invoke
return 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 {
@Override
public 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>() {
@Override
public Void call() throws Exception {
actual.run();
return null;
}
});
this.parentThreadState = HystrixRequestContext.getContextForCurrentThread();
}
@Override
public void run() {
HystrixRequestContext existingState = HystrixRequestContext.getContextForCurrentThread();
try {
// set the state of this thread to that of its parent
HystrixRequestContext.setContextOnCurrentThread(parentThreadState);
// execute actual Callable with the state of the parent
try {
actual.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
} finally {
// restore this thread back to its original state
HystrixRequestContext.setContextOnCurrentThread(existingState);
}
}
}
通过自定义 HystrixConcurrencyStrategy
解决透传的问题。
参考:Spring Cloud Sleuth 实现 SleuthHystrixConcurrencyStrategy
。 SleuthHystrixConcurrencyStrategy
相关代码如下:
public class SleuthHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
private final Tracing tracing;
private final SpanNamer spanNamer;
// 被装饰者
private HystrixConcurrencyStrategy delegate;
@Override
public <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 {
......
@Override
public <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;
}
@Override
public 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 进行配置即可。