TransmittableThreadLocal和Hystrix共用引发的血案
前段时间为了解决feign超时重试太多次的问题(后续可以分享出来),一言不合就把feign.hystrix.enabled
开关给打开了。一系列相关的问题便接踵而至,眼看就要失业了,整夜未眠,从现象出发,一个一个击破,最终算是解决了,工作保住了。
现象一: 用Feign调用的服务中获取不到在FeignInterceptor添加到Header中的值
之前已经配置好了FeignInterceptor
,把该传递的头信息都放进了header,开关开启前还是好好的,一打开就出问题了。肯定是Hystrix
出了问题,于是搜索了一下Hystrix Feign传值
,结果一堆答案出现。
原来是因为我们使用的Hystrix策略
是THREAD
模式,每一个FeignClient
都会对应一个线程池,核心线程数配置了20个,在HystrixCommand
执行Feign
接口访问时,会在新的线程(会复用线程池中的)执行,这时原来的FeignInterceptor
里面是通过取RequestContextHolder
的上下文获取的值放置到FeignClient
的请求头里面,但是Hystrix
的执行线程并不会把父线程的RequestContextHolder
的信息同步进去,所以FeignInterceptor
里的代码并不能按计划完成。
修改方案也很简单,就是利用Hystrix
提供的并发策略配置接口HystrixConcurrencyStrategy
来自定义自己的线程执行过程,在线程执行前读取RequestContextHolder
的信息,然后在执行时将这个信息设置到线程的RequestContextHolder
中。代码如下:
/**
* Feign的Hystrix并发策略
*
* @author luodongseu
*/
@Component
@Slf4j
public class FeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
public FeignHystrixConcurrencyStrategy() {
try {
HystrixConcurrencyStrategy delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
if (delegate instanceof FeignHystrixConcurrencyStrategy) {
return;
}
HystrixCommandExecutionHook commandExecutionHook =
HystrixPlugins.getInstance().getCommandExecutionHook();
HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy =
HystrixPlugins.getInstance().getPropertiesStrategy();
// 打印日志
if (log.isDebugEnabled()) {
log.debug(
"Current Hystrix plugins configuration is [concurrencyStrategy [{}], eventNotifier [{}], metricPublisher [{}], propertiesStrategy [{}]]",
delegate,
eventNotifier,
metricsPublisher,
propertiesStrategy);
log.debug("Registering FeignWithHystrix Concurrency Strategy.");
}
HystrixPlugins.reset();
HystrixPlugins instance = HystrixPlugins.getInstance();
instance.registerConcurrencyStrategy(this);
instance.registerCommandExecutionHook(commandExecutionHook);
instance.registerEventNotifier(eventNotifier);
instance.registerMetricsPublisher(metricsPublisher);
instance.registerPropertiesStrategy(propertiesStrategy);
} catch (Exception e) {
log.error("Failed to register FeignWithHystrix Concurrency Strategy", e);
}
}
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
// 读取上下文信息
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
String traceId = AppContext.getTraceId();
return () -> {
try {
// 在子线程中添加request上下文信息
RequestContextHolder.setRequestAttributes(requestAttributes);
return callable.call();
} finally {
RequestContextHolder.resetRequestAttributes();
}
};
}
}
你以为这就完了?
问题二:致命的交易问题来自于线程复用和线程之间的信息传递
为了做日志的链路追踪,前段时间刚引入了TransmittableThreadLocal
这个阿里开源的小工具包,确实挺好用,可以在多线程中传递父线程的信息。于是,链路追踪的ID
以及访问用户的身份信息等就这样放在了TransmittableThreadLocal
中。本以为可以开开心心的听着歌敲着代码,然而上线不到1天,客户A来投诉了对账对不上,一查日志发现客户A的¥打到了客户B的账上。呕呕。。。
顿时,感觉到了即将失业的恐惧😱。
饭也顾不上吃了,目不转睛的找代码的问题。在几十万行日志中寻找关键的几个日志。花了几个小时的对比查找,发现很有可能是Feign
在查询客户信息的时候本来应该取A的信息,结果返回了B的信息。立即展开压力测试,现象很明显,前几次的数据正确,概率性的出现数据错乱,大多数都出现了取不到数据。一度怀疑是见了鬼,完全不知道错在哪儿。只好一步步来,把整个Feign
调用链路上打满日志,终于发现在FeignInterceptor
拷贝信息时从TransmittableThreadLocal
读取的客户信息错了。于是翻遍了TransmittableThreadLocal
的代码issues,然而一无所获。这时一个不起眼的直觉告诉我,是不是不能从TransmittableThreadLocal
中取,直接从RequestContextHolder
取试试。果然,RequestContextHolder
中的值可以传递过去。于是乎,问题答案几乎快要水落石出。
猜测可能是因为Hystrix
会线程复用,所以它是不是用了其他线程的数据。对应代码就是在HystrixConcurrencyStrategy
中的wrapCallable
实现时,没有更新TransmittableThreadLocal
中的客户的信息。果然,简单修改了一下真的就不会再出现错误的了。代码如下:
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
// 读取上下文信息
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
String traceId = AppContext.getTraceId();
return () -> {
try {
// 在子线程中添加request上下文信息
TransmittableThreadLocalContext.set("userInfo", "B"); RequestContextHolder.setRequestAttributes(requestAttributes);
return callable.call();
} finally {
RequestContextHolder.resetRequestAttributes();
}
};
}
此时,已经得到答案。
通过仔细的思考和查看了Hystrix
以及TransmittableThreadLocal
的相关源码,得出结论,Hystrix
的线程池由简单的封装了JDK的ThreadPoolExecutor
,然而TransmittableThreadLocal
需要用TtlRunnable
才能保证信息安全同步。因此,在前几次调用Feign
时,由于ThreadPoolExecutor
中并未创建线程,所以会由请求线程RequestThread_1
创建新的线程HystrixThread_1
,此时RequestThread_1
中的TransmittableThreadLocal
内的值("A")会被HystrixThread_1
同步且缓存起来供下次读取。新的请求线程RequestThread_X
(TransmittableThreadLocal
值为"B")来了后如果Hystrix
创建了新的线程HystrixThread_X
,则不会出现数据错误或空值;但如果Hystrix
复用了线程HystrixThread_1
,那么HystrixThread_1
中是不会读取到"B"的信息,此时如果RequestThread_1
线程结束了,此时读取TransmittableThreadLocal
的值就会不存在,但如果RequestThread_1
线程还在运行,那么TransmittableThreadLocal
的值就为"A",就出现了数据错乱。
真的是血的教训,不说了,准备卖肾赔客户的💰。