开启springcloud全家桶5:探索负载均衡组件 Ribbon实现与原理

Posted 黄小斜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开启springcloud全家桶5:探索负载均衡组件 Ribbon实现与原理相关的知识,希望对你有一定的参考价值。

前段时间,笔者为了解决微服务多版本共存调用的问题,笔者深入调研了Ribbon,并将调研的内容记录了下来,以供后续查阅。Ribbon是Spring Cloud核心组件之一,它提供的最重要的功能就是负载均衡,和硬件负载均衡F5不同,它的负载均衡是基于客户端的,Zuul网关和Feign可以通过Ribbon轻松的实现服务的负载均衡,同时避免了与业务无关的冗余代码。在这篇文章中,笔者会讲解负载均衡请求调用的流程以及过程中涉及的一些重要接口。

1. Ribbon示例

下面的代码是利用ribbon实现负载均衡。

@Bean
@LoadBalanced
RestTemplate restTemplate() {
    return new RestTemplate();
}

@Test
public void productInfo() {
    String id = UUID.randomUUID().toString();
    String url = "http://shop-product/product/info?id=" + id;
    Object result = this.restTemplate.getForObject(url, HashMap.class);
    System.out.println(JsonUtil.BeanToJson(result));
}

一个请求如果被Ribbon代理之后会,请求的执行流程如下图所示,接下来笔者会详细讲解泳道图中一些重要的过程

2. @LoadBalanced原理

在Ribbon示例中可以看到,Ribbon通过一个@LoadBalanced注解就实现了RestTemplate请求的负载均衡,那么他的原理是什么呢?

RestTemplate在发送请求的时候会被ClientHttpRequestInterceptor拦截,LoadBalancerInterceptor是ClientHttpRequestInterceptor的实现类,它的作用就是用于RestTemplate的负载均衡,LoadBalancerInterceptor将负载均衡的核心逻辑交给了loadBalancer,核心代码如下所示

public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
		final ClientHttpRequestExecution execution) throws IOException {
	final URI originalUri = request.getURI();
	String serviceName = originalUri.getHost();
	Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
	return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}

@LoadBalanced注解是属于Spring,而不是Ribbon的,Spring在初始化容器的时候,如果检测到Bean被@LoadBalanced注解,Spring会为其设置LoadBalancerInterceptor的拦截器。

@LoadBalanced注解定义如下所示

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

可以看到这个注解可以注释在类变量(field),方法参数(parameter)上,这个注解也被@Qualifier修饰,目的就是为了Spring容器注入参数的时候,只选择注入被LoadBalanced注解修饰的bean对象,比如LoadBalancerAutoConfiguration如下代码

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

3. 获取服务实例列表

Ribbon使用ServerList接口抽象服务实例列表,Ribbon获取服务实例有如下两种方法,可以使用参数{service-name}.ribbon.NIWSServerListClassName进行选择。它的配置可选项目如下

配置参数意义
com.netflix.loadbalancer.ConfigurationBasedServerList使用配置文件
com.netflix.loadbalancer.DiscoveryEnabledNIWSServerList使用注册中心

3.1 配置文件

配置参数为com.netflix.loadbalancer.ConfigurationBasedServerList。在没有使用注册中心的情况下,Ribbon可以通过配置文件手动列举服务实例的地址,它的命令规范是{{服务名}}.ribbon.listOfServers,Ribbon通过ConfigurationBasedServerList类实现配置服务列表,多个服务实例用逗号隔开

spring.application.name=shop-order
shop-product.ribbon.listOfServers=http://localhost:8001,http://localhost:8002

使用配置文件是不是意味着服务实例列表就不会不变了呢?不是的,其实还会定时更新

3.2 利用注册中心获取

利用配置文件获取服务实例列表扩展性很差,因为在服务实例上线或者下线的情况下,需要手动修改配置文件,扩展性很低,一个健壮的微服务系统会采用注册中心的方式维护服务的上下线。Ribbon可以使用DiscoveryEnabledNIWSServerList维护和Eureka之间的服务上下线

4. 动态更新服务实例列表

服务实例上下线在微服务系统中是一个非常常见的场景,Ribbon也实现了该功能。Ribbon定时更新的接口抽象为ServerListUpdater。当Ribbon从注册中心获取了服务实例列表之后,Ribbon需要动态更新服务实例列表,抽象接口为ServerListUpdater,更新的方式有两种,一种是通过定时任务定时拉取服务实例列表,另一种是通过Eureka服务事件通知的方式。Ribbon可以通过配置项{service-name}.ribbon.ServerListUpdaterClassName进行选择更新方式,配置可选项目如下所示

配置参数意义
com.netflix.loadbalancer.PollingServerListUpdater定时拉取
com.netflix.niws.loadbalancer.EurekaNotificationServerListUpdater事件通知

4.1 定时拉取

Ribbon会使用一个定时任务线程池定时拉取更新数据。

Ribbon也提供了一些参数,用于控制拉取的实现细节。

参数名称意义
{service-name}.ribbon.ServerListRefreshInterval更新频率
DynamicServerListLoadBalancer.ThreadPoolSize定时更新的线程数目

PollingServerListUpdater只是控制了线程池的动作,但是具体的业务逻辑则是封装在UpdateAction。

4.2 事件通知

和PollingServerListUpdater不同的是,如果注册中心是Eureka,可以采用事件通知的方式,即当Eureka注册中心发生注册信息变更的时候,那么就将消息发送到事件监听者,Ribbon使用EurekaNotificationServerListUpdater实现类进行更新,首先会创建一个Eureka监听器,当接口接受到通知事件之后,会将更新逻辑提交到线程池中执行,更详细的代码如下

public synchronized void start(final UpdateAction updateAction) {
    if (isActive.compareAndSet(false, true)) {
        this.updateListener = new EurekaEventListener() {
            @Override
            public void onEvent(EurekaEvent event) {
                if (event instanceof CacheRefreshedEvent) {
                    if (!updateQueued.compareAndSet(false, true)) {  // if an update is already queued
                        logger.info("an update action is already queued, returning as no-op");
                        return;
                    }

                    try {
                        refreshExecutor.submit(new Runnable() {
                            @Override
                            public void run() {
                                  updateAction.doUpdate();
                            }
                        }); 
                    } catch (Exception e) {
                        updateQueued.set(false);  // if submit fails, need to reset updateQueued to false
                    }
                }
            }
        };
        //注册事件监听器,省略不重要的代码
    } else {
        logger.info("Update listener already registered, no-op");
    }
}

5. 对服务进行心跳检测

服务列表中的服务实例未必一直都处于可用的状态,Ribbon会对服务实例进行检测,PingerStrategy接口抽象检测的策略,Ribbon默认采用了串行的方式进行检测,如果有必要,我们可以通过该接口实现并行的检测方式。Pinger会定时通过PingerStrategy获取更新的服务实例,并调用监听者。

// 避免在检测过程中服务实例列表发生变更,预先进行复制,代码省略

//在线服务实例列表
final List<Server> newUpList = new ArrayList<Server>();
//发生状态变更的服务实例列表
final List<Server> changedServers = new ArrayList<Server>();

for (int i = 0; i < numCandidates; i++) {
    boolean isAlive = results[i];
    Server svr = allServers[i];
    boolean oldIsAlive = svr.isAlive();

    svr.setAlive(isAlive);

    if (oldIsAlive != isAlive) {
        changedServers.add(svr);
        logger.debug("LoadBalancer [{}]:  Server [{}] status changed to {}", 
            name, svr.getId(), (isAlive ? "ALIVE" : "DEAD"));
    }

    if (isAlive) {
        newUpList.add(svr);
    }
}

除此之外,还有一个IPing接口,它的目的是检测单个服务的可用性,对于Eureka来说使用的是NIWSDiscoveryPing策略

6. 服务路由

ServerListFilter接口的作用就是从一批接口中选择一些符合条件的接口并返回。接口定义如下所示

它有什么作用呢?比如说上文中笔者谈到的,如果笔者希望得到某个版本的微服务实例,那么这个接口就能派上用场了,但是Ribbon没有这样的实现,如果笔者需要解决该需求就要自己开发接口了。在默认情况下,Ribbon采取了区域优先的过滤策略(ZoneAffinityServerListFilter),也就是说,优先使用和当前调用者一样的区域微服务实例。

7. 负载均衡调度器

从ServerListFilter获取到一个微服务实例集合后,ILoadBalancer需要使用某个策略从集合中选择一个服务实例, 而策略的抽象接口为IRule,如下所示

public interface IRule{
    //省略一些不重要的方法
    public Server choose(Object key);

}

选择服务实例之后,ILoadBalancer在调用过程中,会记录请求的执行结果,比如请求的失败成功情况,调用耗时等,IRule接口也可以根据这些信息决定是否使用某个Server。

Ribbon提供了七种负载均衡策略,默认的负载均衡策略是轮训策略。

名称解释
RoundRobinRule轮训策略
RandomRule随机策略
BestAvailableRule过滤出故障服务器后,选择一个并发量最小的
WeightedResponseTimeRule针对响应时间加权轮询
AvailabilityFilteringRule可用过滤策略,先过滤出故障的或并发请求大于阈值的一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个;
ZoneAvoidanceRule从最佳区域实例集合中选择一个最优性能的服务实例
RetryRule选择一个Server,如果失败,重新选择一个Server重试

 

 

以上是关于开启springcloud全家桶5:探索负载均衡组件 Ribbon实现与原理的主要内容,如果未能解决你的问题,请参考以下文章

SpringCloud全家桶包含哪些组件?

开启springcloud全家桶:springcloud常见面试题

※Spring全家桶从入门到X神--Ribbon负载均衡+源码分析

开启springcloud全家桶1:springcloud套餐简介

Spring Cloud Alibaba全家桶——微服务负载均衡器Ribbon与LoadBalancer

开启springcloud全家桶10:用SpringCloud Admin快速搭建你的监控中心