Spring Cloud - Ribbon 原理解析

负载均衡分类

负载均衡技术可以按照软件或硬件进行分类,也可以按照服务器列表存放的位置划分为服务端负载和客户端负载均衡。

a、硬件负载均衡

F5 就是常见的硬件负载均衡产品。

优点:性能稳定,具备很多软件负载均衡不具备的功能,如应用交换,会话交换、状态监控等。

缺点:设备价格昂贵、配置冗余,没有软件负载均衡灵活,不能满足定制化需求。

b、软件负载均衡

Nginx:性能好,可以负载超过 1W。工作在网络的7层之上,可以针对http应用做一些分流的策略。Nginx也可作为静态网页和图片服务器。Nginx仅能支持http、https和Email协议。

LVS(Linux Virtual Server):是一个虚拟服务器集群系统,采用 IP 地址均衡技术和内容请求分发技术实现负载均衡。接近硬件设备的网络吞吐和连接负载能力。抗负载能力强、是工作在网络4层之上仅作分发之用。自身有完整的双机热备方案,如LVS+Keepalived。软件本身不支持正则表达式处理,不能做动静分离。

c、服务端负载均衡

Nginx 和 F5 都可以划分到服务端的负载均衡里面,后端的服务器地址列表是存储在后端服务器中或者存在专门的 Nginx 服务器或 F5 上。

服务器的地址列表的来源是通过注册中心或者手动配置的方式来的。

d、客户端负载均衡

终于轮到 Ribbon 登场了,它属于客户端负载均衡器,客户端自己维护一份服务器的地址列表。这个维护的工作就是由 Ribbon 来干的。

Ribbon 会从 Eureka Server 读取服务信息列表,存储在 Ribbon 中。如果服务器宕机了,Ribbon 会从列表剔除宕机的服务器信息。

Ribbon 有多种负载均衡算法,我们可以自行设定规则从而请求到指定的服务器。

均衡策略

1、轮循均衡(Round Robin)

原理:如果给服务器从 0 到 N 编号,轮询均衡策略会从 0 开始依次选择一个服务器作为处理本次请求的服务器。-

场景:适合所有父亲都有相同的软硬件配置,且请求频率相对平衡。

2、权重轮询均衡(Weighted Round Robin)

原理:按照服务器的不同处理能力,给服务器分配不同的权重,然后请求会按照权重分配给不同的服务器。

场景:服务器的性能不同,充分利用高性能的服务器,同时也能照顾到低性能的服务器。

3、随机均衡(Random)

原理:将请求随机分配给不同的服务器。

场景:适合客户端请求的频率比较随机的场景。

4、响应速度均衡(Response Time)

原理:负载均衡设备对每个服务器发送一个探测请求,看看哪台服务器的响应速度更快,

场景:适合服务器的响应性能不断变化的场景。

注意:响应速度是针对负载均衡设备和服务器之间的。

Ribbon 核心组件

Load Balance负载均衡是用于解决一台机器(一个进程)无法解决所有请求而产生的一种算法。像nginx可以使用负载均衡分配流量,ribbon为客户端提供负载均衡,dubbo服务调用里的负载均衡等等,很多地方都使用到了负载均衡。

使用负载均衡带来的好处很明显:

当集群里的1台或者多台服务器down的时候,剩余的没有down的服务器可以保证服务的继续使用
使用了更多的机器保证了机器的良性使用,不会由于某一高峰时刻导致系统cpu急剧上升
负载均衡有好几种实现策略,常见的有:

随机 (Random)
轮询 (RoundRobin)
一致性哈希 (ConsistentHash)
哈希 (Hash)
加权(Weighted)

ILoadBalance 负载均衡器

ribbon是一个为客户端提供负载均衡功能的服务,它内部提供了一个叫做ILoadBalance的接口代表负载均衡器的操作,比如有添加服务器操作、选择服务器操作、获取所有的服务器列表、获取可用的服务器列表等等。

ILoadBalance的继承关系如下:

负载均衡器是从EurekaClient(EurekaClient的实现类为DiscoveryClient)获取服务信息,根据IRule去路由,并且根据IPing判断服务的可用性。

负载均衡器多久一次去获取一次从Eureka Client获取注册信息呢?

在BaseLoadBalancer类下,BaseLoadBalancer的构造函数,该构造函数开启了一个PingTask任务setupPingTask();,代码如下:

 setupPingTask()的具体代码逻辑,它开启了ShutdownEnabledTimer执行PingTask任务,在默认情况下pingIntervalSeconds为10,即每10秒钟,向EurekaClient发送一次”ping”

void setupPingTask() {
        if (canSkipPing()) {
            return;
        }
        if (lbTimer != null) {
            lbTimer.cancel();
        }
        lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
                true);
        lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
        forceQuickPing();
    }

PingTask源码,即new一个Pinger对象,并执行runPinger()方法。

查看Pinger的runPinger()方法,最终根据 pingerStrategy.pingServers(ping, allServers)来获取服务的可用性,如果该返回结果,如之前相同,则不去向EurekaClient获取注册列表,如果不同则通知ServerStatusChangeListener或者changeListeners发生了改变,进行更新或者重新拉取。

完整过程是:

LoadBalancerClient(RibbonLoadBalancerClient是实现类)在初始化的时候(execute方法),会通过ILoadBalance(BaseLoadBalancer是实现类)向Eureka注册中心获取服务注册列表,并且每10s一次向EurekaClient发送“ping”,来判断服务的可用性,如果服务的可用性发生了改变或者服务数量和之前的不一致,则从注册中心更新或者重新拉取。LoadBalancerClient有了这些服务注册列表,就可以根据具体的IRule来进行负载均衡。

IRule 路由

IRule接口代表负载均衡策略:

public interface IRule{
    public Server choose(Object key);
    public void setLoadBalancer(ILoadBalancer lb);
    public ILoadBalancer getLoadBalancer();    
}

IRule接口的实现类有以下几种:

 

 

 

其中RandomRule表示随机策略、RoundRobinRule表示轮询策略、WeightedResponseTimeRule表示加权策略、BestAvailableRule表示请求数最少策略等等。

随机策略很简单,就是从服务器中随机选择一个服务器,RandomRule的实现代码如下:

public Server choose(ILoadBalancer lb, Object key) {
    if (lb == null) {
        return null;
    }
    Server server = null;
 
    while (server == null) {
        if (Thread.interrupted()) {
            return null;
        }
        List<Server> upList = lb.getReachableServers();
        List<Server> allList = lb.getAllServers();
        int serverCount = allList.size();
        if (serverCount == 0) {
            return null;
        }
        int index = rand.nextInt(serverCount); // 使用jdk内部的Random类随机获取索引值index
        server = upList.get(index); // 得到服务器实例
 
        if (server == null) {
            Thread.yield();
            continue;
        }
 
        if (server.isAlive()) {
            return (server);
        }
 
        server = null;
        Thread.yield();
    }
    return server;
}

RoundRobinRule轮询策略表示每次都取下一个服务器,比如一共有5台服务器,第1次取第1台,第2次取第2台,第3次取第3台,以此类推:

    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }
 
        Server server = null;
        int count = 0;
        while (server == null && count++ < 10) {
            List<Server> reachableServers = lb.getReachableServers();
            List<Server> allServers = lb.getAllServers();
            int upCount = reachableServers.size();
            int serverCount = allServers.size();
 
            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }
 
            int nextServerIndex = incrementAndGetModulo(serverCount);
            server = allServers.get(nextServerIndex);
 
            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }
 
            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }
 
            // Next.
            server = null;
        }
 
        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }
 
    /**
     * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
     *
     * @param modulo The modulo to bound the value of the counter.
     * @return The next value.
     */
    private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }

WeightedResponseTimeRule继承了RoundRobinRule,开始的时候还没有权重列表,采用父类的轮询方式,有一个默认每30秒更新一次权重列表的定时任务,该定时任务会根据实例的响应时间来更新权重列表,choose方法做的事情就是,用一个(0,1)的随机double数乘以最大的权重得到randomWeight,然后遍历权重列表,找出第一个比randomWeight大的实例下标,然后返回该实例,代码略。

BestAvailableRule策略用来选取最少并发量请求的服务器:

public Server choose(Object key) {
    if (loadBalancerStats == null) {
        return super.choose(key);
    }
    List<Server> serverList = getLoadBalancer().getAllServers(); // 获取所有的服务器列表
    int minimalConcurrentConnections = Integer.MAX_VALUE;
    long currentTime = System.currentTimeMillis();
    Server chosen = null;
    for (Server server: serverList) { // 遍历每个服务器
        ServerStats serverStats = loadBalancerStats.getSingleServerStat(server); // 获取各个服务器的状态
        if (!serverStats.isCircuitBreakerTripped(currentTime)) { // 没有触发断路器的话继续执行
            int concurrentConnections = serverStats.getActiveRequestsCount(currentTime); // 获取当前服务器的请求个数
            if (concurrentConnections < minimalConcurrentConnections) { // 比较各个服务器之间的请求数,然后选取请求数最少的服务器并放到chosen变量中
                minimalConcurrentConnections = concurrentConnections;
                chosen = server;
            }
        }
    }
    if (chosen == null) { // 如果没有选上,调用父类ClientConfigEnabledRoundRobinRule的choose方法,也就是使用RoundRobinRule轮询的方式进行负载均衡        
        return super.choose(key);
    } else {
        return chosen;
    }
}

 

 

使用Ribbon提供的负载均衡策略很简单,只需以下几部:

1、创建具有负载均衡功能的RestTemplate实例

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

使用RestTemplate进行rest操作的时候,会自动使用负载均衡策略,它内部会在RestTemplate中加入LoadBalancerInterceptor这个拦截器,这个拦截器的作用就是使用负载均衡。
默认情况下会采用轮询策略,如果希望采用其它策略,则指定IRule实现,如:

@Bean
public IRule ribbonRule() {
    return new BestAvailableRule();
}

这种方式对Feign也有效。

我们也可以参考ribbon,自己写一个负载均衡实现类。

可以通过下面方法获取负载均衡策略最终选择了哪个服务实例:

    @Autowired
    LoadBalancerClient loadBalancerClient; 
    
    //测试负载均衡最终选中哪个实例
    public String getChoosedService() {
        ServiceInstance serviceInstance = loadBalancerClient.choose("USERINFO-SERVICE");
        StringBuilder sb = new StringBuilder();
        sb.append("host: ").append(serviceInstance.getHost()).append(", ");
        sb.append("port: ").append(serviceInstance.getPort()).append(", ");
        sb.append("uri: ").append(serviceInstance.getUri());
        return sb.toString();
    }

 

Ribbon 拦截请求的原理 

第一步:Ribbon 拦截所有标注@loadBalance注解的 RestTemplate。RestTemplate 是用来发送 HTTP 请求的。

第二步:将 Ribbon 默认的拦截器 LoadBalancerInterceptor 添加到 RestTemplate 的执行逻辑中,当 RestTemplate 每次发送 HTTP 请求时,都会被 Ribbon 拦截。

第三步:拦截后,Ribbon 会创建一个 ILoadBalancer 实例。

第四步:ILoadBalancer 实例会使用 RibbonClientConfiguration 完成自动配置。就会配置好 IRule,IPing,ServerList。

第五步:Ribbon 会从服务列表中选择一个服务,将请求转发给这个服务。

Ribbon 初始化的原理 

第一步:Ribbon 有一个自动配置类 LoadBalancerAutoConfiguration,SpringBoot 加载自动配置类,就会去初始化 Ribbon。

第二步:当我们给 RestTemplate 或者 AsyncRestTemplate 添加注解后,Ribbon 初始化时会收集加了 @LoadBalanced 注解的 RestTemplate 和 AsyncRestTemplate ,把它们放到一个 List 里面。

第三步:然后 Ribbon 里面的 RestTemplateCustomizer 会给每个 RestTemplate 进行定制化,也就是加上了拦截器:LoadBalancerInterceptor。

第四步:从 Eureka 注册中心获取服务列表,然后存到 Ribbon 中。

第五步:加载 YMAL 配置文件,配置好负载均衡配置,创建一个 ILoadbalancer 实例。

 



 

 



原文链接:https://blog.csdn.net/wudiyong22/article/details/80829808

                  https://blog.csdn.net/summer_fish/article/details/121806563

posted @ 2022-06-23 13:17  Nausicaa0505  阅读(285)  评论(0编辑  收藏  举报