概述
负载均衡
负载均衡,即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项目,引入如下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
此GAV是最后一个版本,已不再发布新版本。
如果是非Spring Cloud项目使用Ribbon:
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-loadbalancer</artifactId>
<version>2.7.18</version>
</dependency>
不是很常见。
使用
主要是使用@RibbonClient和@LoadBalanced两个注解:
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
以及在启动类上添加:
@RibbonClient(name = "demo-service", configuration = MyRule.class)
其中MyRule是实现IRule的自定义的负载均衡算法。
配置
有注解配置和文件配置两种方式:
@Bean
public IRule rule() {
return new BestAvailableRule();
}
application.yml
配置:
ribbon:
eager-load:
#开启饥饿加载模式
enabled: true
#指定需要饥饿加载的服务名
# http建立socket超时时间,毫秒
ConnectTimeout: 2000
# http读取响应socket超时时间
ReadTimeout: 5000
# 同一台实例最大重试次数,不包括首次调用
MaxAutoRetries: 0
# 重试负载均衡其他的实例最大重试次数,不包括首次server
MaxAutoRetriesNextServer: 1
# 是否所有操作都重试,POST请求注意多次提交错误。
# 默认false,设定为false的话,只有get请求会重试
OkToRetryOnAllOperations: false
对应的配置类有:RibbonEagerLoadProperties和RibbonProperties。前者就2个配置,相对简单,且很好理解:
@ConfigurationProperties(prefix = "ribbon.eager-load")
public class RibbonEagerLoadProperties {
private boolean enabled = false;
private List<String> clients;
}
RibbonProperties用于存储与访问IClientConfig,IClientConfig是一个接口,其实现类AbstractDefaultClientConfigImpl已废弃;另一个实现类ReloadableClientConfig,提供一大堆get或get变形方法,传参是IClientConfigKey。IClientConfigKey也是一个接口,他的非匿名非内部类的实现类就一个,CommonClientConfigKey。这是一个抽象泛型类,支持各种类型的参数,可以看到这个类里面定义很多参数,设置参数的两种形式:
public static final IClientConfigKey<Boolean> OkToRetryOnAllOperations = new CommonClientConfigKey<Boolean>("OkToRetryOnAllOperations", false) {};
以及通过static静态代码块的形式:
public static final IClientConfigKey<Integer> ConnectionCleanerRepeatInterval;
static {
ConnectionCleanerRepeatInterval = new CommonClientConfigKey<Integer>("ConnectionCleanerRepeatInterval", 30000) {};
}
构造方法是两个参数时,如上面两个例子,第一个参数表示配置Key,第二个参数表示Value默认值。
注解
Spring Cloud提供的@RibbonClient
@Configuration(proxyBeanMethods = false)
@Import(RibbonClientConfigurationRegistrar.class)
public @interface RibbonClient {
// value和name用于指定服务名
String value() default "";
String name() default "";
Class<?>[] configuration() default {};
}
RibbonClients是数组形式的RibbonClient。
@RibbonClientName则
@Value("${ribbon.client.name}")
public @interface RibbonClientName {
}
Spring Cloud提供的@LoadBalanced是一个使用@Qualifier的空注解:
@Qualifier
public @interface LoadBalanced {
}
进阶
IRule策略
public interface IRule {
/*
* choose one alive server from lb.allServers or
* lb.upServers according to key
*/
Server choose(Object key);
void setLoadBalancer(ILoadBalancer lb);
ILoadBalancer getLoadBalancer();
}
自定义:可以实现IRule,也可以继承RoundRibbonRule。
自定义配置类如果放在@ComponentScan能扫描到的路径下,则会被所有的Ribbon客户端所使用,如果想某个客户端使用自定义配置类,则需要加以调整。
其类结构图
内置7种负载均衡策略:
- RoundRobinRule:轮询策略,按照一定的顺序依次调用服务实例
- WeightedResponseTimeRule:权重策略,根据每个服务提供者的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性也就越低。它的实现原理是,刚开始使用轮询策略并开启一个计时器,每一段时间收集一次所有服务提供者的平均响应时间,然后再给每个服务提供者附上一个权重,权重越高被选中的概率也越大
- RandomRule:随机策略,从服务提供者的列表中随机选择一个服务实例
- BestAvailableRule:最小连接数策略,最小并发数策略,遍历服务提供者列表,选取连接数最小的⼀个服务实例。如果有相同的最小连接数,会调用轮询策略进行选取
- RetryRule:重试策略,按照轮询策略来获取服务,如果获取的服务实例为null或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回null
- AvailabilityFilteringRule:可用性敏感策略,先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例
- ZoneAvoidanceRule:区域敏感策略,根据服务所在区域的性能和服务的可用性来选择服务实例,在没有区域的环境下,该策略和轮询策略类似
自动配置
直接看LoadBalancerAutoConfiguration源码:
@AutoConfiguration
@Conditional(BlockingRestClassesPresentCondition.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerClientsProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
}
}
RestTemplateCustomizer的注入在LoadBalancerInterceptorConfig类里面:
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
看到LoadBalancerInterceptor,这是Ribbon中的默认拦截器,当调用RestTemplate的getObject方法时,就会调用拦截器中的方法:
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
继续看RibbonLoadBalancerClient的execute方法:
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server,
isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
getLoadBalancer
方法用于获取LB,getServer
方法获取一个服务:
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
if (loadBalancer == null) {
return null;
}
// Use 'default' on a null hint, or just pass it on?
return loadBalancer.chooseServer(hint != null ? hint : "default");
}
最终会调用ILoadBalancer.chooseServer
方法。
ILoadBalancer是一个顶层接口:
public interface ILoadBalancer {
// 添加服务器
void addServers(List<Server> newServers);
// 选择服务器
Server chooseServer(Object key);
// 下线某个服务器(不是立马下线,而是直到下一个ping周期才下线)
void markServerDown(Server server);
@Deprecated
public List<Server> getServerList(boolean availableOnly);
// 返回所有在线的和可达的
public List<Server> getReachableServers();
// 返回所有已知的服务器,包括可达的和不可达的
public List<Server> getAllServers();
}
其实现类图如下:
回到前面chooseServer
方法,具体选择哪个实现类的方法:
可以通过在每个实现类里都加上断点来调试加以判断。
也可以推理,肯定不是NoOpLoadBalancer。当然,这需要一点点源码阅读经验,在众多源码里,NopXxx
或NoOpXxx
表示空实现,如SLF4J里的slf4j-nop
这个JAR包只有一个类,即实现org.slf4j.spi.SLF4JServiceProvider
的NOPServiceProvider。又或者Spring Security里PasswordEncoder的实现类NoOpPasswordEncoder
里根本就没有任何密码加密策略,密码直接明文存储。看NoOpLoadBalancer的源码,几个方法都是空实现,便能得以验证。
但,不管是BaseLoadBalancer还是ZoneAwareLoadBalancer,都需要被初始化。是在RibbonClientConfiguration类中被加载的:
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
// 几大核心组件
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, serverListFilter, serverListUpdater);
}
可知使用ZoneAwareLoadBalancer。通过IDEA定位到构造函数,有个super方法,即执行父类DynamicServerListLoadBalancer的构造函数,最后调用里面的restOfInit方法:
void restOfInit(IClientConfig clientConfig) {
boolean primeConnection = this.isEnablePrimingConnections();
// turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
this.setEnablePrimingConnections(false);
// 调用PollingServerListUpdater.start方法
enableAndInitLearnNewServersFeature();
// 执行ping逻辑等
updateListOfServers();
if (primeConnection && this.getPrimeConnections() != null) {
this.getPrimeConnections().primeConnections(getReachableServers());
}
this.setEnablePrimingConnections(primeConnection);
}
请求拦截
基于上述源码分析,得出总结:
- Ribbon拦截所有标注@LoadBalanced注解的RestTemplate;
- 将Ribbon默认的拦截器LoadBalancerInterceptor添加到RestTemplate的执行逻辑中,当RestTemplate每次发送HTTP请求时,都会被Ribbon拦截
- 拦截后,Ribbon会创建一个ILoadBalancer实例
- ILoadBalancer实例会使用RibbonClientConfiguration完成自动配置:IRule,IPing,ServerList
- Ribbon会从服务列表中选择一个服务,将请求转发给这个服务
ServerListUpdater
ServerListUpdater是一个接口,实现类为PollingServerListUpdater:
public class PollingServerListUpdater implements ServerListUpdater {
// 用于构造函数传参
private static long LISTOFSERVERS_CACHE_UPDATE_DELAY = 1000; // msecs;
private static int LISTOFSERVERS_CACHE_REPEAT_INTERVAL = 30 * 1000; // msecs;
@Override
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
final Runnable wrapperRunnable = () -> {
if (!isActive.get()) {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
try {
updateAction.doUpdate();
} catch (Exception e) {
// just logging
}
};
scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs, // 1000
refreshIntervalMs, // 30000
TimeUnit.MILLISECONDS
);
} else {
// just logging
}
}
}
30秒更新一次服务器信息。
拓展
Ribbon、Feign、Nginx三者负载均衡区别
不要笑,这是一道面试官的题目。
前面提过,Nginx是集中式LB。重点比较Ribbon和Feign,实际上Feign基于Ribbon。