Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >SpringCloud源码:Ribbon负载均衡分析

SpringCloud源码:Ribbon负载均衡分析

作者头像
吴生
修改于 2018-12-14 02:10:07
修改于 2018-12-14 02:10:07
45800
代码可运行
举报
文章被收录于专栏:吴生的专栏吴生的专栏
运行总次数:0
代码可运行

本文主要分析 SpringCloud 中 Ribbon 负载均衡流程和原理。

SpringCloud版本为:Edgware.RELEASE。

一.时序图

和以前一样,先把图贴出来,直观一点:

在这里插入图片描述

在这里插入图片描述

二.源码分析

我们先从 contoller 里面看如何使用 Ribbon 来负载均衡的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  @GetMapping("/user/{id}")
 public User findById(@PathVariable Long id) {
   //return this.restTemplate.getForObject("http://192.168.2.110:8001/" + id, User.class);
   return this.restTemplate.getForObject("http://microservice-provider-user/" + id, User.class);
 }

可以看到,在整合 Ribbon 之前,请求Rest是通过IP端口直接请求。整合 Ribbon 之后,请求的地址改成了 http://applicationName ,官方取名为虚拟主机名(virtual host name),当 Ribbon 和 Eureka 配合使用时,会自动将虚拟主机名转换为微服务的实际IP地址,我们后面会分析这个过程。

首先从 RestTemplate#getForObject 开始:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws 
    RestClientException {
        // 设置RequestCallback的返回类型responseType
        RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
        // 实例化responseExtractor
        HttpMessageConverterExtractor<T> responseExtractor =
                new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger);
        return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
    }

接着执行到 RestTemplate 的 execute,主要是拼装URI,如果存在baseUrl,则插入baseUrl。拼装好后,进入实际"执行"请求的地方:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
            ResponseExtractor<T> responseExtractor, Object... uriVariables) throws 
    RestClientException {
        // 组装 URI
        URI expanded = getUriTemplateHandler().expand(url, uriVariables);
        // 实际"执行"的地方
        return doExecute(expanded, method, requestCallback, responseExtractor);
    }

RestTemplate#doExecute,实际“执行”请求的地方,执行超过后,返回 response:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    protected <T> T doExecute(URI url, HttpMethod method, RequestCallback requestCallback,
            ResponseExtractor<T> responseExtractor) throws RestClientException {
        ClientHttpResponse response = null;
        try {
            // 实例化请求,url为请求地址,method为GET
            ClientHttpRequest request = createRequest(url, method);
            if (requestCallback != null) {// AcceptHeaderRequestCallback
                requestCallback.doWithRequest(request);
            }
            // 实际处理请求的地方
            response = request.execute();
            // 处理response,记录日志和调用对应的错误处理器
            handleResponse(url, method, response);
            if (responseExtractor != null) {// 使用前面的HttpMessageConverterExtractor从Response里面抽取数据
                return responseExtractor.extractData(response);
            }
            else {
                return null;
            }
        }
        ......
    }

到了请求被执行的地方,AbstractClientHttpRequest#execute,跳转到 executeInternal:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public final ClientHttpResponse execute() throws IOException {
        // 断言请求还没被执行过
        assertNotExecuted();
        // 跳转到 executeInternal 处理请求
        ClientHttpResponse result = executeInternal(this.headers);
        // 标记请求为已经执行过
        this.executed = true;
        return result;
    }

AbstractBufferingClientHttpRequest#executeInternal,AbstractBufferingClientHttpRequest是AbstractClientHttpRequest的子抽象类,作用是缓存output,使用了一个字节数组输出流:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
        // 首次进来,bytes内容为空
        byte[] bytes = this.bufferedOutput.toByteArray();
        if (headers.getContentLength() < 0) {
            // 设置 Content-Length 为 1
            headers.setContentLength(bytes.length);
        }
        // 模板方法,跳转到了实现类中的方法,InterceptingClientHttpRequest#executeInternal
        ClientHttpResponse result = executeInternal(headers, bytes);
        // 拿到结果后,清空缓存
        this.bufferedOutput = null;
        return result;
    }

executeInternal是一个抽象方法,跳转到了其实现类InterceptingClientHttpRequest#executeInternal:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) 
    throws IOException {
        InterceptingRequestExecution requestExecution = new InterceptingRequestExecution();
        // InterceptingRequestExecution是一个内部类
        return requestExecution.execute(this, bufferedOutput);
    }
    // 内部类,负责执行请求
    private class InterceptingRequestExecution implements ClientHttpRequestExecution {
        private final Iterator<ClientHttpRequestInterceptor> iterator;// 所有HttpRequest的拦截器
        public InterceptingRequestExecution() {
            this.iterator = interceptors.iterator();
        }
        @Override
        public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
            if (this.iterator.hasNext()) {// 如果还有下一个拦截器,则执行其拦截方法
                // 这里的拦截器是 MetricsClientHttpRequestInterceptor,对应"metrics"信息,记录执行时间和结果
                ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
                // 执行拦截方法
                return nextInterceptor.intercept(request, body, this);
            }
            ......
        }
    }

跳转到了拦截器 MetricsClientHttpRequestInterceptor 的拦截方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
            ClientHttpRequestExecution execution) throws IOException {
        long startTime = System.nanoTime();// 标记开始执行时间
        ClientHttpResponse response = null;
        try {
            // 传入请求和Body,处理执行,又跳转回 InterceptingRequestExecution
            response = execution.execute(request, body);
            return response;
        }
        finally {// 在执行完方法,返回response之前,记录一下执行的信息
            SmallTagMap.Builder builder = SmallTagMap.builder();
            for (MetricsTagProvider tagProvider : tagProviders) {
                for (Map.Entry<String, String> tag : tagProvider
                        .clientHttpRequestTags(request, response).entrySet()) {
                    builder.add(Tags.newTag(tag.getKey(), tag.getValue()));
                }
            }
            MonitorConfig.Builder monitorConfigBuilder = MonitorConfig
                    .builder(metricName);
            monitorConfigBuilder.withTags(builder);
            // 记录执行时间
            servoMonitorCache.getTimer(monitorConfigBuilder.build())
                    .record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
        }
    }

又跳转回了 InterceptingRequestExecution,下个拦截器是 - LoadBalancerInterceptor,最后的Boss,调用LoadBalancerClient完成请求的负载。

LoadBalancerInterceptor#intercept,主角登场了,终于等到你,还好没放弃:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        // 获取原始URI
        final URI originalUri = request.getURI();
        // 获取请求中的服务名字,也就是所谓的"虚拟主机名"
        String serviceName = originalUri.getHost();
        // 转由 LoadBalancerClient 处理请求
        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }

下面空一行先停下来休息一下,然后看看,负载均衡是怎样实现的。

LoadBalancerInterceptor这里默认的实现是 RibbonLoadBalancerClient,Ribbon是Netflix发布的负载均衡器

RibbonLoadBalancerClient#execute,负载均衡算法选出实际处理请求的Server:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        // serviceId即前面的虚拟主机名 "microservice-provider-user",获取loadBalancer
        //这里获取到的是 DynamicServerListLoadBalancer
        ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
        // 基于loadBalancer,选择实际处理请求的服务提供者
        Server server = getServer(loadBalancer);
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        }
        RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
                serviceId), serverIntrospector(serviceId).getMetadata(server));
        return execute(serviceId, ribbonServer, request);
    }

RibbonLoadBalancerClient#getServer,转交 loadBalancer 选择Server:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    protected Server getServer(ILoadBalancer loadBalancer) {
        if (loadBalancer == null) {
            return null;
        }
        // 由 loadBalancer 完成选Server的重任,这里的 key 是默认值 "default"
        return loadBalancer.chooseServer("default"); // TODO: better handling of key
    }

chooseServer也是一个抽象的模板方法,最后的实现是 ZoneAwareLoadBalancer#chooseServer:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public Server chooseServer(Object key) {
        if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
            logger.debug("Zone aware logic disabled or there is only one zone");
            // 到了 BaseLoadBalancer的chooseServer
            return super.chooseServer(key);
        }
        ......
    }

BaseLoadBalancer#chooseServer,转交规则来选择Server:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        // counter是一个计数器,起始值是"0",下面自增一次,变为 "1"
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                // 默认的挑选规则是 "ZoneAvoidanceRule"
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }

PredicateBasedRule是ZoneAvoidanceRule的父类。PredicateBasedRule#choose,可以看到,基础负载规则采用的是"RoundRobin"即轮询的方式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public Server choose(Object key) {
        ILoadBalancer lb = getLoadBalancer();
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }       
    }

下面分析"轮询"的过程,AbstractServerPredicate#chooseRoundRobinAfterFiltering,传入Server列表的长度,自增取模实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
        // 首先拿到所有"合格"的Server
        List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
        if (eligible.size() == 0) {
            return Optional.absent();
        }
        // 在 incrementAndGetModulo 中获取,"自增取模"
        return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
    }

AbstractServerPredicate#incrementAndGetModulo,维护了一个nextIndex,记录下次请求的下标:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextIndex.get();// 第一次 current是"0"
            int next = (current + 1) % modulo;// current+1对size取模,作为下次的"current"
            // "0" == current,则以原子方式将该值设置为 next
            if (nextIndex.compareAndSet(current, next))
                return current;
        }
    }

最后,我们通过控制台来验证一下请求是不是"轮询"分配到服务提供者的,本地启动了8000和8001两个Provider:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
2018-12-09 18:55:30.794  c.i.c.s.user.controller.MovieController  : microservice-provider-user:192.168.2.117:8001
2018-12-09 18:55:33.196  c.i.c.s.user.controller.MovieController  : microservice-provider-user:192.168.2.117:8000
2018-12-09 18:55:34.713  c.i.c.s.user.controller.MovieController  : microservice-provider-user:192.168.2.117:8001
2018-12-09 18:55:34.975  c.i.c.s.user.controller.MovieController  : microservice-provider-user:192.168.2.117:8000
2018-12-09 18:55:35.175  c.i.c.s.user.controller.MovieController  : microservice-provider-user:192.168.2.117:8001
2018-12-09 18:55:35.351  c.i.c.s.user.controller.MovieController  : microservice-provider-user:192.168.2.117:8000
2018-12-09 18:55:35.534  c.i.c.s.user.controller.MovieController  : microservice-provider-user:192.168.2.117:8001

可以看到,请求确实被轮询给两个Provider处理的。

至此,我们完成了 SpringCloud 中 Ribbon 负载均衡的过程,知道了默认采用的是"轮询"的方式,实现是通过维护一个index,自增后取模来作为下标挑选实际响应请求的Server。除了轮询的方式,还有随机等算法。感兴趣可以按照类似思路分析测试一下。

文章来源:https://my.oschina.net/javamaster/blog/2985895 推荐阅读:https://www.roncoo.com/search/spring%20

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
SpringCloudRibbon负载均衡实现原理
在SpringCloud中,我们最常使用到的负载均衡库就是ribbon。使用方式,一般是通过自动配置类注入,然后在类中定义负载均衡实例bean
tunsuy
2022/10/27
3330
Eurkea,Ribbon和RestTemplate是如何结合到一起完成服务注册与发现功能的? --上
ResrTemplate组件是用来完成远程调用功能的,而Ribbon组件负责完成客户端负载均衡功能的,Eurkea服务端负责保存服务名和真实服务器地址的映射关系的,如果我们想要这三者结合起来完成服务发现与注册功能,有一个很简单的思路如下:
大忽悠爱学习
2023/02/13
6160
Eurkea,Ribbon和RestTemplate是如何结合到一起完成服务注册与发现功能的? --上
微服务组件【负载均衡】Netflix Ribbon
Netflix Ribbon是一个客户端负载均衡组件,用于将用户请求根据负载均衡算法负载到后端不同的服务集群节点上,从而降低单点服务器压力。
默 语
2024/11/22
910
Ribbon源码分析
对于Spring中的AnnotationMetadata不太熟悉的同学,可以跑一下下面的CASE
用户2032165
2020/03/27
8030
Ribbon源码分析
面试官:说说Ribbon是如何实现负载均衡的?
Ribbon的作用是负载均衡,但是根据我面试他人的情况来看,很多人只忙于业务,而不清楚具体的底层原理,在面试中是很容易吃亏的。基于此,本文就来分析一下这里面的请求流程,里面贴的源码会比较多,如果看不惯的话,可以直接看最后的总结。
业余草
2020/09/18
1.8K0
RestTemplate的逆袭之路,从发送请求到负载均衡
上篇文章我们详细的介绍了RestTemplate发送请求的问题,熟悉Spring的小伙伴可能会发现:RestTemplate不就是Spring提供的一个发送请求的工具吗?它什么时候具有了实现客户端负载
江南一点雨
2018/04/02
3.3K0
RestTemplate的逆袭之路,从发送请求到负载均衡
Ribbon 负载均衡器 LoadBalancer 源码解析
什么是负载均衡?简单来说一个应用,后台有多个服务来支撑,即利用多台服务器提供单一服务,当某个服务挂了或者负载过高的时候,负载均衡器能够选择其他的服务来处理请求,用来提高应用的高可用性和高并发行;此外,当用户请求过来的时候,负载均衡器会将请求后台内网服务器,内网服务器将请求的响应返回给负载平衡器,负载平衡器再将响应发送到用户,这样可以阻止了用户直接访问后台服务器,使得服务器更加安全。
Java技术编程
2020/05/21
2.2K0
客户端负载均衡Ribbon之源码解析
假设有一个分布式系统,该系统由在不同计算机上运行的许多服务组成。但是,当用户数量很大时,通常会为服务创建多个副本。每个副本都在另一台计算机上运行。此时,出现 “Load Balancer(负载均衡器)”。它有助于在服务器之间平均分配传入流量。
程序员果果
2019/05/28
1.3K0
为何一个@LoadBalanced注解就能让RestTemplate拥有负载均衡的能力?【享学Spring Cloud】
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
YourBatman
2019/09/18
1.7K0
给RestTemplate加全局日志打印
项目中几乎所有的rpc调用都用了RestTemplate,日志并不完善, 同事要对所有请求增加一个日志和响应的日志输出
eeaters
2023/08/08
1K0
给RestTemplate加全局日志打印
Spring Cloud netflix ribbon源码分析
依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> <version>2.2.3.RELEASE</version> </dependency> 配置 假设订单服务有两台,分别分8060、8061的两个服务 micro-order-service.ribbon.listOfServers=\
用户1215919
2020/10/26
4850
Spring Cloud netflix ribbon源码分析
ribbon源码
这里通过@LoadBalance注解, 引入了ribbon, 自动实现ribbon的负载均衡策略
用户7798898
2020/09/27
6570
ribbon源码
springcloud系列之-ribbon使用及原理讲解
本章节将要学习springcloud的组件ribbon的使用,ribbon是一个实现了客户端负载均衡的工具,透明的实现了负载均衡策略,我们只需要在resttemplate加上loadbalenced注解就可以开启负载均衡,非常方便,学完这节后,你将会知道如何在你的项目上去使用这个小工具
AI码师
2020/11/19
8020
springcloud系列之-ribbon使用及原理讲解
通过 Ribbon 查询 Nacos 服务实例
Nacos 提供了开放 API 可通过 /nacos/v1/ns/instance/list 获取服务列表。如果我们采用 spring-cloud 方式去获取服务,最终会通过 Nacos Client + loadbalancer 的方式进行客户端负载均衡。
没有故事的陈师傅
2021/08/13
1.9K0
Spring Cloud系列二:Ribbon
在微服务环境下每个服务实例少则几个,多则上百个,如何让请求均匀分布到各服务实例上是微服务架构下必须解决的一个问题,这方面有2种解决方案:
心平气和
2021/02/26
3780
Ribbon分析
调用时,直接使用在eureka中注册的服务名进行调用,就可以由ribbon来进行负载均衡了
zhaozhen
2021/07/13
3720
SpringCloud源码学习(三) 服务列表拉取和Ribbon源码学习
在上篇文章中,我们了解了Eureka客户端通过两个定时任务去从Eureka服务上获取服务列表信息和心跳,而且默认30秒进行一次服务列表和心跳检测,如果一次获取列表超时了,就会将默认的30秒扩大一倍,并与扩容上限300(30*10)秒对比,也就是默认心跳时间间隔与最大容许的时间间隔。
写一点笔记
2020/11/02
8610
SpringCloud源码学习(三) 服务列表拉取和Ribbon源码学习
Spring Cloud 快速上手之 Ribbon 负载均衡
Spring Cloud Ribbon是基于HTTP和TCP的客户端负载工具,它是基于Netflix Ribbon实现的。通过Spring Cloud的封装,可以轻松地将面向服务的REST 模板请求,自动转换成客户端负载均衡服务调用。提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。
架构探险之道
2020/05/17
7690
微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项
二、FeignClent注解剖析+Spring Cloud Feign基本功能配置解读
全栈程序员站长
2022/08/11
4200
微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项
【一起学源码-微服务】Ribbon 源码一:Ribbon概念理解及Demo调试
前面文章已经梳理清楚了Eureka相关的概念及源码,接下来开始研究下Ribbon的实现原理。
一枝花算不算浪漫
2020/01/14
7430
【一起学源码-微服务】Ribbon 源码一:Ribbon概念理解及Demo调试
相关推荐
SpringCloudRibbon负载均衡实现原理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验