最近刚刚把springcloud的版本升级到Finchley.RC1,就遇到些小麻烦,分享一下解决的过程和收获。 🥕
springcloud最近的版本更迭很快,为了更好的契合springboot2,我也把版本从Finchley.M9升级到Finchley.RC1,重新跑了下gateway,发现zuul重试功能莫名的失效了。因为一开始直接拿了网上的配置,也没有多去考究,正好趁着这个机会好好挖掘一下。
注:文章有点长,包括ribbon配置源码查阅的思路和zuul重试源码的解读,可以根据导航栏选择需要的阅读。
ribbon 配置问题
如果要实现zuul重试功能是要先引入:1
2
3
4<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
然后添加配置,我一开始的配置是这样的:1
2
3
4
5
6
7
8zuul:
retryable: true
ribbon:
connectTimeout: 500 # 请求连接的超时时间
readTimeout: 1000 # 请求处理的超时时间
maxAutoRetries: 2 # 对当前实例的重试次数
maxAutoRetriesNextServer: 1 # 切换实例的重试次数
okToRetryOnAllOperations: false # 对所有操作请求都进行重试
查阅源码后发现正确的写法应该是:1
2
3
4
5
6
7
8zuul:
retryable: true
ribbon:
ConnectTimeout: 500 # 请求连接的超时时间
ReadTimeout: 1000 # 请求处理的超时时间
MaxAutoRetries: 2 # 对当前实例的重试次数
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
OkToRetryOnAllOperations: false # 对所有操作请求都进行重试
上面的方式是全局的配置,如果需要局部的配置,可以这样写成1
2
3
4
5
6
7(serviceId):
ribbon:
ConnectTimeout: 500 # 请求连接的超时时间
ReadTimeout: 1000 # 请求处理的超时时间
MaxAutoRetries: 2 # 对当前实例的重试次数
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
OkToRetryOnAllOperations: false # 对所有操作请求都进行重试
其实,就是大小写导致参数失效,而且这些在配置文件中是不给提示的,那就是代表没有对应的@ConfigurationProperties,那是如何生效呢,这就勾起了我的好奇心。
ribbon 源码解读
对应这些配置的参数查阅源码,可以发现ribbon-core
源码里有对应的CommonClientConfigKey
类
1 | public abstract class CommonClientConfigKey<T> implements IClientConfigKey<T> { |
这3个参数在DefaultLoadBalancerRetryHandler
类中被使用,而这个类在RibbonClientConfiguration
中完成初始化的,因为@ConditionalOnMissingBean则不会被多次加载。
1 |
|
其他参数也是类似,不过被使用在其他类中。它们去获得对应的value都是使用getProperty方法,通过调用getInstance获得包装过的动态属性类,再拿到对应的value。
1 | public class DefaultClientConfigImpl implements IClientConfig { |
到这里其实整个向下的流程我们已经知道了,那剩余的就是看看ALL_PROPS是在何时被填充数据的了。这里面牵扯的类比较多,源码就不一一呈现了,捡取几个重要的。
其中在DefaultClientConfigImpl
有个很重要的方法loadProperties,作用是来加载配置。
1 | /** |
通过loadDefaultValues方法调ConcurrentCompositeConfiguration
的getProperty方法,再调PropertySourcesPropertyResolver
的getProperty方法。这样就从archauus-core包调到了spring-core包了。在propertySources中就包含了我们在yml的设置的参数。然后再通过DefaultClientConfigImpl
类的setPropertyInternal方法完成一系列对dynamicProperties和ALL_PROPS的数据填充。
zuul 源码解读
上面已经分析了配置的参数是如何被使用,但是还是有一个问题,那就是很关键的loadProperties的方法是何时被调用。查看源码可以发现该方法是在RibbonClientConfiguration
类中被调用。查看所在的jar包的spring.factories
,发现这个类并不会在服务启动时被加载。
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
那既然不是在服务启动的时候加载,那猜测是从Zuul进行路由转发的时候准备的,继续挖源码。很自然我们的目标就在ZuulFilter
的子类上了。其中一个很重要的Filter就是RibbonRoutingFilter
,它主要是完成请求的路由转发,很符合我们的想法,查看它的run方法,然后我们跟进到forward方法。
1 | public class RibbonRoutingFilter extends ZuulFilter { |
其中有2个重要的方法,分别是create和execute。很明显配置加载的工作只有可能在create方法中,为了验证之前的想法,继续看create的源码。这里的ribbonCommandFactory是在RibbonCommandFactoryConfiguration
完成初始化,查看spring.factories
,该类会在服务启动的时候加载。上面的@import很重要,跟进去可以发现,会先去判断是否有ribbon.restclient.enabled
配置,再判断是否有okhttp3.OkHttpClient
类,如果都没有设置的话,这个ribbonCommandFactory将会是HttpClientRibbonCommandFactory
。
1 |
|
查看HttpClientRibbonCommandFactory
的create方法:
1 | public class HttpClientRibbonCommandFactory extends AbstractRibbonCommandFactory { |
同样的道理,准备的工作一般会在创建的时候完成,往getClient方法跟下去,这里涉及的类也不详细列举了,跟下去后会到NamedContextFactory
的createContext方法的context.refresh();
,刷新了context,会调用到finishBeanFactoryInitialization方法中的beanFactory.preInstantiateSingletons()
,该方法会将这个上下文的bean工厂初始化,并初始化所有剩余的单例bean。这样之前的疑问就解答了,果然是在zuul做转发的时候完成了加载。
1 | public void preInstantiateSingletons() throws BeansException { |
到此,forward中的create方法已经看了,问题也解决了。那把剩余的execute也一并看了吧。该方法最后执行的会是AbstractRibbonCommand
的run方法,里面重要的就是executeWithLoadBalancer方法:
1 | public abstract class AbstractLoadBalancerAwareClient<S extends ClientRequest, T extends IResponse> extends LoadBalancerContext implements IClient<S, T>, IClientConfigAware { |
然后就是LoadBalancerCommand
的submit方法:
1 | public Observable<T> submit(final ServerOperation<T> operation) { |
最后就是RetryableRibbonLoadBalancingHttpClient
的execute方法:
1 |
|
唠唠叨叨了好多,就先到这了~
参考文献
http://blog.didispace.com/spring-cloud-zuul-retry-detail/