一、背景介绍

项目中使用到的SpringCloud Alibaba这一套微服务架构中服务注册与发现Nacos兼容了Feign,而Feign默认集成了Ribbon,当Nacos下使用Feign默认实现了负载均衡的效果。即使是默认集成了,也要追根溯源。



二、过程

负载均衡是什么?

将请求分摊到多个服务器上去执行

为什么要负载均衡?

分担压力,当开发的应用同时被成千上万,甚至更多用户同时访问的时候,并发问题就出现了,如果所有的请求都使用同一台机器,可能这个机器无法承受同时的高并发,这时候就可以将大量请求分发给不同的机器,应用的处理性能(吞吐量、网络处理能力)就需要得到提高
故障转移,实现高可用。如果某台服务器坏了(机器停机、进程异常等),其他服务器可以提供相同服务,顶替上岗
安全防护。可以实现过滤,如黑白名单处理等

一个请求是如何到达服务后端的?

【SpringCloud】-Ribbon负载均衡_ribbon



【SpringCloud】-Ribbon负载均衡_spring_02


①、Nginx:通过在前端接收到来自客户端的请求,将请求分发到后端服务器,实现负载均衡分发HTTP请求

假设我现在在浏览器访问【**https://internetbar.tech/root/user-service/api/login**】这个地址,通过HTTP请求到达【internetbar.tech】这个服务器的【Nginx】,Nginx接收到请求后根据请求的路径,在默认配置文件【nginx.conf】中找到对应的配置。 Nginx的配置文件中会有对GateWay的代理配置。


【SpringCloud】-Ribbon负载均衡_spring cloud_03


在http字段中配置的upstream api模块中定义了需要反向代理的服务器地址及端口,将要进行负载均衡的时候就会从这两个服务器进行选择。根据请求路径中的/root/进行匹配发现配置了proxy_pass模块,结合配置的负载均衡策略—8080端口的权重大于6688的权重,Nginx优先将请求转发到8080这个端口对应的服务器地址,在将请求转发给8080这个服务之前,Nginx还可以进行一些预处理操作,比方说请求的重定向、添加请求头等等

②、GateWay:路由转发、负载均衡

此时,8080端口的Gateway接收到通过Nginx代理的请求,GateWay与Nacos集成,并且GateWay配置的各个微服务的信息都通过Nacos做了服务配置。

请求到达GateWay服务的过滤器后,根据配置文件中预先定义的路由规则:

【SpringCloud】-Ribbon负载均衡_Nginx_04


发现请求的Url中包含/user-service/,满足配置的断言规则,GateWay确定请求要转发到的目标服务是【internetbar-provider-user】。GateWay定时从Nacos注册中心拉取服务列表(Nacos中记录了服务名、ip地址、端口号等服务实例信息)动态地获取可用的服务实例,拉取到列表后发现此时【internetbar-provider-user】服务有多个实例。

③、Ribbon:GateWay与Ribbon来实现对服务的负载均衡

在转发请求时,GateWay会利用Ribbon进行负载均衡,利用Ribbon提供的负载均衡算法(默认是使用轮询算法,可以自定义策略)选择一个健康的服务实例来处理请求




根据负载均衡发生的位置不同分为两类

①、服务端负载均衡:发生在服务提供者一方,如nginx负载均衡

客户端发送请求到达Nginx,Nginx通过负载均衡策略选择其中的一个服务地址进行访问,此时Nginx作为了服务端

【SpringCloud】-Ribbon负载均衡_spring_05

②、客户端负载均衡:发生在服务请求一方

在图中GateWay作为了客户端,通过集成ribbon根据负载均衡策略,在发送请求前通过负载均衡策略选择目标服务器,再将请求分发到不同的服务端。此时发送请求的GateWay作为客户端

【SpringCloud】-Ribbon负载均衡_spring_06



Ribbon是什么?

是基于Netflix Ribbon实现的一套客户端负载均衡的工具。是Spring Cloud的一个组件,通过提供使用提供的注解我们就能自动化的实现负载均衡

Ribbon的作用是什么?

实现负载均衡和服务调用

如何使用Ribbon?

添加@LoadBalance注解

package cn.itcast.order;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

    /**
     * @Description: 将RestTemplate注册到容器中
     * @Author: denglimei
     * @Date: 2023/3/13 11:28
     * @return: org.springframework.web.client.RestTemplate
     **/
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

此时我们通过浏览器进行四次访问,我们看看结果:

【SpringCloud】-Ribbon负载均衡_spring cloud_07

说明调用成功,此时我们看看idea中的四次访问分别是怎么样的一个顺序呢?

【SpringCloud】-Ribbon负载均衡_Nginx_08

根据每次对userservice这个服务的访问来看,是采用了轮询的策略。内部到底如何进行负载均衡的?策略有哪些?是否默认采用用轮询,我们来看看源码如何说的!


Ribbon负载均衡的策略由哪些?

策略名

策略描述

实现说明

BestAvailableRule

选择一个最小的并发请求的server

逐个考察Server,如果Server被tripped了,则忽略,在选择其中ActiveRequestsCount最小的server

AvailabilityFilteringRule

过滤掉因为连接失败被标记为circuit tripped的后端server,和高并发的后端server(active connections超过配置的阈值)

使用一个AvailabilityPredicate来包含过滤server的逻辑,检查status里记录的各个server的运行状 态

WeightedResponseTimeRule

根据相应时间分配一个weight,相应时间越长,weight越小,被选中的可能性越低

一个后台线程定期的从status里面读取评价响应时间,为每个server计算一个weight。Weight的计算也比较简单,responsetime减去每个server自己平均的responsetime是server的权重。当刚开始运行,没有形成statas时,使用RoundRobinRule策略选择server。

RetryRule

对选定的负载均衡策略进行重试机制

在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server

RoundRobinRule

轮询方式轮询选择server

轮询index,选择index对应位置的server

RandomRule

随机选择一个server

在index上随机,选择index对应位置的server

ZoneAvoidanceRule

复合判断server所在区域的性能和server的可用性选择server

使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。


Ribbon自定义负载均衡策略

方式一:代码方式,定义随机策略

package cn.itcast.order;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

    /**
     * @Description: 将RestTemplate注册到容器中
     * @Author: denglimei
     * @Date: 2023/3/13 11:28
     * @return: org.springframework.web.client.RestTemplate
     **/
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    /**
     * @Description: 修改负载均衡策略方式
     * @Author: denglimei
     * @Date: 2023/3/13 19:56
     * @return: com.netflix.loadbalancer.IRule
     **/
    @Bean
    public IRule randomRule() {
        return new RandomRule();
    }
}



方式二:配置文件方式

userservice:
  ribbon:
    MFLoadBalancerRuleClassName: com.netlix.loadbalancer.RandomRule #负载均衡规则



不同:
代码方式:只要是order访问任何其他服务都采用随即方式
配置文件方式:只针对order这个服务

优缺点:
代码方式:配置灵活,但修改时需要重新打包发布
配置方式:直观,方便,无需重新打包发布,但是无法做全局配置

通过浏览器访问order服务,五次调用效果如下:

其中1-4次分别调用了userservice的8081这个端口;第5次调用了userservice的8082这个端口

【SpringCloud】-Ribbon负载均衡_Nginx_09




Ribbon实现原理

1、获取请求信息(url、服务名称……)
@Override
	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,
				this.requestFactory.createRequest(request, body, execution));
	}

【SpringCloud】-Ribbon负载均衡_spring cloud_10




实现负载均衡的方法—execute

public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        return this.execute(serviceId, (LoadBalancerRequest)request, (Object)null);
}



对外暴露的接口,也是实现负载均衡的关键,具体实现逻辑如下:

public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
       //拉取服务列表
        ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
        //选择服务
        Server server = this.getServer(loadBalancer, hint);
        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, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
        }
    }



this.getLoadBalancer(serviceId)方法内部实现逻辑:

2、根据服务名称拉取服务列表

【SpringCloud】-Ribbon负载均衡_spring_11


this.getServer(loadBalancer, hint)方法内部实现逻辑

protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
        return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
}



3、选择服务
public Server chooseServer(Object key) {
        if (ENABLED.get() && this.getLoadBalancerStats().getAvailableZones().size() > 1) {
            Server server = null;

            try {
                LoadBalancerStats lbStats = this.getLoadBalancerStats();
                Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
                logger.debug("Zone snapshots: {}", zoneSnapshot);
                if (this.triggeringLoad == null) {
                    this.triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2D);
                }

                if (this.triggeringBlackoutPercentage == null) {
                    this.triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999D);
                }

                Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, this.triggeringLoad.get(), this.triggeringBlackoutPercentage.get());
                logger.debug("Available zones: {}", availableZones);
                if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
                    String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                    logger.debug("Zone chosen: {}", zone);
                    if (zone != null) {
                        BaseLoadBalancer zoneLoadBalancer = this.getLoadBalancer(zone);
                        server = zoneLoadBalancer.chooseServer(key);
                    }
                }
            } catch (Exception var8) {
                logger.error("Error choosing server using zone aware logic for load balancer={}", this.name, var8);
            }

            if (server != null) {
                return server;
            } else {
                logger.debug("Zone avoidance logic is not invoked.");
                return super.chooseServer(key);
            }
        } else {
            logger.debug("Zone aware logic disabled or there is only one zone");
        //选择服务
            return super.chooseServer(key);
        }
    }



4、轮询

super.chooseServer(key)内部具体业务逻辑

【SpringCloud】-Ribbon负载均衡_spring cloud_12


【SpringCloud】-Ribbon负载均衡_spring_13




负载均衡实现后的返回如下结果:

【SpringCloud】-Ribbon负载均衡_spring cloud_14


Ribbon饥饿加载和懒加载

懒加载

默认是采用,即第一次访问时才会去创建LoadBalanceClient去拉取服务列表,第一次请求时间会很长,但后续请求时间会变短

【SpringCloud】-Ribbon负载均衡_ribbon_15


我们在通过访问浏览器看看效果:

【SpringCloud】-Ribbon负载均衡_Nginx_16



饥饿加载

急不可耐,当服务启动时就创建LoadBalanceClient去拉取服务列表,降低第一次访问的耗时
需要在配置文件中进行配置

ribbon:
  eager-load:
    enabled: true # 开启饥饿加载
    clients: userservice # 指定对userservice这个服务饥饿加载

当服务启动时查看,效果如下:

【SpringCloud】-Ribbon负载均衡_Nginx_17



三、总结

Ribbon帮助我们在去选择对应策略的服务实例,SpringCloud中许多组件都集成了Ribbon,Ribbon提供的内置策略,我们也可以自定义策略,虽然知道SpringCloud的某些组件集成了Ribbon,但是怎么集成的Ribbon?Ribbon如何和其他组件关联的?我们还要知其所以然。