spring-cloud-alibaba
spring-cloud-alibaba copied to clipboard
修复同时引入sleuth和seata时,出现TracingFeignClient和LazyTracingFeignClient无限相互调用,最终导致StackOverFlowError的bug
相关issue
https://github.com/alibaba/spring-cloud-alibaba/issues/1909
背景
背景见我之前提的PR:https://github.com/alibaba/spring-cloud-alibaba/pull/1915
目前该PR已被我关闭,因为我发现从feign.Client的继承体系来看,seata的实现是不合理的,所以,有了这个新的PR。
feign.Client的继承体系
从继承体系上来看,feign.Client主要包含两大类:
- LoadBalancerFeignClient,用于处理负载均衡策略的Client
- 其他Client,用于增强的Client,即充当delegate的Client,比如上图中的TracingFeignClient、SeataFeignClient等
seata的定位
seata在feign.Client的继承体系中的定位是增强,具体点讲,就是在发送请求之前把事务ID带上,没有其它作用了,更不用说负载均衡的作用了。
所以,seata不应该继承LoadBalancerFeignClient,生成SeataLoadBalancerFeignClient类,而只应该保留SeataFeignClient,把SeataFeignClient加入到delegate链中就可以了。
这样就遗留了一个问题,如何才能把SeataFeignClient加入到delegate链中呢?
让我们看看SeataFeignObjectWrapper.wrap()
方法,在这里,只要动态修改LoadBalancerFeignClient的delegate属性,把SeataFeignClient加进去就可以了,参考sleuth中的TraceFeignObjectWrapper.wrap()
方法,我们使用反射来动态修改delegate。
为什么删除SeataFeignContext和SeataContextBeanPostProcessor
我们看下SeataFeignContext的两个getInstance()方法:
public class SeataFeignContext extends FeignContext {
@Override
public <T> T getInstance(String name, Class<T> type) {
T object = this.delegate.getInstance(name, type);
// 注意这里是Client类型没有调用wrap()方法
if (object instanceof Client) {
return object;
}
return (T) this.seataFeignObjectWrapper.wrap(object);
}
@Override
public <T> Map<String, T> getInstances(String name, Class<T> type) {
Map<String, T> instances = this.delegate.getInstances(name, type);
if (instances == null) {
return null;
}
Map<String, T> convertedInstances = new HashMap<>();
for (Map.Entry<String, T> entry : instances.entrySet()) {
// 注意这里是Client类型没有调用wrap()方法
if (entry.getValue() instanceof Client) {
convertedInstances.put(entry.getKey(), entry.getValue());
}
else {
convertedInstances.put(entry.getKey(),
(T) this.seataFeignObjectWrapper.wrap(entry.getValue()));
}
}
return convertedInstances;
}
}
再来看现有SeataFeignObjectWrapper的wrap()方法:
public class SeataFeignObjectWrapper {
Object wrap(Object bean) {
// 是Client类型,在上面的getInstance()方法中就返回了,不会进到这里
// 不是Client类型,不会进入下面这个if分支
// 所以,SeataFeignContext中调用这个wrap()方法并没有什么用
if (bean instanceof Client && !(bean instanceof SeataFeignClient)) {
if (bean instanceof LoadBalancerFeignClient) {
LoadBalancerFeignClient client = ((LoadBalancerFeignClient) bean);
return new SeataLoadBalancerFeignClient(client.getDelegate(), factory(),
clientFactory(), this);
}
if (bean instanceof FeignBlockingLoadBalancerClient) {
FeignBlockingLoadBalancerClient client = (FeignBlockingLoadBalancerClient) bean;
return new SeataFeignBlockingLoadBalancerClient(client.getDelegate(),
beanFactory.getBean(BlockingLoadBalancerClient.class), this);
}
return new SeataFeignClient(this.beanFactory, (Client) bean);
}
return bean;
}
}
所以,怎么来看,SeataFeignContext 都是多余的,故删除之。
关于sleuth中feign.Client的实现
在2.2.6.RELEASE版本中,已经明确标记TraceLoadBalancerFeignClient为弃用状态,经过笔者验证,确实在3.0.0版本中彻底删除了这个类,可能也是认为TraceLoadBalancerFeignClient的实现不符合feign.Client的继承体系,但是我并没有找到相关证据。
测试
本PR的修改已经在以下场景验证通过:
- 同时引入sleuth和seata的场景
- 只引入seata的场景
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.
:white_check_mark: alan-tang-tt
:x: tangtong
tangtong seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.
大佬牛逼
发送请求之前把事务ID带上,事务ID通过Header全链路传递?如果仅仅是这个功能,是否必要做的这么复杂?我认为,前置过滤用Spring OnceFilter,后置过滤用Feign和RestTemplate那个拦截器,是否能满足需求了?毕竟去替换一些核心,总可能会有一些意想不到的风险
发送请求之前把事务ID带上,事务ID通过Header全链路传递?如果仅仅是这个功能,是否必要做的这么复杂?我认为,前置过滤用Spring OnceFilter,后置过滤用Feign和RestTemplate那个拦截器,是否能满足需求了?毕竟去替换一些核心,总可能会有一些意想不到的风险
修改这么多代码确实有风险,不过,我的项目已经这样修改,生产环境跑了大半年了,暂时没有发现问题,官方可以评估下。
我看到在2.2.6.RELEASE的版本上移除了这个pr, 意思是2.2.6.RELEASE是好的?
2.2.5.RELEASE和2.2.6.RELEASE的两个版本我都试过了, 都不行
2.2.5.RELEASE和2.2.6.RELEASE的两个版本我都试过了, 都不行
@songhanlin 改动量太多了,官方不愿意合并,你可以在你的工程下面新建跟官方包一样的路径,然后,重写那个类。
代码如下:
public class SeataFeignObjectWrapper {
private static final Log LOG = LogFactory.getLog(SeataFeignObjectWrapper.class);
private static final String EXCEPTION_WARNING = "Exception occurred while trying to access the delegate's field. Will fallback to default instrumentation mechanism, which means that the delegate might not be instrumented";
private static final String DELEGATE = "delegate";
private final BeanFactory beanFactory;
private CachingSpringLoadBalancerFactory cachingSpringLoadBalancerFactory;
private SpringClientFactory springClientFactory;
SeataFeignObjectWrapper(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
Object wrap(Object bean) {
if (bean instanceof Client && !(bean instanceof SeataFeignClient)) {
if (bean instanceof LoadBalancerFeignClient) {
return instrumentedLoadBalancerClient(bean);
}
if (bean instanceof FeignBlockingLoadBalancerClient) {
return instrumentedLoadBalancerClient(bean);
}
return new SeataFeignClient(this.beanFactory, (Client) bean);
}
return bean;
}
private Object instrumentedLoadBalancerClient(Object bean) {
try {
Field delegate = bean.getClass().getDeclaredField(DELEGATE);
delegate.setAccessible(true);
delegate.set(bean, new SeataFeignObjectWrapper(this.beanFactory)
.wrap(delegate.get(bean)));
}
catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException
| SecurityException e) {
LOG.warn(EXCEPTION_WARNING, e);
}
return bean;
}
CachingSpringLoadBalancerFactory factory() {
if (this.cachingSpringLoadBalancerFactory == null) {
this.cachingSpringLoadBalancerFactory = this.beanFactory
.getBean(CachingSpringLoadBalancerFactory.class);
}
return this.cachingSpringLoadBalancerFactory;
}
SpringClientFactory clientFactory() {
if (this.springClientFactory == null) {
this.springClientFactory = this.beanFactory
.getBean(SpringClientFactory.class);
}
return this.springClientFactory;
}
}
这个业务场景什么情况下会发生
这个业务场景什么情况下会发生 引入spring-cloud-starter-sleuth 和spring-cloud-starter-alibaba-seata 就会出现这个bug 都2年了不知道官方修复了没
我这里有一个错误的demo https://github.com/nishubin/spring-cloud-starter-alibaba-seata--bug
这个你能复现吗
这个你能复现吗
你跑一下那个demo,然后用jmeter循环调用就会复现