Ribbon 实现负载均衡的接口以及内置的负载均衡规则
BaseLoadBalancer
的负载均衡规则是轮询。任何一个负载均衡器的规则由 IRule
接口的实现类的 choose()
方法来定义。
常见的实现类:
RoundRobinRule
:默认的规则,直接轮训Server List
;AvailabilityFilteringRule
:考察服务器的可用性来调用;WeightedResponseTimeRule
:以响应时间作为权重;ZoneAvoidanceRule
:根据区域来调用;BestAvailableRule
:直接忽略连接失败的服务器,再去找并发量较低的服务器;RandomRule
:随机访问;RetryRule
:提供重试机制,即访问失败后重新找一个;
也可以自定义一个规则。
Ribbon
的主打功能就是负载均衡,其他都是次要的。看源码的重点就要放在负载机会机制上面。
Ribbon 判断服务器是否存活的接口
负载均衡器,LoadBalancer
需要定时 ping Server List
里面的服务器,确保它们存活。ping 操作的时间间隔定义在 ILoadBalancer
的 getPingInterval()
方法,是否存活的标准可以在 IPing
的实现类的 isAlive()
方法中定义。
常见的 IPing
实现类:
Ribbon
比 Eureka
简单得多,直接在 Spring Cloud
环境下看源码即可。
Ribbon 实现负载均衡的大概流程
@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
在代码中注入这个 Bean,使用它发送 HTTP 请求就会自动实现负载均衡。
Ribbon 原理:以 @LoadBalanced 注解作为入口
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
public @interface LoadBalanced {
}
标记一个 RestTemplate
的 bean,这个 bean 会使用 LoadBalancerClient
通信。
根据流程图找到自动配置类:LoadBalancerAutoConfiguration
;
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
// 前面自己使用@LoadBalanced标记的 RestTemplate Bean 会放入这个list
// 至于怎么放入的先别管
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer
(final List<RestTemplateCustomizer> customizers) {
// 遍历 restTemplates 中的 RestTemplate 对象,
// 调用每个 RestTemplateCustomizer 的 customize() 方法进行定制
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
}
};
}
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
// 负载均衡拦截器 LoadBalancerInterceptor
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
// RestTemplate 定制器
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
// 调用RestTemplate提供的方法发起HTTP请求时,
// 会先经过一些拦截器
return new RestTemplateCustomizer() {
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
// 将 LoadBalancerInterceptor 添加到拦截器列表
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}
}
......
}
@LoadBalanced
的原理:
Ribbon 原理:Spring Cloud 通过负载均衡拦截器来改变 RestTemplate 的行为
调用 RestTemplate
的时候:restTemplate.getForObject("http://ServiceA/hello/"+name,String.class)
,会在底层封装好一个 Request
对象,包含请求的 URL、参数、期望响应结果的类型等等,然后经过一系列的拦截器,其中就有 LoadBalancerInterceptor
。
LoadBalancerInterceptor
的 intercept()
方法:
@Override // RestTemplate 封装好的请求对象 // 请求体
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
// Spring底层负责HTTP通信的组件
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
// Assert...
// private LoadBalancerClient loadBalancer,构造器中设置的
// 所以,通过拦截器,将本来应该由 RestTemplate 发送的请求变成 LoadBalancerClient
// 回顾一下 @LoadBalanced 的注释
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
拦截器会将 RestTemplate
构造的 HttpRequest
对象封装成 LoadBalancerRequest
对象,然后发送请求。
Ribbon 原理:LoadBalancerClient 实现负载均衡的流程
Ribbon
的自动装配类 RibbonAutoConfiguration
:
// 表示必须在 EurekaClientAutoConfiguration 被装配完毕之后才可以装配 RibbonAutoConfiguration
@AutoConfigureAfter(
name = {"org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration"}
)
@Configuration
public class RibbonAutoConfiguration {
// 用于发送 HTTP 请求的组件:LoadBalancerClient
@Bean
@ConditionalOnMissingBean({LoadBalancerClient.class})
public LoadBalancerClient loadBalancerClient() {
// 实现类
return new RibbonLoadBalancerClient(this.springClientFactory());
}
}
发送 HTTP 请求的时候调用 RibbonLoadBalancerClient
的 execute()
方法:
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
// 关键代码:选择合适的机器
Server server = this.getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
} else {
RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
return this.execute(serviceId, ribbonServer, request);
}
}
getServer
源码:loadBalancer
中包含 IRule
组件,负责定义实现负载均衡的逻辑
protected Server getServer(ILoadBalancer loadBalancer) {
return loadBalancer == null ? null : loadBalancer.chooseServer("default");
}
Ribbon:如何获取 ILoadBalancer 对象
RibbonLoadBalancerClient
的 execute()
方法中:
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId)
,获取到一个 LoadBalancer
。
源码:
protected ILoadBalancer getLoadBalancer(String serviceId) { // serviceId 要调用的服务名称
// private SpringClientFactory clientFactory
// 构造RibbonLoadBalancerClient的时候作为参数传入
return this.clientFactory.getLoadBalancer(serviceId);
}
跟进去最终调用这个方法:在NamedContextFactory
类中
// name=serviceId, type=ILoadBalancer.class
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
type).length > 0) {
return context.getBean(type);
}
return null;
}
SpringClientFactory
的注释中说明:每个服务名都对应一个 Spring ApplicationContext
。所以每个服务可以部署在多台机器上,即多个服务实例,这些服务实例对应同一个 ILoadBalancer
对象。
Ribbon:ILoadBalancer 的bean创建
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);
}
// 可见,Ribbon 默认创建的负载均衡器是 ZoneAwareLoadBalancer
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
在 SpringBoot、SpringCloud 的体系中,一般某个关键组件对应的 bean 都会在 XxxAutoConfiguration
、XxxConfiguration
中创建。
继承关系:
获取 ILoadBalancer
对象的流程:对 ServiceA
进行负载均衡访问
Ribbon:如何整合Eureka来获取注册表
负载均衡器 ILoadBalancer
对象控制对某个服务的负载均衡访问时,需要获取这个服务的各个实例,每个实例对应一个节点,这些实例的集合被封装成 BaseLoadBalancer
类的两个成员变量:
// 所有节点
@Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> allServerList = Collections
.synchronizedList(new ArrayList<Server>());
// 可用节点
@Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> upServerList = Collections
.synchronizedList(new ArrayList<Server>());
Ribbon
如何获取到这些信息 ?显然,通过 Eureka Server
来获取。
已知,Ribbon 默认创建的负载均衡器是 ZoneAwareLoadBalancer
,在其父类 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);
// 启用并初始化学习新的服务器的功能
// 猜测:应该是有新的服务实例加入时,可以感知到
enableAndInitLearnNewServersFeature();
// 更新服务器列表....
// 跟进去,发现调用成员变量 volatile ServerList<T> serverListImpl 的 getUpdatedListOfServers()
// 即更新服务器列表
// serverListImpl在构造 DynamicServerListLoadBalancer 时被传入
updateListOfServers();
if (primeConnection && this.getPrimeConnections() != null) {
this.getPrimeConnections()
.primeConnections(getReachableServers());
}
this.setEnablePrimingConnections(primeConnection);
}
ServerList
接口:提供了全量获取、增量获取的服务器列表的方法
public interface ServerList<T extends Server> {
public List<T> getInitialListOfServers();
public List<T> getUpdatedListOfServers();
}
ServerList
的 bean 什么时候放入 Spring 容器中?EurekaRibbonClientConfiguration
类:Eureka 和 Ribbon 整合类
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config, Provider<EurekaClient> eurekaClientProvider) {
if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
return this.propertiesFactory.get(ServerList.class, config, serviceId);
}
DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
config, eurekaClientProvider);
// Ribbon 默认使用的 ServerList 的实现是 DomainExtractingServerList
// 但 getInitialListOfServers()、getUpdatedListOfServers() 方法里面使用了 discoveryServerList
// 然后进行一些封装
DomainExtractingServerList serverList = new DomainExtractingServerList(
discoveryServerList, config, this.approximateZoneFromHostname);
return serverList;
}
即 Ribbon
默认的 ServerList
接口的实现类为 DomainExtractingServerList
,但里面调用的是 DiscoveryEnabledNIWSServerList
实现的方法。
而DiscoveryEnabledNIWSServerList
也是通过服务的 EurekaClient
对象,根据每个服务实例的虚拟 IP 地址,获取到对应的 InstanceInfo
对象,封装成 Server
对象加入 List 中。
Ribbon 获取到注册表之后,如何持续更新
EurekaClient
对象默认每隔 30s 会获取增量注册表信息,所以 Ribbon
的负载均衡器肯定也会定期更新 server list
。
在构造默认的负载均衡器 ZoneAwareLoadBalancer
的时候,调用了父类的 restOfInit()
方法,里面:
enableAndInitLearnNewServersFeature();
当时猜测这个方法的作用为启用和初始化学习新服务器的功能,很可能与持续更新注册表有关:
protected volatile ServerListUpdater serverListUpdater;
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
};
public void enableAndInitLearnNewServersFeature() {
// serverListUpdater对象被作为构造器的参数传入
// 默认使用的实现类:PollingServerListUpdater
serverListUpdater.start(updateAction);
}
start()
方法的源码:
@Override
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
// 创建一个用于增量获取注册表信息的线程
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
if (!isActive.get()) {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
try {
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
logger.warn("Failed one update cycle", e);
}
}
};
// 调度
scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
// 构造器中指定
// 默认1000ms
initialDelayMs,
// 默认 30x1000ms
refreshIntervalMs,
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}
所以,增量获取注册表信息的核心方法就是 ServerListUpdater.UpdateAction
的 doUpdate()
,就是调用 ServerList
的getUpdatedListOfServers()
来获取,而这个方法底层也是直接向 EurekaClient
对象拿到增量的注册表信息。
默认使用的 ServerList
实现为 DiscoveryEnabledNIWSServerList
,这两个方法的实现:
@Override
public List<DiscoveryEnabledServer> getInitialListOfServers(){
return obtainServersViaDiscovery();
}
@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
return obtainServersViaDiscovery();
}
初次获取与后续的更新一样,都是调用obtainServersViaDiscovery()
方法。
Ribbon 默认的负载均衡算法
回顾:每个服务,如ServiceA,都有一个 ApplicationContext,所以服务和负载均衡器为一一对应的关系,每个负载均衡器对象都包含一份 server list
。要访问某个服务时,就要获取到该服务对应的server list
,通过一定的算法,获取到某个服务实例(对应的机器,因为一个服务可能部署在多台机器),使对这个服务的请求均匀地打在不同的机器上。
在执行 RibbonLoadBalancerClient
的 execute()
方法之后,选择目标的服务实例的方法为 getServer()
:
protected Server getServer(ILoadBalancer loadBalancer) {
// 这里的 loadBalancer 默认为 ZoneAwareLoadBalancer
return loadBalancer == null ? null : loadBalancer.chooseServer("default");
}
这里先暂时不管 Zone
的概念,Zone
实际上就是机房,在 Ribbon
的设计中,如果一个服务部署在不同机房的机器,就不是一个服务对应一个 ILoadBalancer
对象了,而是每个机房一个。所以 ZoneAwareLoadBalancer
的 chooseServer()
方法会获取到每个机房的负载均衡器,然后调用这些负载均衡器的 chooesServer()
方法来获取目标服务实例:
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
// key = Default
server = zoneLoadBalancer.chooseServer(key);
}
原来最终还是使用了 BaseLoadBalancer
的实现,所以默认的负载均衡算法就是直接轮询 server list
:
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
// protected IRule rule = DEFAULT_RULE;
// private final static IRule DEFAULT_RULE = new RoundRobinRule();
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
负载均衡的算法被定义在 IRule
对象的choose()
方法中,RoundRobinRule
的实现为:
// 获取下一个被访问的服务实例在 server list 中的下标
int nextServerIndex = incrementAndGetModulo(serverCount);
// 获取出来
server = allServers.get(nextServerIndex);
计算下标的方法:就是记录一个原子变量,对 server list 的 size 取模之后,对其进行 CAS 设置的操作。
private int incrementAndGetModulo(int modulo) {
for (;;) {
// private AtomicInteger nextServerCyclicCounter
int current = nextServerCyclicCounter.get();
// modulo 为server list 的size
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
所以,默认的负载均衡算法很简单,就是自增并取模,这样就达到了轮询 server list 的效果。
Ribbon 选取到 Server 对象如何发送请求
RibbonLoadBalancerClient
执行 execute()
方法,最终获取到 Server 对象之后,如何发送 HTTP 请求到这个服务实例?
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
...
// 封装成 RibbonServer,ServiceInstance的子类
RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
// 调用重载的方法
return this.execute(serviceId, ribbonServer, request);
}
前面提及,我们通过 RestTemplate
发送请求的时候,会通过拦截器来改变它的行为,这个过程中,一个 HttpRequest
被封装成一个 LoadBalancerRequest
对象。那么,LoadBalancerRequest
是什么?
在 Ribbon 提供的负载均衡拦截器中:
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
// 构造一个LoadBalancerRequest对象
// 这里执行的 execute() 方法实际上就是调用了 LoadBalancerRequest 的 apply() 方法
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
实际上,LoadBalancerRequest
是一个接口,这里只是构造一个匿名内部类来实现它:
public interface LoadBalancerRequest<T> {
public T apply(ServiceInstance instance) throws Exception;
}
构造 LoadBalancerRequest
的过程:
public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request,
final byte[] body, final ClientHttpRequestExecution execution) {
return new LoadBalancerRequest<ClientHttpResponse>() {
// 用于发送请求
@Override
public ClientHttpResponse apply(final ServiceInstance instance)
throws Exception {
HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
if (transformers != null) {
for (LoadBalancerRequestTransformer transformer : transformers) {
serviceRequest = transformer.transformRequest(serviceRequest, instance);
}
} // 真正发生HTTP请求的组件:ClientHttpRequestExecution
return execution.execute(serviceRequest, body);
}
};
}
大致流程:
Ribbon 使用 Ping 机制来判断服务实例是否存活有效吗
在 SpringCloud
整合 Ribbon
、Eureka
的场景下,注册了以下的 IPing
对象:
@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
if (this.propertiesFactory.isSet(IPing.class, name)) {
return this.propertiesFactory.get(IPing.class, config, name);
}
return new DummyPing();
}
即在默认的情况下,注册了一个 DummyPing
的对象:
public class DummyPing extends AbstractLoadBalancerPing {
public DummyPing() {
}
public boolean isAlive(Server server) {
return true;
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
然而这个 bean 里面什么也没有做,即默认的情况下,Ribbon
的负载均衡器并不会对 server list
中的服务实例进行 ping 操作。因为 Eureka
会自行判断服务实例是否存活,并且有故障感知与下线的功能,而 EurekaClient
对象又会定期获取增量注册表,所以 server list
中的服务实例往往都是可用的,并不需要通过 ping 来验证。在开发中,也可以自定义一个 IPing
对象或者 Ribbon 提供的其他实现,如:NIWSDiscoveryPing
// 检查负载均衡器的 server list 中的 server 对应的InstanceInfo的状态
public boolean isAlive(Server server) {
boolean isAlive = true;
if (server!=null && server instanceof DiscoveryEnabledServer){
DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server;
InstanceInfo instanceInfo = dServer.getInstanceInfo();
if (instanceInfo!=null){
InstanceStatus status = instanceInfo.getStatus();
if (status!=null){
// UP 就表示存活
isAlive = status.equals(InstanceStatus.UP);
}
}
}
return isAlive;
}
有一点需要注意:调度这个 PingTask
的定时任务时,传入的时间间隔 pingIntervalSeconds
不是默认的 10s。在RibbonClientConfiguration
中注册ZoneAwareLoadBalancer
的bean的时候,会调用到父类以下的构造器:
public BaseLoadBalancer(IClientConfig config, IRule rule, IPing ping) {
initWithConfig(config, rule, ping);
}
void initWithConfig(IClientConfig clientConfig, IRule rule, IPing ping) {
...
int pingIntervalTime = Integer.parseInt(""
+ clientConfig.getProperty(
CommonClientConfigKey.NFLoadBalancerPingInterval,
Integer.parseInt("30")));'
// 设置时间间隔,并启动 PingTask 任务
setPingInterval(pingIntervalTime);
...
}
public void setPingInterval(int pingIntervalSeconds) {
if (pingIntervalSeconds < 1) {
return;
}
this.pingIntervalSeconds = pingIntervalSeconds;
...
// 启动 PingTask的任务,一秒后执行一次,以后每30s一次,对server list中的server执行一次isAlive()方法
setupPingTask(); // since ping data changed
}
有的博客跟踪源码的时候,走到了错误的构造器,所以说成每10s一次。
Ribbon 默认的负载均衡算法存在的问题
默认负载均衡算法就是轮询,这种不考虑服务实例可用性的方式会存在一些问题:
所以,在默认的情况下,SereviceA peer2
宕机之后,ServiceB
最多可能在 180+30+30s
即 4 分钟之后才会将其从 peer2
中移除。但一般也不建议修改默认的负载均衡算法,这个问题应该留给 hystrix
的熔断机制去解决。