SpringCloud - Ribbon 核心源码解析
Spring cloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端负载均衡工具,简单的说,它能够使用负载均衡器基于某种规则或算法调用我们的微服务集群,并且我们也可以很容易地使用 Ribbon 实现自定义负载均衡算法。
在之前使用 Eureka 的过程中,需要导入对应的依赖,但是 Ribbon 有一点特殊,不需要引入依赖也可以使用。这是因为在 Eureka-client 中,已经默认为我们集成好了 Ribbon,可以直接拿来使用。
根据 Spring Boot 自动配置原理,先从各个 starter 的spring.factories
中寻找可能存在的相关配置类:
在 spring-cloud-common 中,存在自动配置类
LoadBalancerAutoConfiguration
在 eureka-client 中,存在配置类
RibbonEurekaAutoConfiguration
在 ribbon 中,存在配置类
RibbonAutoConfiguration
需要注意,RibbonEurekaAutoConfiguration
中存在@AutoConfigureAfter
注解,说明需要在加载RibbonAutoConfiguration
配置类后再加载当前配置类。这三个类的配置将在后面结合具体代码调试中说明。
下面我们通过代码调试的方式来探究 Ribbon 的运行流程。
调用流程
Ribbon 的调用过程非常简单,使用RestTemplate
加上@LoadBalanced
注解就可以开启客户端的负载均衡,写一个简单的测试用例进行测试:
结果:
通过结果可以看出,RestTemplate
基于服务名称,即可实现访问 Eureka-client 集群下的不同服务实例,实现负载均衡的调用方式。看一下@LoadBalanced
注解的定义:
注释说明了@LoadBalanced
用于注解在RestTemplate
上实现负载均衡,那么来看一下@LoadBalanced
注解是如何生效的呢?回到前面提到的配置类LoadBalancerAutoConfiguration
中:
在配置类中定义了一个LoadBalancerInterceptor
拦截器,并且为restTemplate
添加了这个拦截器。在restTemplate
每次执行方法请求时,都会调用intercept
方法执行拦截:
在上面的intercept
拦截方法中,首先获取本次访问的 url 地址,从中获取本次要访问的服务名,然后调用RibbonLoadBalancerClient
中的execute
方法。
在这里通过服务名获取了该服务对应的负载均衡器ILoadBalancer
的实例对象,然后调用该实例的chooseServer
方法获取一个可用服务实例,关于ILoadBalancer
会在后面具体介绍。
在execute
方法调用apply
方法的过程中,会调用LoadBalancerContext
的reconstructURIWithServer
方法重构将要访问的url
地址:
在拼接完成url
后,调用AbstractClientHttpRequest
类的execute
方法发送请求。
调用executeInternal
方法:
可以看到,最终RestTemplate
底层调用了HttpURLConnection
来发送请求。
总体的调用流程我们总结完了,那么负载均衡的过程究竟是如何实现的呢?我们来详细梳理一下。
负载均衡过程
在 Ribbon 中有个非常重要的组件LoadBalancerClient
,它是负载均衡的一个客户端,我们从这入手写一个测试接口:
调用接口测试结果,可以看出是通过LoadBalancerClient
的choose
方法,选择调用了不同端口上的服务实例,体现了负载均衡:
对代码进行调试,发现注入的LoadBalancerClient
的实现类正是之前看见过的RibbonLoadBalancerClient
,进入其choos
方法中,先后调用两次getServer
方法:
此时loadBalancer
实例对象为ZoneAwareLoadBalancer
,并且里面的allServerList
列表已经缓存了所有的服务列表。调用chooseServer
方法,由于此时我们只有一个zone
,所以默认调用父类BaseLoadBalancer
的chooseServer
方法:
在父类的方法中,根据IRule
实例定义的规则来确定返回哪一个具体的 Server:
这里的IRule
实现使用了默认的ZoneAvoidanceRule
,为区域内亲和选择算法。关于IRule
负载均衡算法在后面再做介绍。由于ZoneAvoidanceRule
中没有实现choose
方法,直接调用其父类PredicateBasedRule
的choose
方法:
调用AbstractServerPredicate
的chooseRoundRobinAfterFiltering
方法:
实现非常简单,通过轮询的方式选择下标:
返回choose
方法中,可以看到已经获得了一个 server 实例:
在上一篇文章中,我们介绍了Ribbon中的调用流程与负载均衡过程,本文我们再来详细看一看它的核心组件ILoadBalancer
。
核心组件 ILoadBalancer
返回服务实例的调用过程大体已经了解了,但是我们在上篇中略过了一个内容,就是获取LoadBalancer
的过程,回去看第一次调用的getServer
方法:
这里通过getLoadBalancer
方法返回一个ILoadBalancer
负载均衡器,具体调用了 Spring 的BeanFactoryUtil
,通过getBean
方法从 spring 容器中获取类型匹配的 bean 实例:
回到前面getServer
方法调用的那张图,你就会发现这时候已经返回了一个ZoneAwareLoadBalancer
,并且其中已经保存好了服务列表。看一下ILoadBalancer
的接口定义:
该接口定义了 Ribbon 中核心的两项内容,服务获取与服务选择,可以说,ILoadBalancer
是 Ribbon 中最重要的一个组件,它起到了承上启下的作用,既要连接 Eureka 获取服务地址,又要调用IRule
利用负载均衡算法选择服务。下面分别介绍。
服务获取
Ribbon 在选择之前需要获取服务列表,而 Ribbon 本身不具有服务发现的功能,所以需要借助 Eureka 来解决获取服务列表的问题。回到文章开头说到的配置类RibbonEurekaAutoConfiguration
:
其中定义了其默认配置类为EurekaRibbonClientConfiguration
,在它的ribbonServerList
方法中创建了服务发现组件DiscoveryEnabledNIWSServerList
:
DiscoveryEnabledNIWSServerList
实现了ServerList
接口,该接口用于初始化服务列表及更新服务列表。首先看一下ServerList
的接口定义,其中两个方法分别用于初始化服务列表及更新服务列表:
在DiscoveryEnabledNIWSServerList
中,初始化与更新两个方法其实调用了同一个方法来实现具体逻辑:
进入obtainServersViaDiscovery
方法:
可以看到,这里先得到一个EurekaClient
的实例,然后借助EurekaClient
的服务发现功能,来获取服务的实例列表。在获取了实例信息后,判断服务的状态如果为UP
,那么最终将它加入serverList
中。
在获取得到serverList
后,会进行缓存操作。首先进入DynamicServerListLoadBalancer
的setServerList
方法,然后调用父类BaseLoadBalancer
的setServersList
方法:
在BaseLoadBalancer
中,定义了两个缓存列表:
在父类的setServersList
中,将拉取的serverList
赋值给缓存列表allServerList
:
在 Ribbon 从 Eureka 中得到了服务列表,缓存在本地 List 后,存在一个问题,如何保证在调用服务的时候服务仍然处于可用状态,也就是说应该如何解决缓存列表脏读问题?
在默认负载均衡器ZoneAwareLoadBalancer
的父类BaseLoadBalancer
构造方法中,调用setupPingTask
方法,并在其中创建了一个定时任务,使用ping
的方式判断服务是否可用:
runPinger
方法中,调用SerialPingStrategy
的pingServers
方法:
pingServers
方法中,调用NIWSDiscoveryPing
的isAlive
方法:
NIWSDiscoveryPing
实现了IPing
接口,在IPing
接口中,仅有一个isAlive
方法用来判断服务是否可用:
NIWSDiscoveryPing
的isAlive
方法实现:
因为本地的serverList
为缓存值,可能与 eureka 中不同,所以从 eureka 中去查询该实例的状态,如果 eureka 里面显示该实例状态为UP
,就返回 true,说明服务可用。
返回Pinger
的runPingger
的方法调用处:
在获取到服务的状态列表后进行循环,如果状态改变,加入到changedServers
中,并且把所有可用服务加入newUpList
,最终更新upServerList
中缓存值。但是在阅读源码中发现,创建了一个监听器用于监听changedServers
这一列表,但是只是一个空壳方法,并没有实际代码对列表变动做出实际操作。
需要注意的是,在调试过程中当我下线一个服务后,results
数组并没有按照预期的将其中一个服务的状态返回为 false,而是results
数组中的元素只剩下了一个,也就说明,除了使用 ping 的方式去检测服务是否在线外,Ribbon 还使用了别的方式来更新服务列表。
我们在BaseLoadBalancer
的setServersList
方法中添加一个断点:
等待程序运行,可以发现,在还没有进入执行IPing
的定时任务前,已经将下线服务剔除,只剩下了一个可用服务。查看调用链,最终可以发现使用了定时调度线程池调用了PollingServerListUpdater
类的start
方法,来进行更新服务操作:
回到BaseLoadBalancer
的setServersList
方法中:
在这里就用新的服务列表更新了旧服务列表,因此当执行IPing
的线程再执行时,服务列表中只剩下了一个服务实例。
综上可以发现,Ribbon 为了解决服务列表的脏读现象,采用了两种手段:
更新列表
ping 机制
在测试中发现,更新机制和 ping 机制功能基本重合,并且在 ping 的时候不能执行更新,在更新的时候不能运行 ping,所以很难检测到 ping 失败的情况。
服务选取
服务选取的过程就是从服务列表中按照约定规则选取服务实例,与负载均衡算法相关。这里引入 Ribbon 对于负载均衡策略实现的接口IRule
:
其中choose
为核心方法,用于实现具体的选择逻辑。
Ribbon 中,下面 7 个类默认实现了IRule
接口,为我们提供负载均衡算法:
在刚才调试过程中,可以知道 Ribbon 默认使用的是ZoneAvoidanceRule
区域亲和负载均衡算法,优先调用一个zone
区间中的服务,并使用轮询算法,具体实现过程前面已经介绍过不再赘述。
当然,也可以由我们自己实现IRule
接口,重写其中的choose
方法来实现自己的负载均衡算法,然后通过@Bean
的方式注入到 spring 容器中。当然也可以将不同的服务应用不同的IRule
策略,这里需要注意的是,Spring cloud 的官方文档中提醒我们,如果多个微服务要调用不同的IRule
,那么创建出IRule
的配置类不能放在ComponentScan
的目录下面,这样所有的微服务都会使用这一个策略。
需要在主程序运行的 com 包外另外创建一个 config 包用于专门存放配置类,然后在启动类上加上@RibbonClients
注解,不同服务应用不同配置类:
总结
综上所述,在 Ribbon 的负载均衡中,大致可以分为以下几步:
拦截请求,通过请求中的 url 地址,截取服务名称
通过
LoadBalancerClient
获取ILoadBalancer
使用 Eureka 获取服务列表
通过
IRule
负载均衡策略选择具体服务ILoadBalancer
通过IPing
及定时更新机制来维护服务列表重构该 url 地址,最终调用
HttpURLConnection
发起请求
了解了整个调用流程后,我们更容易明白为什么 Ribbon 叫做客户端的负载均衡。与 nginx 服务端负载均衡不同,nginx 在使用反向代理具体服务的时候,调用端不知道都有哪些服务。而 Ribbon 在调用之前,已经知道有哪些服务可用,直接通过本地负载均衡策略调用即可。而在实际使用过程中,也可以根据需要,结合两种方式真正实现高可用。
版权声明: 本文为 InfoQ 作者【码农参上】的原创文章。
原文链接:【http://xie.infoq.cn/article/228be497e88aa86e7890cde8f】。文章转载请联系作者。
评论