原因
公司项目使用Spring Cloud做后端架构,后端接口间调用使用的是Feign。前几天在生产上发现了一个问题,定时任务会重复执行。分析了日志后,得出结论,因为定时任务执行时间较长,定时任务模块调用业务模块接口会等到超时,然后Spring Cloud 的Feign会重试请求,导致定时任务执行两次。
百度查到的都不能解决我遇到的问题,我下决心要看源码透彻了解这个问题。
以下不完全是上述问题的解决,也有对Spring其他源码的理解,若嫌篇幅长,可直接翻到底看解决方案。
分析Feign
既然大致确定是因为Feign导致请求重发,我们就从分析Feign的重试来着手。
Feign有个Retryer类来控制请求的重试,Retryer是个interface,有两个实现,一个是Retryer.Default,一个是Retryer.NEVER_RETRY。看源码我们得知,Retryer.Default是重试5次,Retryer.NEVER_RETRY是不重试,那Feign默认使用的事哪个实现呢?
获取Retryer
在spring的项目中,所有的Bean都是通过Spring容器来管理的。先来看看从Spring管理的bean中能否获取到Retryer,获取到的Retryer是哪个。首先在注解EnableFeignClients有个注解@Import(FeignClientsRegistrar.class),它引入了FeignClientsRegistrar这个配置类。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
//...
在FeignClientsRegistrar的registerBeanDefinitions方法中执行了方法registerFeignClients。
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
//...
以下是registerFeignClients关键代码,在registerFeignClients方法中,将FeignClientFactoryBean注册到了spring容器中
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
// ...
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
FeignClientFactoryBean实现了ApplicationContextAware接口,会执行getObject方法,并将返回结果注入到Spring容器中,关键代码如下
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
ApplicationContextAware {
@Override
public Object getObject() throws Exception {
FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
// ...
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, new HardCodedTarget<>(
this.type, this.name, url));
}
先来看看 return targeter.target 这个方法,这个方法最终会返回一个Feign的实例,并最终注入到Spring的容器中
再来看getObject方法中的 Feign.Builder builder = feign(context); ,这个方法最终会执行方法configureUsingConfiguration,configureUsingConfiguration的关键代码如下:
protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) {
//...
Retryer retryer = getOptional(context, Retryer.class);
if (retryer != null) {
builder.retryer(retryer);
}
//...
}
这里获取了Retryer的实例,我们可以在此打断点,然后使用idea的Evluation Expression执行 retryer == Retryer.NEVER_RETRY,执行结果是true,说明Feign默认使用的是Retryer.NEVER_RETRY。那么为什么还是会重复发送请求呢,先来看看spring是怎么注入Retryer.NEVER_RETRY是怎么注入到Spring容器中的。
注入Retryer
在spring.factories中有org.springframework.cloud.netflix.feign.FeignAutoConfiguration,在FeignAutoConfiguration中实例化了FeignContext
@Configuration
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class})
public class FeignAutoConfiguration {
@Bean
public FeignContext feignContext() {
FeignContext context = new FeignContext();
context.setConfigurations(this.configurations);
return context;
}
//...
FeignContext.java:
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
public FeignContext() {
super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}
}
以下是NamedContextFactory.java中的关键代码
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
private Class<?> defaultConfigType;
public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName,
String propertyName) {
//设置加载的配置类,此处为FeignClientsConfiguration.class
this.defaultConfigType = defaultConfigType;
this.propertySourceName = propertySourceName;
this.propertyName = propertyName;
}
@Override
public void setApplicationContext(ApplicationContext parent) throws BeansException {
this.parent = parent;
}
protected AnnotationConfigApplicationContext createContext(String name) {
//...
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType); //注册配置类到当前容器中,
//...
if (this.parent != null) {
// 设置当前的context为applicationContxt的子context
context.setParent(this.parent);
}
// 刷新context,子容器的初始化 //父子容器查看DefaultListableBeanFactory.java的344行getBean方法
context.refresh();
return context;
}
这里的context.refresh()会初始化当前容器,也就是将FeignClientsConfiguration中所有的配置Bean加载到当前容器中。
再来看看FeignClientsConfiguration.java这个配置类,这里初始化了很多Bean,其中有这么一段,也就是注入到spring的Retryer实例
@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
既然Feign默认使用的是Retryer.NEVER_RETRY。那么为什么还是会重复发送请求呢?
看完注入和获取实例的代码没有解决问题,只能再看看调用接口的代码了。
分析Ribbon
调用接口的代码在SynchronousMethodHandler的invoke方法中
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
retryer.continueOrPropagate(e);
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
这里使用了Feign的Retryer对象,上面已经分析过,这里的retryer是Retryer.NEVER_RETRY,也就是在catch的时候不会重试请求,会直接跳出这个while循环。再来看看executeAndDecode方法,
Object executeAndDecode(RequestTemplate template) throws Throwable {
Request request = targetRequest(template);
Response response = client.execute(request, options);
//...
executeAndDecode方法中,这里的client有两个实现类,Client.Default和LoadBalancerFeignClient,来看看这里用的是哪个
在spring.factories中,有org.springframework.cloud.netflix.feign.ribbon.FeignRibbonClientAutoConfiguration,FeignRibbonClientAutoConfiguration在引入了DefaultFeignLoadBalancedConfiguration.class
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
OkHttpFeignLoadBalancedConfiguration.class,
DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {
//...
DefaultFeignLoadBalancedConfiguration.java代码如下
@Configuration
class DefaultFeignLoadBalancedConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null),
cachingFactory, clientFactory);
}
}
这段代码将LoadBalancerFeignClient注入到了Spring容器中,上面那个client的实现类就是LoadBalancerFeignClient。也就是说只要引入spring-cloud-feign这个jar包,那Ribbon就是使用LoadBalancerFeignClient作为发起接口调用请求的实现类
再来看LoadBalancerFeignClient的execute方法,一层层看,发现他调用了LoadBalancerCommand的submit方法。这里有两个参数:
final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();//同一个server的重试次数
final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();//下一个server的重试次数
看到这里,终于明白是什么参数在导致请求重发的问题,maxRetrysSame表示对同一个server的请求的最大重试次数,maxRetrysNext表示对相同服务的不同节点的请求的最大重试次数。
往下看一层,发现在RequestSpecificRetryHandler.java,获取这两个参数的关键代码如下:
if (requestConfig != null) {
if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetries)) {
retrySameServer = requestConfig.get(CommonClientConfigKey.MaxAutoRetries);
}
if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetriesNextServer)) {
retryNextServer = requestConfig.get(CommonClientConfigKey.MaxAutoRetriesNextServer);
}
}
这段逻辑就是从配置文件中获取retrySameServer和retrySameServer,若配置文件中没有,则获取默认值
然后看DefaultClientConfigImpl.java,发现这两个参数有默认值
public static final IClientConfigKey<Integer> MaxAutoRetries = new CommonClientConfigKey<Integer>("MaxAutoRetries"){};
public static final IClientConfigKey<Integer> MaxAutoRetriesNextServer = new CommonClientConfigKey<Integer>("MaxAutoRetriesNextServer"){};
public static final int DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER = 1;
public static final int DEFAULT_MAX_AUTO_RETRIES = 0;
看到这里,终于明白ribbon重试的机制,于是只需要配置这两个参数即可停止接口调用的重试
解决
在配置文件中增加以下配置,解决了会重发请求的问题
ribbon.MaxAutoRetries=0
ribbon.MaxAutoRetriesNextServer=0
结语
分析了一大堆,其实有很多是和接口调用重试无关的,相当于是把这几天所有源码分析的过程都记录下来了,也是想更加巩固Spring和Spring cloud许多底层只是,包括Bean注入,Bean获取,父子容器初始化,接口调用,重试机制等知识。