05、集成Ribbion实现客户端负载均衡

负载均衡

负载均衡,英文名称为Load Balance,其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行。

在分布式架构中,某个服务后台肯定存在多个副本,这个时候就需要负载均衡组件,将流量分摊到这些副本中。

可以通过硬件或软件实现负载均衡,比如F5、Nginx、API网关等。

服务端负载均衡

服务器端负载均衡就是在服务端进行请求处理,比如使用Nginx时,请求到达Nginx,由其进行负载均衡处理,将请求转发到具体的后台服务。

客户端不知道负载均衡的具体算法,也不知道具体后台的服务地址。

 

客户端负载均衡

客户端负载均衡就是负载均衡器在客户端,由客户端去注册中心或者配置中拉取服务端的服务信息,在本地维护一个可用服务列表,发送请求时,直接在本地选择一个服务地址,客户端可以自己控制负载均衡算法。

在服务之间进行RPC 调用时,我们就需要一个客户端均衡器,这样才能实现多个提供方时,消费者可以选择哪一个提供方服务。
 

负载均衡算法

1、轮询法

将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。

2、随机法

通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,

其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。

3、源地址哈希法

源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

4、加权轮询法

不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。

5、加权随机法

与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。

6、最小连接数法

最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前

积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器

Ribbion

简介

GitHub 地址

Ribbon是一个客户端负载平衡器,它提供以下功能

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

停止维护声明

 

在2018年底,Netflix宣布Hystrix 进入维护模式。Ribbon自 2016 年以来一直处于类似状态。

在Spring Cloud 2020.x 版本中,已将这些模块完全移除,所有Ribbon 已成为了过去式,但是已经大规模使用,研究下还是可以的。。。

使用案例

1. 修改版本

修改feign-demo pom 文件,降低相关版本。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.pearl</groupId>
    <artifactId>feign-demo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>account-service</module>
        <module>order-service</module>
    </modules>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <!--Spring-->
<!--        <spring.boot.version>2.5.2</spring.boot.version>
        <spring.cloud.version>2020.0.3</spring.cloud.version>-->
        <spring.cloud.alibaba.version>2.2.6.RELEASE</spring.cloud.alibaba.version>
        <spring.cloud.version>Hoxton.SR9</spring.cloud.version>
        <spring.boot.version>2.2.13.RELEASE</spring.boot.version>
    </properties>

    <!--Spring版本-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring.cloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <!--不使用Ribbon 进行客户端负载均衡-->
<!--            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                </exclusion>
            </exclusions>-->
        </dependency>
        <!--SpringCloud Feign在Hoxton.M2 RELEASED版本之后不再使用Ribbon而是使用spring-cloud-loadbalancer-->
<!--        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

</project>

2.测试

添加了上面这些依赖后,因为Feign (低版本)默认集成了Ribbon ,所以不用再添加 Ribbon 了。
&nbsp;
首先修改订单服务,让其接口,返回当前服务的端口,然后启动两个不一样端口的订单服务。

    @Value("${server.port}")
    String port;

    @GetMapping("insert")
    public Order insertOrder(Long accountId, String commodityCode, Long count, Long money) {

        // 模拟下订单并返回
        Order order = new Order();
        order.setAccountId(accountId);
        order.setCommodityCode(commodityCode);
        order.setCount(count);
        order.setMoney(1L);
        order.setPort(port);
        return order;
    }

访问账户服务,发现端口在不停切换,说明Ribbon 已经生效,而且使用的是轮询算法。
&nbsp;
&nbsp;

3. Ribbon 执行流程分析

Feign 接口生成代码对象后,进行方法执行,是由MethodHandler(方法处理器)调用Client 去执行的,可以看到同步方法执行器中的Client 是LoadBalancerFeignClient(负载均衡客户端),由它代理实际的 Http Client。

&nbsp;
LoadBalancerFeignClient的execute 方法,

            // 创建请求URI =》http://order-service/order/insert?accountId=111&commodityCode=iphone11&count=1&money=100
            URI asUri = URI.create(request.url());
            // 主机名,这里是服务注册,所以是服务名=》order-service
            String clientName = asUri.getHost();
            // 去掉主机名的URI 
            URI uriWithoutHost = cleanUrl(request.url(), clientName);
            // 创建Ribbon 请求对象
            RibbonRequest ribbonRequest = new RibbonRequest(this.delegate, request, uriWithoutHost);
            // 获取Client 配置信息,比如超时时间
            IClientConfig requestConfig = this.getClientConfig(options, clientName);
            // 获取具体的负载均衡器并执行负载均衡请求。
            return ((RibbonResponse)this.lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse();

executeWithLoadBalancer 方法中,会创建一个LoadBalancerCommand 对象(负载均衡执行命令类),然后调用其submit 进行请求提交,获取响应,并经过进一步处理,最终Feign 解码返回给客户端。

    public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {

        // LoadBalancerCommand=》 负载均衡命令行,封装了请求、可用服务端列表等信息。
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
        // 提交请求命令
        try {

            return command.submit(
                new ServerOperation<T>() {

                    @Override
                    public Observable<T> call(Server server) {

                        // 最终调用的URI 地址:http://192.168.0.100:9112/order/insert?accountId=111&commodityCode=iphone11&count=1&money=100
                        URI finalUri = reconstructURIWithServer(server, request.getUri());
                        S requestForServer = (S) request.replaceUri(finalUri);
                        try {

                            // 调用OK HTTP 执行远程请求
                            return 
                                Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                        } 
                        catch (Exception e) {

                            return Observable.error(e);
                        }
                    }
                })
                .toBlocking()
                .single();
        } catch (Exception e) {

        // 省略.....
        }

    }

4. Ribbon 负载均衡算法流程

在第三步的selectServer选择服务器方法中,会在负载均衡器中,查询一个可用的服务并返回。

    private Observable<Server> selectServer() {

        return Observable.create(new OnSubscribe<Server>() {

            @Override
            public void call(Subscriber<? super Server> next) {

                try {

                    // FeignLoadBalancer.getServerFromLoadBalancer
                    Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);   
                    next.onNext(server);
                    next.onCompleted();
                } catch (Exception e) {

                    next.onError(e);
                }
            }
        });
    }

getServerFromLoadBalancer方法,最终会调用BaseLoadBalancer. chooseServer()方法,在该方法中,会调用负载均衡算法规则类,计算出最终需要调用哪个服务。可以看到默认的规则类为ZoneAvoidanceRule,该规则维护了一个组合断言器和一个轮询算法规则。
&nbsp;
ZoneAvoidanceRule的choose 方法调用的是父类PredicateBasedRule的choose 方法,该方法会获取ILoadBalancer对象,在该对象中维护服务名、可用服务、服务名对应的所有服务列表、Ribbion客户端配置等相关信息。

    @Override
    public Server choose(Object key) {

        // 1. 获取ILoadBalancer 
        ILoadBalancer lb = getLoadBalancer();
        // 2. 获取服务
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        if (server.isPresent()) {

            return server.get();
        } else {

            return null;
        }       
    }

chooseRoundRobinAfterFiltering方法中,会调用incrementAndGetModulo方法,该方法是轮询算法的实现,会计算出当前服务应该使用的坐标数。

    private int incrementAndGetModulo(int modulo) {

        // modulo为后台副本数
        for (;;) {

            // 获取当前坐标数,是AtomicInteger,所以是线程安全的。
            int current = nextIndex.get();
            // 计算下一个坐标 
            int next = (current + 1) % modulo;
            // 操作完成后用CAS操作将next赋值给nextServerCyclicCounter
            if (nextIndex.compareAndSet(current, next) && current < modulo)
                return current;
        }
    }

最终通过轮询算法,返回了我们当前需要调用的服务真实的IP及端口信息,然后将请求转给HTTP Client发送。
&nbsp;