在从源码底层去分析其实现原理之前,我们首先要知道Ribbon在工作过程中是怎样的一个流程,如下图:
相信用过Ribbin的同学(Feign底层也是封装的Ribbon)都知道Ribbon是一个用于客户端的服务发现负载均衡的调用组件,我们的服务提供者在启动的时候会主动向注册中心nacos(这里用nacos举例)去注册自己的信息,然后服务消费者通过加了@LoadBalance注解的RestTemplate组件向服务提供者发送调用请求,此时ribbon会解析这个host为服务名的url,取出host(调用的服务名),然后再去调用nacos client去nacos server中根据服务名去进行服务发现,取出该服务名对应的实例集合,然后ribbon再通过自己的负载均衡算法去选取其中的一个实例ip进行调用,这就是大概ribbon在调用的时候的流程。
在RestTemplate中能够添加一系列的Interceptor对我们的请求进行拦截处理相关的拦截逻辑,其实ribbon就是在RestTemplate中加入了相关的拦截器然后加入到容器中,而这个拦截器所做的工作就是上述流程,所以在我们使用加了@LoadBalance注解的RestTemplate组件的时候会被ribbon拦截器所拦截达到负载均衡的效果。
接下来我们通过阅读源码去看上述的过程在代码是如何实现的。
首先我们需要找到源码的入口,通常这些框架与springboot整合都是通过springboot的自动装配机制去整合的,所以入口也通常在一些XXXAutoConfiguration类里面,上面已经说了ribbon的核心实现流程主要是在RestTemplate中加入了ribbon的Interceptor来实现的,所以这个Interceptor应该就是入口了。来到@LoadBalance注解所在的包,我们能够发现一个LoadBalanceAutoConfiguration类,在里面发现一段配置:
这段代码把一个LoadBalancerInterceptor以及一个RestTemplateCustomizer加入到容器中,这个RestTemplateCustomzier就是一个RestTemplate的定制器,它里面对加入容器的RestTemplate添加了一个LoadBalancerInterceptor,但是这个定制器在什么时候会被调用呢?我们可以看到还有一个Bean的配置:
这段代码往容器里面添加了一个SmartInitializingSingleton,定制器就是在这个类的方法里面被执行的,首先拿到所有被@LoadBalanced接口注解的RestTemplate,然后遍历这些RestTemplate往里面执行我们的定制器的方法。那么这个类的方法又是什么时候执行的,熟悉Spring的同学应该知道,其实这个类是Spring中的一个接口类,在所有Bean都完成其生命周期的时候从容器找出所有实现了该接口的bean,然后执行这个接口的方法。
这个Interceptor里面主要还是依靠LoadBalancerClient组件去执行的,这个组件我们通过一开始说的方法可以找到是在RibbonAutoConfiguration类中,并且是在上面说的LoadBalancerAutoConfiguration类执行之前去执行加入到容器中的
可以看到这个LoadBalancerClient是RibbonLoadBalancerClient,进去其excute方法中
这里有两句代码,很明显是首先拿到一个LoadBalancer的负载均衡器,然后通过这个负载均衡器去找到对应的某一个具体的服务的ip然后去进行调用,getLoadBalancer(serviceId)这句代码其实是从spring容器中去拿到对应的负载均衡器,所以肯定在某个配置类@Bean加入了一个具体的负载均衡器,通过寻找可以在RibbonClientConfiguration中找到
接着来到getServer里面的chooseServer方法,通过该方法名可以大概推断出该方法是用来选择具体的某一个服务的,进去父类的chooseServer
这里可以看到一句关键的代码,推断大概意思就是根据一定的规则去选择具体的服务,因为ribbon是提供多种规则或者自定义规则去选择服务的,而这个rule是一个接口所以具体的实现类大概又是在配置类中通过@Bean加入到容器中的了,也是在RibbonClientConfiguration中可以找到
大家注意下这些组件很多都是有@ConditionalOnMissingBean注解的所以需要的话我们也可以自己往容易中扔进去我们自己自定义的组件
但是这个类并没有choose方法,其实这个方法是在它的父类PredicateBasedRule中
关键代码就是红框中的代码
里面获取到所有可用的服务然后通过一个incrementAndGetModulo方法拿到服务集合的下标,可以看下该方法是通过什么规则拿的
这里使用了cas去防止并发问题,简单来说其实就是一个轮询规则,至此ribbon选择服务的流程就跑完了 。
我们上面说了ribbon是定时地从注册中心里面去拉取服务从而更新自己本地的服务列表,那么这个拉取的流程是怎样的呢?而且既然ribbon需要从注册中心去拉取服务列表,因为ribbon没有规定说一定要与哪个注册中心绑定在一起呀,那么当我们自己开发出一个注册中心的话,那么ribbon就肯定得提供相应的接口给注册中心去整合吧,下面我们来看看ribbon在与注册中心整合去实现拉取服务的过程。
首先我们知道这个拉取的过程不是我们自己去触发的,所以该拉取的源头肯定是在类初始化的时候,所以应该是在某个类的构造方法里面,当该类加入到容器中并且初始化成功之后去触发,仔细寻找其实是在初始化ZoneAwareLoadBalancer这个负载均衡器的时候它的父类DynamicServerListLoadBalancer有一个初始化方法去做的。
来到DynamicServerListLoadBalancer的构造方法中的restOfInit方法
这里我们重点关注红框中这两个方法,进去enableAndInitLearnNewServersFeature中
这里面的serverListUpdate其实也是在配置类中通过@Bean加入到容器中的
所以我们进去PollingServerListUpdater这个类的start方法中
这个方法就比较关键了,可以看出这个方法里面new了一个线程任务,然后通过线程池去延迟定时地去执行该任务,我们之前说过ribbon是定时地去从注册中心中拉取服务,可以看到这个初始延迟时间是1000ms,间隔执行时间30s,也就是说ribbon默认是隔30s去从注册中心拉取最新的服务列表的。而该任务中有一个重要的方法就是doUpdate,这个方法里面就是做从注册中心中拉取服务的事情了,进去doUpdate方法中
可以看到里面有一句serverListImpl.getUpdatedListOfServers的代码,这句代码就是从我们的注册中心中去拉取服务的,先找出serverListImpl的实现类
第一个类是从配置类中去通过@Bean去加载的
这个ServerList组件主要是读取IClientConfig里面set的serverList,而IClientConfig是在配置类中@Bean加入到容器的,默认里面set的serverList为空,所以如果我们单独使用ribbon的话,可以通过手动@Bean一个set了serverList的IConfig,从而让ribbon去读取自己手动set的serverList,但是我们通常都是去整合注册中心去使用,所以这种方式没什么意义
第二个ServerList组件我们可以看到一个特殊名字NacosServerList,这个组件就是nacos通过继承ribbon提供暴露出来的抽象类去实现的了,里面的逻辑就是调用nacos的客户端API去nacos服务端中拉取服务了
这里面的调用逻辑就是nacos客户端里面服务发现的源码了,这一块我们等看nacos源码的时候再去说吧
然后就是当ribbon每次从注册中心拿到了最新的服务列表去更新到本地
来到DynamicServerListLoadBalancer的父类BaseLoadBalancer,这个父类里面有一个全局变量
这个全局变量就是用来存储从注册中心拉取回来的最新的服务列表
继续看它的setServerList方法,可以看到有一句最关键的代码
把上面的存储服务列表的变量的引用指向了我们最新的获取到的服务列表集合,这就完成了本地服务列表缓存的更新了
下面通过一张图去说明ribbon各组件实现整个拉取服务负载均衡的流程