从零学SpringCloud系列(三):客户端负载均衡Ribbon

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
EMR Serverless StarRocks,5000CU*H 48000GB*H
应用型负载均衡 ALB,每月750个小时 15LCU
简介: 从零学SpringCloud系列(三):客户端负载均衡Ribbon

一、Spring Cloud Ribbon简介


Spring Cloud是一个基于HTTP和TCP的客户端 负载工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松的将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon属于一个工具类框架, 在 项目中它不需要单独部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。所以Spring Cloud对于构建微服务 非常重要。


二、项目中使用


通过Spring Cloud Ribbon 的封装,我我们在微服务架构中使用客户端负载均衡调用 非常简单, 只需要两步:


服务提供者只需启动多个服务实例并注册到一个服务中心 或者多个相关联的服务注册中心

服务消费者直接调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用

项目地址:https://github.com/zhenghaoxiao/spring-cloud-in-action/tree/dev


三、RestTemplate详解


image.png

Get请求的具体实现方式:


第一种:  getForEntity函数。该方法返回的是ResponseEntity,该对象是Spring对HTTP请求响应的封装,其中主要存储了HTTP的几个重要元素,其中包括请求状态吗、在它的父类中还存储着请求头信息对象HttpHeader 以及泛型类型的请求体对象。而返回的RestReponseEntity对象中的 body内容类型会根据第二个参数转换为S tring类型。

 @Autowired
    private RestTemplate restTemplate;
    @GetMapping("/consumer")
    public String helloConsumer() {
        String body = restTemplate.getForEntity("http://hello-service/hello", String.class).getBody();
        return body;
    }

上面例子是比较常用的方法,getForEntity函数实际上提供了一下三种不同的重载实现。

1、getForEntity(String url, Class<T> responseType, Object... uriVariables)

   @GetMapping("/consumer/{id}")
    public String helloConsumer1(@PathVariable("id")String id) {
        String body = restTemplate.getForEntity("http://hello-service/hello/{id}", String.class,id).getBody();
        return body;
    }

这里需要注意的是,由于uriVariables参数是一个数组,所以它的顺序会对应url 中占位符定义的数字顺序。


2、String url, Class<T> responseType, Map<String, ?> uriVariables


 @GetMapping("/consumer/{id}")
    public String helloConsumer2(@PathVariable("id")String id) {
        Map<String,String> map =  new HashMap();
        map.put("id",id);
        String body = restTemplate.getForEntity("http://hello-service/hello/{id}", String.class,map).getBody();
        return body;
    }


使用这种这种方法进行参数绑定的时候需要在占位符中指定Map中参数的key值。


3、getForEntity(URI url, Class<T> responseType

  @GetMapping("/consumer/{id}")
    public String helloConsumer3(@PathVariable("id")String id) {
        UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://hello-service/hello/{id}")
                .build().expand(id).encode();
        URI uri = uriComponents.toUri();
        String body = restTemplate.getForEntity(uri, String.class).getBody();
        return body;
    }


第二种:getForObject函数,该方法可以理解为对getForEntity的进一步封装,它通过HttpMessageConverterExtractor对HTTP的请求响应体body内容进行对象转换,实现请求直接返回包装好的对象内容。

 @GetMapping("/consumer/{id}")
    public String helloConsumer4(@PathVariable("id")String id) {
        String body = restTemplate.getForObject("http://hello-service/hello/{id}", String.class,id);
        return body;
    }


当我们不需要关注请求响应除body外的其他内容时,该函数就非常好用,可以少一个从Response中获取body的步骤。他与前面我们介绍的getForEntity类似,也提供了三种不同的重载实现,在这里我们就不在重复描述了。


post请求具体实现:


第一种:postForEntity,该函数和前面我们介绍的getForEntity非常相似,也有三种不同的实现方式


 User user = new User("jack",18);
        String body = restTemplate.postForEntity("http://hello-service/user/",user, String.class).getBody();
        return body;

postForEntity函数也实现了三种不同的重载方法


1、String url, @Nullable Object request, Class<T> responseType, Object... uriVariables


2、String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables


3、URI url, @Nullable Object request, Class<T> responseType


第二种:postForObject函数我们在这里就不在赘述。


四、负载均衡策略

20200414195348784.png

1、RandomRule


该策略实现了从服务实例清单中随机选择一个服务实例的功能。下面我们从源码角度分析一下它的实现逻辑。

 public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        } else {
            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 = this.chooseRandomInt(serverCount);
                server = (Server)upList.get(index);
                if (server == null) {
                    Thread.yield();
                } else {
                    if (server.isAlive()) {
                        return server;
                    }
                    server = null;
                    Thread.yield();
                }
            }
            return server;
        }

从原 代码中我们可以发现,该实现类中的choose方法增加了一个负载均衡器的参数,它会使用 传入的负载均衡器来获得可用实例 列表uplist和所有的实例列表allList,并通过rand.nextInt(serverCount)函数来获得一个随机数,并且该随机数作为uplist的索引值来返回具体实例。同时,具体选择逻辑在一个while(server == null)循环之内,而根据选择逻辑的实现, 正常情况下每次选择都应该选出一个服务实例,如果出现死循环获取不到服务的情况,则很可能存在并发bug。


2、RondRobinRule


该策略实现了按照线性轮询的方式依次选择每个服务实例的 功能,具体实现如下:

public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        } else {
            Server server = null;
            int count = 0;
            while(true) {
                if (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) {
                        int nextServerIndex = this.incrementAndGetModulo(serverCount);
                        server = (Server)allServers.get(nextServerIndex);
                        if (server == null) {
                            Thread.yield();
                        } else {
                            if (server.isAlive() && server.isReadyToServe()) {
                                return server;
                            }
                            server = null;
                        }
                        continue;
                    }
                    log.warn("No up servers available from load balancer: " + lb);
                    return null;
                }
                if (count >= 10) {
                    log.warn("No available alive servers after 10 tries from load balancer: " + lb);
                }
                return server;
            }
        }
    }
  private int incrementAndGetModulo(int modulo) {
        int current;
        int next;
        do {
            current = this.nextServerCyclicCounter.get();
            next = (current + 1) % modulo;
        } while(!this.nextServerCyclicCounter.compareAndSet(current, next));
        return next;
    }


从源代码中我们可以发现,在循环条件中增加了一个 count计数变量,该变量会在每次循环之后累加, 也就说,如果一直选择不到server超过10次,那么就会结束尝试,并打印警告信息No available alive servers after 10 tries from load balancer: " + lb


而 线性轮询的实现则是通过AtomicInteger nextServerCyclicCounter来实现,每次就行实例选择时通调用incrementAndGetModulo函数实现递增。


3、RetryRule


该策略实现了一个具备重试机制的实例选择功能,具体实现如下:

 public Server choose(ILoadBalancer lb, Object key) {
        long requestTime = System.currentTimeMillis();
        long deadline = requestTime + this.maxRetryMillis;
        Server answer = null;
        answer = this.subRule.choose(key);
        if ((answer == null || !answer.isAlive()) && System.currentTimeMillis() < deadline) {
            InterruptTask task = new InterruptTask(deadline - System.currentTimeMillis());
            while(!Thread.interrupted()) {
                answer = this.subRule.choose(key);
                if (answer != null && answer.isAlive() || System.currentTimeMillis() >= deadline) {
                    break;
                }
                Thread.yield();
            }
            task.cancel();
        }
        return answer != null && answer.isAlive() ? answer : null;
    }


在其内部定义了一个IRule对象,默认使用RoundRobinRule实例,在具体的choose 方法中则实现了对内部定义策略的反复尝试,若期间能选择到具体的实例对象则返回,如果选择不到就根据设置的尝试结束时间 为阈值(maxRetyMills参数定义的值+choose方法开始执行的时间),当超过该阈值以后就放null。


4、WeightedResponseTimeRule


该策略是对RoundRobinRule的扩展,增加了根据实例运行情况来计算权重, 并根据权重来选择实例,以 达到更优的分配效果,它的实现具体 包含下面三个内容:


1)、定时任务


该策略 在初始化的时候会通过 this.serverWeightTimer.schedule(new WeightedResponseTimeRule.DynamicServerWeightTask(), 0L, (long)this.serverWeightTaskTimerInterval) 来启动一个定时任务,用来为每个服务实例计算权重,该任务默认30s执行一次。

    class DynamicServerWeightTask extends TimerTask {
        DynamicServerWeightTask() {
        }
        public void run() {
            WeightedResponseTimeRule.ServerWeight serverWeight = WeightedResponseTimeRule.this.new ServerWeight();
            try {
                serverWeight.maintainWeights();
            } catch (Exception var3) {
                WeightedResponseTimeRule.logger.error("Error running DynamicServerWeightTask for {}", WeightedResponseTimeRule.this.name, var3);
            }
        }

2)权重计算


在源代码中我们可以看到定义了这样一个变量  private volatile List<Double> accumulatedWeights = new ArrayList(); 该list就是用来存储权重对象的,在该list中每个权重值所处的位置对应了负载均衡器维护的服务实例清单中所有实例在清单中的位置。


权重计算的方法maintainWights 代码如下:

public void maintainWeights() {
            ILoadBalancer lb = WeightedResponseTimeRule.this.getLoadBalancer();
            if (lb != null) {
                if (WeightedResponseTimeRule.this.serverWeightAssignmentInProgress.compareAndSet(false, true)) {
                    try {
                        WeightedResponseTimeRule.logger.info("Weight adjusting job started");
                        AbstractLoadBalancer nlb = (AbstractLoadBalancer)lb;
                        LoadBalancerStats stats = nlb.getLoadBalancerStats();
                        if (stats != null) {
                            //计算所有实例的平均响应时间的总和
                            double totalResponseTime = 0.0D;
                            ServerStats ss;
                            for(Iterator var6 = nlb.getAllServers().iterator(); var6.hasNext(); totalResponseTime += ss.getResponseTimeAvg()) {
                                Server server = (Server)var6.next();
                                ss = stats.getSingleServerStat(server);
                            }
                            Double weightSoFar = 0.0D;
                            List<Double> finalWeights = new ArrayList();
                            Iterator var20 = nlb.getAllServers().iterator();
                            while(var20.hasNext()) {
                                Server serverx = (Server)var20.next();
                                ServerStats ssx = stats.getSingleServerStat(serverx);
                                double weight = totalResponseTime - ssx.getResponseTimeAvg();
                                weightSoFar = weightSoFar + weight;
                                finalWeights.add(weightSoFar);
                            }
                            WeightedResponseTimeRule.this.setWeights(finalWeights);
                            return;
                        }
                    } catch (Exception var16) {
                        WeightedResponseTimeRule.logger.error("Error calculating server weights", var16);
                        return;
                    } finally {
                        WeightedResponseTimeRule.this.serverWeightAssignmentInProgress.set(false);
                    }
                }
            }
        }

该函数主要实现分为 以下两个步骤:


根据LoadBalancerStats记录的每个实例的统计信息,累加所有实例的平均响应时间,得到总平均响应时间totalReponseTime,该值用于后续计算。

为负载均衡器中维护的实例清单逐个计算权重,计算规则 为weightSoFar +  totalResponseTime - 实例的平均响应时间,其中weightSoFar初始值为0,并且没计算好一个权重需要累加到weightSoFar上供下一次计算 使用。

举个例子来理解一下这个计算过程,假设有四个实例 A,B,C,D 他们的平均响应时间为10、40、80、100,所以总 响应时间为10+40+80+100=230,每个 实例的权重为 总响应时间与实例自身响应平均响应时间的差的累计所得,所以他们四个的权重分别为:


A:0+230-10=220


B:220+(230-40)=410


C:410+(230-80) = 560


D:560+(230-100)=690


需要注意的是,这里的权重值只是表示了各个实例权重区间的上限,并非 某个实例的优先级,所以不是数值越大越容易被选中的概率越大。这里我们需要介绍一个权重区间的概念,以上面的例子的计算结果为例,它实际上是为这4个实例构建了4个不同的实例区间,每个实例区间下限是上一个实例区间的上限,而每个实例的区间上限则是我们上面计算并存储于List  accumulatedWeights中的权重值,其中第一个实例下限默认为0. 所以我们可以得到每个实例的权重区间:


A:[0,220]


B:(220,410]


C:(410,560]


D:(560,690)


不难发现,实际上每个区间的宽度就是:总的平均响应时间  -  实例的平均响应时间,所以实例的平均响应时间越短、权重区间的宽度越大,而权重区间的宽度越大被选中的概率越高。从上面的区间开闭规则来看,非常的不规则,下面我们从实例选择的角度分析一下区间边界问题。


实例选择


实例选择的实现 和前面介绍的算法结构类似,下面是它的具体实现


 public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        } else {
            Server server = null;
            while(server == null) {
                List<Double> currentWeights = this.accumulatedWeights;
                if (Thread.interrupted()) {
                    return null;
                }
                List<Server> allList = lb.getAllServers();
                int serverCount = allList.size();
                if (serverCount == 0) {
                    return null;
                }
                int serverIndex = 0;
                //获取最后一个实例权重
                double maxTotalWeight = currentWeights.size() == 0 ? 0.0D : (Double)currentWeights.get(currentWeights.size() - 1);
                if (maxTotalWeight >= 0.001D && serverCount == currentWeights.size()) {
                    //如果最后一个实例的权重值>=0.001就产生一个[0,maxTotalWeight]的随机数
                    double randomWeight = this.random.nextDouble() * maxTotalWeight;
                    int n = 0;
                    //便利维护的权重清单,若权重大于等于随机得到的数值,就选择这个实例
                    for(Iterator var13 = currentWeights.iterator(); var13.hasNext(); ++n) {
                        Double d = (Double)var13.next();
                        if (d >= randomWeight) {
                            serverIndex = n;
                            break;
                        }
                    }
                    server = (Server)allList.get(serverIndex);
                } else {
                    //如果最后一个实例权重<0.001,则采用父类实现的线性轮询的策略
                    server = super.choose(this.getLoadBalancer(), key);
                    if (server == null) {
                        return server;
                    }
                }
                if (server == null) {
                    Thread.yield();
                } else {
                    if (server.isAlive()) {
                        return server;
                    }
                    server = null;
                }
            }
            return server;
        }
    }

从源代码中我们可以看到,选择实例的核心过就分为两步:


生成一个[0,最大权重值)区间内的随机数


遍历权重列表,比较权重值与随机数的大小,如果权重值大于等于随机数,就拿当前权重列表的索引值去服务实例列表中获取具体实例。从 生成的随机数的区间,我们就可以分析出上面4个实例区间的边界值的问题了。


若继续以上面的数据为例进行服务实例选择,则该方法会 从[0,690)区间中选出一个 随机数,比如选择出230,由于该值位于第二个区间,所以此时就会选择实例B来就行请求。


4、ClientConfigEnabledRoundRobinRule


该策略 比较特殊,我们一般不质检使用它。因为它本身没有实现什么特殊的处理逻辑,从源码中我们可以看到,在它内部定义了一个RoundRibonRule策略,而choose函数的实现也正是使用了RoundRobinRule的线性轮询机制,所以它实现的功能实际上与RoundRibonRule 相同,所以该策略是下面我们将要介绍的一些高级策略的父类。


一下高级的选择策略在下一篇博文中介绍

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
目录
相关文章
|
14天前
|
负载均衡 Java Nacos
Ribbon负载均衡
Ribbon负载均衡
23 1
Ribbon负载均衡
|
11天前
|
负载均衡 监控 网络协议
SpringCloud之Ribbon使用
通过以上步骤,就可以在Spring Cloud项目中有效地使用Ribbon来实现服务调用的负载均衡,提高系统的可靠性和性能。在实际应用中,根据具体的业务场景和需求选择合适的负载均衡策略,并进行相应的配置和优化,以确保系统的稳定运行。
39 15
|
11天前
|
负载均衡 算法 Java
除了 Ribbon,Spring Cloud 中还有哪些负载均衡组件?
这些负载均衡组件各有特点,在不同的场景和需求下,可以根据项目的具体情况选择合适的负载均衡组件来实现高效、稳定的服务调用。
28 5
|
2月前
|
负载均衡 Java Nacos
SpringCloud基础1——远程调用、Eureka,Nacos注册中心、Ribbon负载均衡
微服务介绍、SpringCloud、服务拆分和远程调用、Eureka注册中心、Ribbon负载均衡、Nacos注册中心
SpringCloud基础1——远程调用、Eureka,Nacos注册中心、Ribbon负载均衡
|
3月前
|
负载均衡 算法 Java
SpringCloud之Ribbon使用
通过 Ribbon,可以非常便捷的在微服务架构中实现请求负载均衡,提升系统的高可用性和伸缩性。在实际使用中,需要根据实际场景选择合适的负载均衡策略,并对其进行适当配置,以达到更佳的负载均衡效果。
59 13
|
2月前
|
负载均衡 Java 开发者
Ribbon框架实现客户端负载均衡的方法与技巧
Ribbon框架为微服务架构中的客户端负载均衡提供了强大的支持。通过简单的配置和集成,开发者可以轻松地在应用中实现服务的发现、选择和负载均衡。适当地使用Ribbon,配合其他Spring Cloud组件,可以有效提升微服务架构的可用性和性能。
33 0
|
2月前
|
SpringCloudAlibaba API 开发者
新版-SpringCloud+SpringCloud Alibaba
新版-SpringCloud+SpringCloud Alibaba
|
3月前
|
资源调度 Java 调度
Spring Cloud Alibaba 集成分布式定时任务调度功能
定时任务在企业应用中至关重要,常用于异步数据处理、自动化运维等场景。在单体应用中,利用Java的`java.util.Timer`或Spring的`@Scheduled`即可轻松实现。然而,进入微服务架构后,任务可能因多节点并发执行而重复。Spring Cloud Alibaba为此发布了Scheduling模块,提供轻量级、高可用的分布式定时任务解决方案,支持防重复执行、分片运行等功能,并可通过`spring-cloud-starter-alibaba-schedulerx`快速集成。用户可选择基于阿里云SchedulerX托管服务或采用本地开源方案(如ShedLock)
124 1
|
1月前
|
JSON SpringCloudAlibaba Java
Springcloud Alibaba + jdk17+nacos 项目实践
本文基于 `Springcloud Alibaba + JDK17 + Nacos2.x` 介绍了一个微服务项目的搭建过程,包括项目依赖、配置文件、开发实践中的新特性(如文本块、NPE增强、模式匹配)以及常见的问题和解决方案。通过本文,读者可以了解如何高效地搭建和开发微服务项目,并解决一些常见的开发难题。项目代码已上传至 Gitee,欢迎交流学习。
128 1
Springcloud Alibaba + jdk17+nacos 项目实践
|
25天前
|
消息中间件 自然语言处理 Java
知识科普:Spring Cloud Alibaba基本介绍
知识科普:Spring Cloud Alibaba基本介绍
56 2