概述
负载均衡
负载均衡,即Load Balance,LB,通器常有两种实现手段,服务端和客户端。
负载均衡器,是带有负载均衡功能的实体(或载体),本文不做严格区分;即缩写LB,可表示负载均衡策略,也可以表示负载均衡器。
服务端LB的缺点,提供更强的流量控制权,但无法满足不同的消费者希望使用不同负载均衡策略的需求,而使用不同负载均衡策略的场景确实是存在的,客户端LB就提供这种灵活性。
客户端LB的缺点:配置不当,可能会导致服务提供者出现热点,或压根就获取不到任何服务。
另外还有一种分类:
- 集中式LB:偏硬件。在服务的消费方和提供方之间使用独立的LB设施(硬件F5或软件Nginx),由该设施负责把访问请求通过某种策略转发到服务的提供方;
- 进程内LB:偏软件。将LB逻辑集成到消费方,消费方从服务注册中心获取可用的服务地址,由消费方根据某种策略选择一个合适的服务提供方,如Ribbon。
本文源码基于ribbon-2.7.18
版本。
Ribbon
Ribbon提供客户端的软负载均衡,Fegin和OpenFegin都是基于Ribbon实现。
Ribbon功能如下:
- 与Eureka、Feign等集成
- 基于Archalus完成运行时配置
- 支持可插拔序列化
组件
Ribbon是Netflix发布的开源项目,主要功能是为REST客户端实现负载均衡。几个核心组件,除RestClient外,都是接口:
- ServerList:负载均衡使用的服务器列表。这个列表会缓存在负载均衡器中并定期更新。根据获取服务信息的方式不同分为:
- 静态存储:从配置文件中获取服务节点列表并存储到本地;
- 动态存储:从注册中心获取服务节点列表并存储到本地。
- ServerListFilter:服务器列表过滤器。接口,主要用于对Service Consumer获取到的服务器列表进行预过滤,过滤的结果也是ServerList。Ribbon提供多种过滤器的实现;
- IPing:探测服务实例是否存活的策略;
- IRule:负载均衡策略,其实现类表述的策略包括:轮询、随机、根据响应时间加权等;
- ILoadBalancer:负载均衡器。这也是一个接口,Ribbon为其提供多个实现,比如ZoneAwareLoadBalancer。而上层代码通过调用其API进行服务调用的负载均衡选择。一般ILoadBalancer的实现类中会引用一个IRule;
- ServerListUpdater:用于更新服务列表
- RestClient:服务调用器,负载均衡后Ribbon向Service Provider发起REST请求的工具;在
ribbon-httpclient-2.3.0
里已经废弃
Ribbon工作流程:
- 优先选择在同一个Zone且负载较少的Eureka Server;
- 定期从Eureka更新并过滤服务实例列表;
- 根据用户指定的策略,在从Server取到的服务注册列表中选择一个实例的地址;
- 通过RestClient进行服务调用。
实战
如果是Spring Cloud项目,引入如下依赖:
此GAV是最后一个版本,已不再发布新版本。
如果是非Spring Cloud项目使用Ribbon:
不是很常见。
使用
主要是使用@RibbonClient和@LoadBalanced两个注解:
以及在启动类上添加:
其中MyRule是实现IRule的自定义的负载均衡算法。
配置
有注解配置和文件配置两种方式:
application.yml
配置:
对应的配置类有:RibbonEagerLoadProperties和RibbonProperties。前者就2个配置,相对简单,且很好理解:
RibbonProperties用于存储与访问IClientConfig,IClientConfig是一个接口,其实现类AbstractDefaultClientConfigImpl已废弃;另一个实现类ReloadableClientConfig,提供一大堆get或get变形方法,传参是IClientConfigKey。IClientConfigKey也是一个接口,他的非匿名非内部类的实现类就一个,CommonClientConfigKey。这是一个抽象泛型类,支持各种类型的参数,可以看到这个类里面定义很多参数,设置参数的两种形式:
以及通过static静态代码块的形式:
构造方法是两个参数时,如上面两个例子,第一个参数表示配置Key,第二个参数表示Value默认值。
注解
Spring Cloud提供的@RibbonClient
RibbonClients是数组形式的RibbonClient。
@RibbonClientName则
Spring Cloud提供的@LoadBalanced是一个使用@Qualifier的空注解:
进阶
IRule策略
自定义:可以实现IRule,也可以继承RoundRibbonRule。
自定义配置类如果放在@ComponentScan能扫描到的路径下,则会被所有的Ribbon客户端所使用,如果想某个客户端使用自定义配置类,则需要加以调整。
其类结构图
内置7种负载均衡策略:
- RoundRobinRule:轮询策略,按照一定的顺序依次调用服务实例
- WeightedResponseTimeRule:权重策略,根据每个服务提供者的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性也就越低。它的实现原理是,刚开始使用轮询策略并开启一个计时器,每一段时间收集一次所有服务提供者的平均响应时间,然后再给每个服务提供者附上一个权重,权重越高被选中的概率也越大
- RandomRule:随机策略,从服务提供者的列表中随机选择一个服务实例
- BestAvailableRule:最小连接数策略,最小并发数策略,遍历服务提供者列表,选取连接数最小的⼀个服务实例。如果有相同的最小连接数,会调用轮询策略进行选取
- RetryRule:重试策略,按照轮询策略来获取服务,如果获取的服务实例为null或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回null
- AvailabilityFilteringRule:可用性敏感策略,先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例
- ZoneAvoidanceRule:区域敏感策略,根据服务所在区域的性能和服务的可用性来选择服务实例,在没有区域的环境下,该策略和轮询策略类似
自动配置
直接看LoadBalancerAutoConfiguration源码:
RestTemplateCustomizer的注入在LoadBalancerInterceptorConfig类里面:
看到LoadBalancerInterceptor,这是Ribbon中的默认拦截器,当调用RestTemplate的getObject方法时,就会调用拦截器中的方法:
继续看RibbonLoadBalancerClient的execute方法:
getLoadBalancer
方法用于获取LB,getServer
方法获取一个服务:
最终会调用ILoadBalancer.chooseServer
方法。
ILoadBalancer是一个顶层接口:
其实现类图如下:
回到前面chooseServer
方法,具体选择哪个实现类的方法:
可以通过在每个实现类里都加上断点来调试加以判断。
也可以推理,肯定不是NoOpLoadBalancer。当然,这需要一点点源码阅读经验,在众多源码里,NopXxx
或NoOpXxx
表示空实现,如 SLF4J里的slf4j-nop
这个JAR包只有一个类,即实现org.slf4j.spi.SLF4JServiceProvider
的NOPServiceProvider。又或者Spring Security里 PasswordEncoder的实现类NoOpPasswordEncoder
里根本就没有任何密码加密策略,密码直接明文存储。看NoOpLoadBalancer的源码,几个方法都是空实现,便能得以验证。
但,不管是BaseLoadBalancer还是ZoneAwareLoadBalancer,都需要被初始化。是在RibbonClientConfiguration类中被加载的:
可知使用ZoneAwareLoadBalancer。通过IDEA定位到构造函数,有个super方法,即执行父类DynamicServerListLoadBalancer的构造函数,最后调用里面的restOfInit方法:
请求拦截
基于上述源码分析,得出总结:
- Ribbon拦截所有标注@LoadBalanced注解的RestTemplate;
- 将Ribbon默认的拦截器LoadBalancerInterceptor添加到RestTemplate的执行逻辑中,当RestTemplate每次发送HTTP请求时,都会被Ribbon拦截
- 拦截后,Ribbon会创建一个ILoadBalancer实例
- ILoadBalancer实例会使用RibbonClientConfiguration完成自动配置:IRule,IPing,ServerList
- Ribbon会从服务列表中选择一个服务,将请求转发给这个服务
ServerListUpdater
ServerListUpdater是一个接口,实现类为PollingServerListUpdater:
30秒更新一次服务器信息。
拓展
Ribbon、Feign、Nginx三者负载均衡区别
不要笑,这是一道面试官的题目。
前面提过,Nginx是集中式LB。重点比较Ribbon和Feign,实际上Feign基于Ribbon。