您当前的位置: 首页 >  负载均衡

庄小焱

暂无认证

  • 2浏览

    0关注

    800博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

SpringCloud——负载均衡Ribbon原理与实战

庄小焱 发布时间:2021-05-04 19:55:14 ,浏览量:2

摘要

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于NetflixRibbon实现。通过Spring Cloud 的封装,可以让我们轻松地将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API 网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容实际上都是通过Ribbon 来实现的,包括后续我们将要介绍的Feign它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要。

主要讲解的是springCloud核心组件:Ribbon。Ribbon是一个客户端负载均衡解决方案,简单来说,就是从Eureka获取可用服务实例列表,然后将请求根据某种策略发到这些实例上面执行。Ribbon是一个为客户端提供负载均衡功能的服务,它内部提供了一个叫做ILoadBalance的接口代表负载均衡器的操作,我们可以在配置文件中Load Balancer后面的所有机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。

  • 负载均衡
  • 容错
  • 多协议(HTTP,TCP,UDP)支持异步和反应模型
  • 缓存和批处理

实际中的code
@Configuration
public class RibbonConfig {
    @Bean
    @LoadBalanced
    RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

消费另外一个的服务的接口,差不多是这样的:

@Service
public class RibbonService {
    @Autowired
    RestTemplate restTemplate;
    public String hi(String name) {
        return restTemplate.getForObject("http://eureka-client/hi?name="+name,String.class);
    }
}
主要包含如下组件:

  • IRule:根据特定算法中从服务列表中选取一个要访问的服务
  • IPing:在后台运行的一个组件,用于检查服务列表是否都活
  • ServerList:存储服务列表。分为静态和动态。如果是动态的,后台有个线程会定时刷新和过滤服务列表;
  • ServerListFilter:DynamicServerListLoadBalancer用于过滤从ServerList实现返回的服务器的组件
  • ServerListUpdater:被DynamicServerListLoadBalancer用于动态的更新服务列表
  • IClientConfig:定义各种配置信息,用来初始化ribbon客户端和负载均衡器
  • ILoadBalancer:定义软件负载平衡器操作的接口。动态更新一组服务列表及根据指定算法从现有服务器列表中选择一个服务
负载均衡

负载均衡是指通过负载均衡策略分配到多个执行单元上,常见的负载均衡方式有两种

  • 独立进程单元,通过负载均衡策略,将请求进行分发到不同执行上,类似于 Nginx

无论是硬件负载均衡还是软件负载均衡都会维护一个可用的服务端清单,然后通过心跳机制来删除故障的服务端节点以保证清单中都是可以正常访问的服务端节点,此时当客户端的请求到达负载均衡服务器时,负载均衡服务器按照某种配置好的规则从可用服务端清单中选出一台服务器去处理客户端的请求。这就是服务端负载均衡。。无论是硬件负载均衡还是软件负载均衡,它的工作原理都不外乎下面这张图:

  • 客户端行为,将负载均衡的策略绑定到客户端上,客户端会维护一份服务提供者列表,通过客户端负载均衡策略分发到不同的服务提供者。

即由请求发起方,客户端负责负载均衡。基本工作流程是,客户端请求出服务器列表之后在其中选择一个执行请求。 Ribbon的实现方式采用的就是客户端负载均衡,在Spring Cloud体系中,Ribbon在每个服务调用方,从Eureka获取服务实例列表,根据一定的负载均衡规则来选择实例执行请求。

这个的dubbo的负载有什么不同吗?

1.Dubbo负载均衡:支持4种(随机,轮循,最少活跃,hash),引入了JVM预热时间加权、权重自定义配置的规则,同时支持控制台动态配置权重值参数,所以是最灵活的。

2.Nginx负载均衡:支持4种,自带 轮询(支持权重)、IP_Hash(避免Session共享的问题)、最少连接数策略,可以扩展fair(响应时间)策略,更专注于功能。

3.Ribbon负载均衡:支持6种,不支持权重:支持轮询、随机、最少连接数、最短响应时间(随机+响应时间加权)、过滤异常节点+轮询,负载策略最全的。

Ribbon(客户端负载)

Ribbon 是 Netflix 公司开源的一款负载均衡组件,负载均衡的行为在客户端发生,所以属于上述第二种,一般而言,SpringCloud 构建以及使用时,会使用 Ribbon 作为客户端负载均衡工具。但是不会独立使用,而是结合 RestTemplate 以及 Feign 使用,Feign 底层集成了 Ribbon,不用额外的配置,开箱即用,所以使用 RestTemplate 充当网络调用工具,RestTemplate 是 Spring Web 下提供访问第三方 RESTFul Http 接口的网络框架。

客户端负载均衡包括如下功能:
  • 可动态配置
  • 获取服务实例列表,维护可用实例(客服端来实现获取列表)
  • 根据请求以及某种负载均衡规则选择服务实例(根据配置的均衡算法来实现选择实例)
  • 执行请求,响应处理
  • 重试
环境准备

注册中心选用阿里 Nacos,创建两个服务,生产者集群启动,消费者使用 RestTemplate + Ribbon 调用,调用总体结构如下:

生产者代码如下,将服务注册 Nacos,并对外暴露 Http Get 服务

消费者代码如下,将服务注册 Nacos,通过 RestTemplate + Ribbon 发起远程负载均衡调用,RestTemplate 默认是没有负载均衡的,所以需要添加 @LoadBalanced

如何获取注册中心服务实例

先来举个例子,当我们执行一个请求时,肯定要进行负载均衡对吧,这个时候代码跟到负载均衡获取服务列表源码的地方

解释一下上面标黄色框框的地方:

  • RibbonLoadBalancerClient:负责负载均衡的请求处理
  • ILoadBalancer:接口中定义了一系列实现负载均衡的方法,相当于一个路由的作用,Ribbon 中默认实现类 ZoneAwareLoadBalancer
  • unknown:ZoneAwareLoadBalancer 是多区域负载均衡器,这个 unkonwn 代表默认区域的意思
  • allServerList:代表了从 Nacos 注册中心获取的接口服务实例,upServerList 代表了健康实例

现在想要知道 Ribbon 是如何获取服务实例的就需要跟进 getLoadBalancer()

getLoadBalancer

首先声明一点,getLoadBalancer() 方法的语意是从 Ribbon 父子上下文容器中获取名称为 ribbon-produce,类型为 ILoadBalancer.class 的 Spring Bean。

之前在讲Feign的时候说过,Ribbon会为每一个服务提供者创建一个 Spring 父子上下文,这里会从子上下文中获取 Bean、

ZoneAwareLoadBalancer

ZoneAwareLoadBalancer 是一个根据区域(Zone)来进行负载均衡器,因为如果不同机房跨区域部署服务列表,跨区域的方式访问会产生更高的延迟,ZoneAwareLoadBalancer 就是为了解决此类问题,不过默认都是同一区域。ZoneAwareLoadBalancer 很重要,或者说它代表的负载均衡路由角色 很重要。进行服务调用前,会使用该类根据负载均衡算法获取可用 Server 进行远程调用,所以我们要掌握创建这个负载均衡客户端时都做了哪些

ZoneAwareLoadBalancer 是在服务第一次被调用时通过子容器创建

@Bean @ConditionalOnMissingBean 
// RibbonClientConfiguration 被加载,从 IOC 容器中获取对应实例填充到 ZoneAwareLoadBalancer
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
                                        ServerList serverList, ServerListFilter serverListFilter,
                                        IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
    ...
    return new ZoneAwareLoadBalancer(config, rule, ping, serverList,
            serverListFilter, serverListUpdater);
}

public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule,
                             IPing ping, ServerList serverList, ServerListFilter filter,
                             ServerListUpdater serverListUpdater) {
    // 调用父类构造方法
    super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
}

在 DynamicServerListLoadBalancer 中调用了父类 BaseLoadBalancer 初始化了一部分配置以及方法,另外自己也初始化了 Server 服务列表等元数据

public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
                                     ServerList serverList, ServerListFilter filter,
                                     ServerListUpdater serverListUpdater) {
    // 调用父类 BaseLoadBalancer 初始化一些配置,包括 Ping(检查服务是否可用)Rule(负载均衡规则)
    super(clientConfig, rule, ping);  
    // 较重要,获取注册中心服务的接口
    this.serverListImpl = serverList;
    this.filter = filter;
    this.serverListUpdater = serverListUpdater;
    if (filter instanceof AbstractServerListFilter) {
        ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
    }
    // 初始化步骤分了两步走,第一步在上面,这一步就是其余的初始化
    restOfInit(clientConfig);
}

先来说一下 BaseLoadBalancer 中初始化的方法,这里主要对一些重要参数以及 Ping、Rule 赋值,另外根据 IPing 实现类执行定时器,下面介绍 Ping 和 Rule 是什么

方法大致做了以下几件事情:
  1. 设置客户端配置对象、名称等关键参数
  2. 获取每次 Ping 的间隔以及 Ping 的最大时间
  3. 设置具体负载均衡规则 IRule,默认 ZoneAvoidanceRule,根据 server 和 zone 区域来轮询
  4. 设置具体 Ping 的方式,默认 DummyPing,直接返回 True
  5. 根据 Ping 的具体实现,执行定时任务 Ping Server
IPing 服务探测

IPing 接口负责向 Server 实例发送 ping 请求,判断 Server 是否有响应,以此来判断 Server 是否可用。接口只有一个方法 isAlive,通过实现类完成探测 ping 功能

public interface IPing {
    public boolean isAlive(Server server);
}

IPing 实现类如下:

  • PingUrl:通过 ping 的方式,发起网络调用来判断 Server 是否可用(一般而言创建 PingUrl 需要指定路径,默认是 IP + Port)
  • PingConstant:固定返回某服务是否可用,默认返回 True,表示可用
  • NoOpPing:没有任何操作,直接返回 True,表示可用
  • DummyPing:默认的类,直接返回 True,实现了 initWithNiwsConfig 方法
IRule 负载均衡

IRule 接口负责根据不用的算法和逻辑处理负载均衡的策略,自带的策略有7种,默认 ZoneAvoidanceRule

  • BestAvailableRule:选择服务列表中最小请求量的 Server
  • RandomRule:服务列表中随机选择 Server
  • RetryRule:根据轮询的方式重试 Server
  • ZoneAvoidanceRule:根据 Server 的 Zone 区域和可用性轮询选择 Server
  • ...

上面说过,会有两个初始化步骤,刚才只说了一个,接下来说一下 这个其余初始化方法 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();
    // 更新服务列表,enableAndInitLearnNewServersFeature 中定时器的执行的就是此方法
    updateListOfServers();
    if (primeConnection && this.getPrimeConnections() != null) {
        this.getPrimeConnections()
                .primeConnections(getReachableServers());
    }
    this.setEnablePrimingConnections(primeConnection);
    LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
}

获取服务列表以及定时更新服务列表的代码都在此处,值得仔细看着源码。关注其中更新服务列表方法就阔以了。

public void updateListOfServers() {
    List servers = new ArrayList();
    if (serverListImpl != null) {
        // 获取服务列表数据
        servers = serverListImpl.getUpdatedListOfServers();
        LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                getIdentifier(), servers);

        if (filter != null) {
            servers = filter.getFilteredListOfServers(servers);
            LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                    getIdentifier(), servers);
        }
    }
    // 更新所有服务列表
    updateAllServerList(servers);
}

第一个问题兜兜转转,终于要找到如何获取的服务列表了,serverListImpl 实现自 ServerList,因为我们使用的 Nacos 注册中心,所以 ServerList 的具体实现就是 NacosServerList

public interface ServerList {
    public List getInitialListOfServers();
    public List getUpdatedListOfServers();
}

ServerList 中只有两个接口方法,分别是 获取初始化服务列表集合、获取更新的服务列表集合,Nacos 实现中两个调用都是一个实现方法,可能设计如此,相当于 Ribbon 提供出接口 ServerList,注册中心开发者们谁想和 Ribbon 集成,那你就实现这个接口吧,到时候 Ribbon 负责调用 ServerList 实现类中的方法实现

Ribbon 和各服务注册中心之间,这种实现方式和 JDBC 与各数据库之间很像

  • 负载均衡客户端在初始化时向 Nacos 注册中心获取服务注册列表信息
  • 根据不同的 IPing 实现,向获取到的服务列表 串行发送 ping,以此来判断服务的可用性。没错,就是串行,如果你的实例很多,可以 考虑重写 ping 这一块的逻辑
  • 如果服务的可用性 发生了改变或者被人为下线,那么重新拉取或更新服务列表
  • 当负载均衡客户端有了这些服务注册类列表,自然就可以进行 IRule 负载均衡策略
服务列表定时维护

针对于服务列表的维护,在 Ribbon 中有两种方式,都是通过定时任务的形式维护客户端列表缓存。

  • 使用 IPing 的实现类 PingUrl,每隔10秒会去 Ping 服务地址,如果返回状态不是 200,那么默认该实例下线
  • Ribbon 客户端内置的扫描,默认每隔30秒去拉取 Nacos 也就是注册中心的服务实例,如果已下线实例会在客户端缓存中剔除

Ribbon 底层原理实现
  1. 创建 ILoadBalancer 负载均衡客户端,初始化 Ribbon 中所需的 定时器和注册中心上服务实例列表
  2. 从 ILoadBalancer 中,通过 负载均衡选择出健康服务列表中的一个 Server
  3. 将服务名(ribbon-produce)替换为 Server 中的 IP + Port,然后生成 HTTP 请求进行调用并返回数据

ILoadBalancer 是负责负载均衡路由的,内部会使用 IRule 实现类进行负载调用

public interface ILoadBalancer {
    public Server chooseServer(Object key);
  	...
}

chooseServer 流程中调用的就是 IRule 负载策略中的 choose 方法,在方法内部获取一个健康 Server

public Server choose(ILoadBalancer lb, Object key) {
    ... 
    Server server = null;
    while (server == null) {
        ...
        List upList = lb.getReachableServers();  // 获取服务列表健康实例
        List allList = lb.getAllServers();  // 获取服务列表全部实例
        int serverCount = allList.size();  // 全部实例数量
        if (serverCount == 0) {  // 全部实例数量为空,返回 null,相当于错误返回
            return null;
        }
        int index = chooseRandomInt(serverCount);  // 考虑到效率问题,使用多线程 ThreadLocalRandom 获取随机数
        server = upList.get(index);  // 获取健康实例
        if (server == null) {
            // 作者认为出现获取 server 为空,证明服务列表正在调整,但是!这只是暂时的,所以当前释放出了 CPU
            Thread.yield();
            continue;
        }
        if (server.isAlive()) {  // 服务为健康,返回
            return (server);
        }
        ...
    }
    return server;
}
简单说一下随机策略 choose 中流程
  1. 获取到全部服务、健康服务列表,判断全部实例数量是否等于 0,是则返回 null,相当于发生了错误
  2. 从全部服务列表里获取下标索引,然后去 健康实例列表获取 Server
  3. 如果获取到的 Server 为空会放弃 CPU,然后再来一遍上面的流程,相当于一种重试机制
  4. 如果获取到的 Server 不健康,设置 Server 等于空,再歇一会,继续走一遍上面的流程

比较简单,有小伙伴可能就问了,如果健康实例小于全部实例怎么办?这种情况下存在两种可能

  1. 运气比较好,从全部实例数量中随机了比较小的数,刚好健康实例列表有这个数,那么返回 Server
  2. 运气比较背,从全部实例数量中随机了某个数,健康实例列表数量为空或者小于这个数,直接会下标越界异常

思考问题 在Ribbon选择实例的时候为什么是先获取的全部的是实例在去获取健康的实例?为什么不直接从健康实例中选择实例呢? 面试问题 Ribbon的自定义配置(java代码方式)生效条件?
  • 第一种方式:将TestConfiguration类放在application启动类上层
  • 第二种方式:将TestConfiguration类放在application启动类同层及以下,
需在application启动类上添加注解 : 
@ComponentScan(excludeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION, value = ExcludeFromComponentScan.class) })

需在application启动类同层添加接口类ExcludeFromComponentScan.java: 
package com.mmzs.cloud;
    
    public @interface ExcludeFromComponentScan {
    
    }

并在ExcludeFromComponentScan.class接口添加注解@ExcludeFromComponentScan;并且注释如下内容: 

@Autowired
IClientConfig config;
Ribbon 饥饿加载(eager-load)模式

我们在搭建完springcloud微服务时,经常会发生这样一个问题:我们服务消费方调用服务提供方接口的时候,第一次请求经常会超时,再次调用就没有问题了。

为什么会这样?

主要原因是Ribbon进行客户端负载均衡的Client并不是在服务启动的时候就初始化好的,而是在调用的时候才会去创建相应的Client,所以第一次调用的耗时不仅仅包含发送HTTP请求的时间,还包含了创建RibbonClient的时间,这样一来如果创建时间速度较慢,同时设置的超时时间又比较短的话,从而就会很容易发生请求超时的问题。

解决方法

既然超时的原因是第一次调用时还需要创建RibbonClient,那么我们能不能提前创建RibbonClient呢?

既然我们都能想到,那么SpringCloud开发者肯定也能想到。

所以我们可以通过设置下面两个属性来提前创建RibbonClient:

参考博文

https://www.jianshu.com/p/9d941f5e0df9

关注
打赏
1657692014
查看更多评论
立即登录/注册

微信扫码登录

0.4497s