一、背景介绍

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



二、过程

负载均衡是什么?

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

为什么要负载均衡?

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

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

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_spring cloud



【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_Nginx_02


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

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


【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_ribbon_03


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

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

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

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

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_spring_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负载均衡实战及源码解析,如何结合Nacos_ribbon_05

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

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

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_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负载均衡实战及源码解析,如何结合Nacos_spring_07

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

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_spring_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负载均衡实战及源码解析,如何结合Nacos_spring cloud_09




Ribbon实现原理——源码解析

核心思想:对加了@LoadBalanced修饰的所有的RestTemplate加一个LoadBalancerInterceptor拦截器,拦截器会对http调用进行拦截,把从Nacos发现的service放在负载均衡器中,负载均衡器根据负载均衡策略选择一个最终的ip和port,然后通过HttpClient进行http调用



我们还是借鉴网上大家都比较熟知的一张图先从宏观上展示一下整体流程

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_负载均衡_10


接下来我们就从源码来分析每一步的执行流程

1、获取请求信息(url、服务名称……)

Ribbon实现的拦截器中,会通过负载均衡器选择一个可用的服务实例,并且执行实际的HTTP请求。其中会Assert.state()方法,用来检测服务吗是否存在,用于提前发现问题

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_Nginx_11

@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负载均衡实战及源码解析,如何结合Nacos_负载均衡_12




实现负载均衡的方法—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);
        //选择服务:通过负载均衡器loadBalancer根据负载均衡算法挑选一个server
        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.getServer()方法中实现,我们接下来看看this.getLoadBalancer如何实现的。


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

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

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_spring cloud_13


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);
        }
    }

我们重点关注最后一行代码:super.chooseServer(key);我们通过debug往下点进去看

4、轮询

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

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_ribbon_14


【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_spring_15




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

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_spring_16


Ribbon如何和Nacos保持联系的?

服务注册: 微服务启动时,通过@EnableDiscoveryClient 注解,服务可以将自己注册到 Nacos 服务注册中心。注册后,Nacos 就知道该服务的存在和可用实例。

@SpringBootApplication
@EnableDiscoveryClient
public class MyMicroserviceApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyMicroserviceApplication.class, args);
    }
}



服务消费: @LoadBalanced 注解使得 RestTemplate 具备了负载均衡的能力,能够通过服务名调用服务。Ribbon 作为一个客户端负载均衡器,可以通过@LoadBalanced 注解和 Nacos 集成的 RestTemplate 或 WebClient 来调用其他服务。@LoadBalanced 注解会使得 Ribbon 在发送请求时,能够通过服务名来自动选择一个可用实例

@Configuration
public class MyConfiguration {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}



服务发现与负载均衡: Ribbon 通过 @LoadBalanced 注解,能够在调用其他服务时,自动利用 Nacos 的服务注册信息进行负载均衡。Ribbon 会从 Nacos 获取服务实例列表,并根据配置的负载均衡策略选择合适的实例。




Ribbon如何保证获取到的Nacos实例是最新的?

Ribbon 使用了定时任务(Scheduled Task)的机制来定期执行服务实例列表的轮询操作,任务的目的是从服务注册中心获取最新的服务实例列表

通过start方法会启动定时任务,并且通过synchronized确保多线程环境中只有一个线程能够成功启动任务

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_负载均衡_17


Ribbon通过一个后台线程周期性地向Nacos注册中心发送请求,获取最新的服务实例列表,然后将获取到的服务实例更新到本地缓存中,这样Ribbon就能够在程序运行时去动态的获取到服务实例的变化啦!!


Ribbon饥饿加载和懒加载

懒加载

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

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_ribbon_18


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

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_spring cloud_19



饥饿加载

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

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

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

【SpringCloud】-Ribbon负载均衡实战及源码解析,如何结合Nacos_ribbon_20



三、总结

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




如果有想要交流的内容欢迎在评论区进行留言,如果这篇文档受到了您的喜欢那就留下你点赞+收藏+评论脚印支持一下博主~