SpringCloud源码:Ribbon负载均衡分析

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

SpringCloud版本为:Edgware.RELEASE。

一.时序图

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

二.源码分析

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

@GetMapping("/user/{id}")  public User findById(@PathVariableLong id) {//return this.restTemplate.getForObject("http://192.168.2.110:8001/" + id, User.class);returnthis.restTemplate.getForObject("http://microservice-provider-user/"+ id, User.class);  }

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

首先从 RestTemplate#getForObject 开始:

public T getForObject(String url,ClassresponseType,Object...uriVariables)throwsRestClientException{// 设置RequestCallback的返回类型responseTypeRequestCallback requestCallback = acceptHeaderRequestCallback(responseType);// 实例化responseExtractorHttpMessageConverterExtractor responseExtractor =newHttpMessageConverterExtractor(responseType, getMessageConverters(), logger);returnexecute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);}

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

publicTexecute(String url, HttpMethod method, RequestCallback requestCallback,

ResponseExtractor<T> responseExtractor, Object... uriVariables)throwsRestClientException{// 组装 URIURI expanded = getUriTemplateHandler().expand(url, uriVariables);// 实际"执行"的地方returndoExecute(expanded, method, requestCallback, responseExtractor);}

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

protected T doExecute(URI url, HttpMethod method, RequestCallback requestCallback,ResponseExtractor responseExtractor) throws RestClientException {ClientHttpResponse response =null;try{// 实例化请求,url为请求地址,method为GETClientHttpRequest request = createRequest(url, method);if(requestCallback !=null) {// AcceptHeaderRequestCallbackrequestCallback.doWithRequest(request);}// 实际处理请求的地方response = request.execute();// 处理response,记录日志和调用对应的错误处理器handleResponse(url, method, response);if(responseExtractor !=null) {// 使用前面的HttpMessageConverterExtractor从Response里面抽取数据returnresponseExtractor.extractData(response);}else{returnnull;}}......}

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

publicfinalClientHttpResponse execute() throws IOException {// 断言请求还没被执行过assertNotExecuted();// 跳转到 executeInternal 处理请求ClientHttpResponse result = executeInternal(this.headers);// 标记请求为已经执行过this.executed =true;returnresult;}

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

protectedClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {// 首次进来,bytes内容为空byte[] bytes =this.bufferedOutput.toByteArray();if(headers.getContentLength() <0) {// 设置 Content-Length 为 1headers.setContentLength(bytes.length);}// 模板方法,跳转到了实现类中的方法,InterceptingClientHttpRequest#executeInternalClientHttpResponse result = executeInternal(headers, bytes);// 拿到结果后,清空缓存this.bufferedOutput =null;returnresult;}

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

protectedfinalClientHttpResponseexecuteInternal(HttpHeaders headers,byte[] bufferedOutput)throwsIOException{InterceptingRequestExecution requestExecution =newInterceptingRequestExecution();// InterceptingRequestExecution是一个内部类returnrequestExecution.execute(this, bufferedOutput);}// 内部类,负责执行请求privateclassInterceptingRequestExecutionimplementsClientHttpRequestExecution{privatefinalIterator iterator;// 所有HttpRequest的拦截器publicInterceptingRequestExecution(){this.iterator = interceptors.iterator();}@OverridepublicClientHttpResponseexecute(HttpRequest request,byte[] body)throwsIOException{if(this.iterator.hasNext()) {// 如果还有下一个拦截器,则执行其拦截方法// 这里的拦截器是 MetricsClientHttpRequestInterceptor,对应"metrics"信息,记录执行时间和结果ClientHttpRequestInterceptor nextInterceptor =this.iterator.next();// 执行拦截方法returnnextInterceptor.intercept(request, body,this);}......}}

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

publicClientHttpResponse intercept(HttpRequest request,byte[] body,ClientHttpRequestExecution execution)throwsIOException {longstartTime = System.nanoTime();// 标记开始执行时间ClientHttpResponse response =null;try{// 传入请求和Body,处理执行,又跳转回 InterceptingRequestExecutionresponse = execution.execute(request, body);returnresponse;}finally{// 在执行完方法,返回response之前,记录一下执行的信息SmallTagMap.Builder builder = SmallTagMap.builder();for(MetricsTagProvidertagProvider :tagProviders) {for(Map.Entrytag :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,主角登场了,终于等到你,还好没放弃:

publicClientHttpResponseintercept(finalHttpRequest request,finalbyte[] body,finalClientHttpRequestExecution execution)throwsIOException{// 获取原始URIfinalURI originalUri = request.getURI();// 获取请求中的服务名字,也就是所谓的"虚拟主机名"String serviceName = originalUri.getHost();// 转由 LoadBalancerClient 处理请求returnthis.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));}

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

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

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

publicTexecute(String serviceId, LoadBalancerRequest<T> request)throwsIOException{// serviceId即前面的虚拟主机名 "microservice-provider-user",获取loadBalancer//这里获取到的是 DynamicServerListLoadBalancerILoadBalancer loadBalancer = getLoadBalancer(serviceId);// 基于loadBalancer,选择实际处理请求的服务提供者Server server = getServer(loadBalancer);if(server ==null) {thrownewIllegalStateException("No instances available for "+ serviceId);}RibbonServer ribbonServer =newRibbonServer(serviceId, server, isSecure(server,serviceId), serverIntrospector(serviceId).getMetadata(server));returnexecute(serviceId, ribbonServer, request);}

RibbonLoadBalancerClient#getServer,转交 loadBalancer 选择Server:

protectedServer getServer(ILoadBalancer loadBalancer) {if(loadBalancer ==null) {returnnull;}// 由 loadBalancer 完成选Server的重任,这里的 key 是默认值 "default"returnloadBalancer.chooseServer("default");//TODO:better handling of key}

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

publicServer chooseServer(Object key) {if(!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <=1) {            logger.debug("Zone aware logic disabled or there is only one zone");// 到了 BaseLoadBalancer的chooseServerreturnsuper.chooseServer(key);        }        ......    }

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

publicServer chooseServer(Object key) {if(counter ==null) {            counter = createCounter();        }// counter是一个计数器,起始值是"0",下面自增一次,变为 "1"counter.increment();if(rule ==null) {returnnull;        }else{try{// 默认的挑选规则是 "ZoneAvoidanceRule"returnrule.choose(key);            }catch(Exception e) {                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);returnnull;            }        }    }

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

publicServerchoose(Object key) {        ILoadBalancer lb = getLoadBalancer();        Optionalserver= getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);if(server.isPresent()) {            returnserver.get();        }else{            returnnull;        }          }

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

public Optional chooseRoundRobinAfterFiltering(List servers,ObjectloadBalancerKey) {// 首先拿到所有"合格"的ServerList eligible = getEligibleServers(servers, loadBalancerKey);if(eligible.size() ==0) {returnOptional.absent();        }// 在 incrementAndGetModulo 中获取,"自增取模"returnOptional.of(eligible.get(incrementAndGetModulo(eligible.size())));    }

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

privateintincrementAndGetModulo(intmodulo) {for(;;) {intcurrent = nextIndex.get();// 第一次 current是"0"intnext= (current +1) % modulo;// current+1对size取模,作为下次的"current"// "0" == current,则以原子方式将该值设置为 nextif(nextIndex.compareAndSet(current,next))returncurrent;        }    }

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

2018-12-0918:55:30.794c.i.c.s.user.controller.MovieController:microservice-provider-user:192.168.2.117:80012018-12-0918:55:33.196c.i.c.s.user.controller.MovieController:microservice-provider-user:192.168.2.117:80002018-12-0918:55:34.713c.i.c.s.user.controller.MovieController:microservice-provider-user:192.168.2.117:80012018-12-0918:55:34.975c.i.c.s.user.controller.MovieController:microservice-provider-user:192.168.2.117:80002018-12-0918:55:35.175c.i.c.s.user.controller.MovieController:microservice-provider-user:192.168.2.117:80012018-12-0918:55:35.351c.i.c.s.user.controller.MovieController:microservice-provider-user:192.168.2.117:80002018-12-0918:55:35.534c.i.c.s.user.controller.MovieController:microservice-provider-user:192.168.2.117:8001

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 本文作者:陈刚,叩丁狼高级讲师。原创文章,转载请注明出处。 本文章会通过断点跟踪的方式来解读 Ribbon 源码 ...
    叩丁狼教育阅读 1,977评论 0 3
  • 说起负载均衡一般都会想到服务端的负载均衡,常用产品包括LBS硬件或云服务、Nginx等,都是耳熟能详的产品。 而S...
    程序员技术圈阅读 1,358评论 0 2
  • 看spring cloud源码分析好绕,还是坚持看完了。 先总结一下Ribbon的运行流程,可以跳过总结看下面,然...
    二月_春风阅读 2,916评论 1 2
  • 自2004年6月高中毕业后,我已经把这本书看过三遍了,起初还不是很明白,最后以丰富的感情讲给我妹妹听的时候,忽然茅...
    张淇阅读 251评论 0 1
  • 青春不是年华,而是心境;青春不是粉面、红唇、柔膝,而是深沉的意志,恢宏的想象,炙热的感情;青春是生命的深泉在涌流。...
    孤独的烟阅读 242评论 0 0