# 问题描述

在使用 open-feign 作为 RPC 调用组件,并开启 hystrix 支持(feign.hystrix.enabled=true)时,会出现 请求头丢失的问题。

# 问题原因

Feign Hystrix Support

假设当前存在两个服务 feign-clientfeign-server ,现在有一次请求如下:

本次请求由 request1 + request2 组成。request1request2 属于两个不同的请求,在某些业务需求中需要将 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 中传入的执行任务会被线程池的某个线程执行。

换句话说,request1request2 的请求是在不同线程进行处理的,request2 是一个新发起的请求,并且在一个新的线程中,无法共享 reqeust1 ThreadLocal 中相关的数据。

所以在使用 feign 进行 RPC 调用的时候, request1 中 head 信息无法通过 RequestInterceptor 的方式统一进行传递。

# 扩展:为什么开启 feign hystrix 支持 链路追踪信息能够通过 header 进行透传。

Spring Cloud Sleuth 原理-feign埋点

在测试过程中,发现 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 配置

#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 实现 SleuthHystrixConcurrencyStrategySleuthHystrixConcurrencyStrategy 相关代码如下:

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 开启的新线程获取到的 RequestAttributesrequest1 中的值,达到参数传递的效果。

上述代码与 SleuthHystrixConcurrencyStrategy 组成的线程逻辑单元结构如下:

SleuthHystrixConcurrencyStrategy 中的 Callable 和 CustomerFeignHystrixConcurrencyStrategy 中的 WrappedCallable 也是装饰者模式。

# 使用

在 feign client 进行配置即可。

精彩内容推送,请关注公众号!
最近更新时间: 4/30/2020, 7:49:57 AM