一、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详解
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函数我们在这里就不在赘述。
四、负载均衡策略
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 相同,所以该策略是下面我们将要介绍的一些高级策略的父类。
一下高级的选择策略在下一篇博文中介绍