为什么要使用Ribbon
上一章节我们学习了注册中心《阿里面试官问我:到底知不知道什么是Eureka,这次,我没沉默》,我们知道但我们存在多个服务提供者的时候,我们会让所有的服务提供者将服务节点信息都注册到EurekaServer中,然后让客户端去拉取一份服务注册列表到本地,服务消费者会从服务注册列表中找到合适的服务实例信息,通过IP:Port的方式去调用服务。
那么,消费者是如何决定调用哪一个服务实例呢,此时,本章节的主人公Ribbon默默的站了起来,笑着说,没错,正是在下。
Srping Cloud Ribbon 是基于 Netflix Ribbon实现的一套 客户端负载均衡的工具。
简单的说,Ribbon是Netflix发布的开源顶目,主要功能是解析配置中或注册中心的服务列表,通过客户端的软件负均衡算法来实现服务请求的分发。
Ribbon客户端组件提供一系列完善配置项如连接超时,重试等。
简单的说,就是在配置文件中列出LoadBalancer后面所有的机器,Ribbon会自动的帮助你基于某种规则(筒单轮洵,随机连接等)去连接这些机器。
我们也很容易使Ribbon实现自定义负载均衡算法。
Ribbon和Nginx又有什么不同呢?
集中式负载均衡
如图所示就是集中式负载均衡集中式负载均衡就好比房屋中介,他手里有很多房屋信息。
服务消费者就好比是需要租房的租客,我们并不能直接的和房屋主人进行租房交易,而是通过房屋中介选择房屋信息达成租房交易。
也就是说,客户端的请求信息并不会直接去请求服务实例,而是在到达负载均衡器的时候,通过负载均衡算法选择某一个服务实例,然后将请求转发到这个服务实例上。
集中式负载均衡又分为硬件负载均衡,如F5,软件负载均衡,如Nginx。
客户端负载均衡
如图所示就是客户端负载均衡就好比,现在有很多租房app,很多房屋的主人并不想通过中介租房,想省一笔中介费。
很多租客也想省一笔中介费,不想通过中介租房,于是租客将App上的租房信息记录在自己的笔记本上。
但是由于租客租房经验不足,并不知道应该选择哪一套房,此时刚好租客有一个做房屋中介好友(就是这么巧),于是租客将好友请到家里来,让他帮忙出谋划策,选择好房源,然后租客到时候直接去看房租房。
也就是说,此时客户端请求不会再去负载均衡器上进行转发了,客户端自己维护了一套服务列表,要掉用的某个服务实例之前首先会通过负载均衡算法选择一个服务节点,直接将请求发送到该服务节点上。
Ribbon 总体架构
首先我们看一张图:
接下来我们详细介绍下上图所示的Ribbon核心的6个组件接口。
IRule
释义:IRule就是根据特定算法中从服务器列表中选取一个要访问的服务,Ribbon默认的算法为轮询算法。
接下来我们来看一张IRule的类继承关系图:
其中用红色方框圈出来的叶子节点是现在还在使用的负载均衡算法,而用紫色方框圈出来的是已经废弃了的。
由图可知,目前我们使用的负载均衡算法有以下几种:
RoundRobinRule和WeightedResponseTimeRule
首先说明下 RoundRobinRule(轮询)策略,虽然我没有圈出来但是他是很常用的负载均衡算法,表示表示每次都取下一个服务器。
线性轮询算法实现:每一次把来自用户的请求轮流分配给服务器,从1开始,直到N(服务器个数),然后重新开始循环。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。
通过图上的继承关系我们可知RoundRobinRule和WeightedResponseTimeRule是继承和被继承的关系。
WeightedResponseTimeRule是根据平均响应时间计算所有服务的权重,响应时间越快的服务权重越大被选中的概率越大。
有一个默认每30秒更新一次权重列表的定时任务,该定时任务会根据实例的响应时间来更新权重列表。
但是由于刚启动时如果统计信息不足,则使用RoundRobinRule(轮询)策略,等统计信息足够,会切换到WeightedResponseTimeRule。
AvailabilityFilteringRule
AvailabilityFilteringRule会先过滤掉由于多次访问故障而处于断路器状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问。
ZoneAvoidanceRule
综合判断Server所在区域的性能和Server的可用性选择服务器。
BestAvailableRule
会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。
RandomRule
随机选取服务。
使用 ThreadLocalRandom.current().nextInt(serverCount);随机选择。
RetryRule
先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败侧在指定的时间会进行重试,继续获取可用的服务。
自定义负载均衡算法
自定义负载均衡算法主要分三步:
-
实现IRule接口或者继承AbstractLoadBalancerRule类
-
重写choose方法
-
指定自定义的负载均衡策略算法类
首先我们创建一个MyRule类,但是这个类不能随便乱放。
官方文档给出警告:这个自定义的类不能放在@ComponentScan所扫描的当前包以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,也就是我们达不到特殊化指定的目的了。
MyRule
package javaer.study.RibbonTest;
import com.netflix.loadbalancer.IRule;
/**
* 自定义负载均衡策略
*
* @author javaMaster
* 公众号:【Java 学习部落】
* @create 2020 09 14
* @Version 1.0.0
*/
public class MyRule {
public IRule myRule () {
return new MyRule_CustomAlgorithm ();
}
}
接下自定义一个负载均衡策略算法 MyRule_CustomAlgorithm。
定义算法:每台服务节点调用三次,代码如下
package javaer.study.RibbonTest;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.List;
/**
* 自定义负载均衡算法
*
* @author javaMaster
* 公众号:【Java学习部落】
* @create 2020 09 14
* @Version 1.0.0
*/
public class MyRule_CustomAlgorithm extends AbstractLoadBalancerRule {
// total = 0 // 当total==5以后,我们指针才能往下走,
// index = 0 // 当前对外提供服务的服务器地址,
// total需要重新置为零,但是已经达到过一个5次,我们的index = 1
// 分析:我们5次,但是微服务只有8001 8002 8003 三台,OK?
private int total = 0; // 总共被调用的次数,目前要求每台被调用5次
private int currentIndex = 0; // 当前提供服务的机器号
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;
}
if(total < 3){
server = upList.get(currentIndex);
total++;
}else {
total = 0;
currentIndex++;
if(currentIndex >= upList.size())
{
currentIndex = 0;
}
}
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
server = null;
Thread.yield();
}
return server;
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
}
使用自定义负载均衡策略的方式:
第一种,直接在启动类上添加
@RibbonClient(name = "order-service",configuration = MyRule.class)
name指的是服务名称,configuration指的是自定义算法类。
第二种,在配置文件中指定自定义的负载均衡算法类。
# 指定order-service的负载策略
user-service.ribbon.NFLoadBalancerRuleClassName=javaer.study.RibbonTest.MyRule
ServerList
ServerList用于获取服务节点列表并存储的组件。
存储分为静态存储和动态存储两种方式。
默认从配置文件中获取服务节点列表并存储称为静态存储。
从注册中心获取对应的服务实例信息并存储称为动态存储。
ServerListFilter
ServerListFilter主要用于实现服务实例列表的过滤,通过传入的服务实例清单,根据规则返回过滤后的服务实例清单。
ServerListUpdater
ServerListUpdater是列表更新器,用于动态的更新服务列表。
ServerListUpdater通过任务调度去定时实现更新操作。所以它有个唯一实现子类:PollingServerListUpdater。
PollingServerListUpdater动态服务器列表更新器要更新的默认实现,使用一个任务调度器ScheduledThreadPoolExecutor完成定时更新。
IPing
缓存到本地的服务实例信息有可能已经无法提供服务了,这个时候就需要有一个检测的组件,来检测服务实例信息是否可用。
IPing就是用来客户端用于快速检查服务器当时是否处于活动状态(心跳检测)
ILoadBalancer
ILoadBalancer是整个Ribbon中最重要的一个环节,它将负载均衡器最核心的资源也就是所有的服务的获取,更新,过滤,选择等操作都能安排的妥妥当当。
package com.netflix.loadbalancer;
import java.util.List;
public interface ILoadBalancer {
void addServers(List<Server> var1);
Server chooseServer(Object var1);
void markServerDown(Server var1);
/** @deprecated */
@Deprecated
List<Server> getServerList(boolean var1);
List<Server> getReachableServers();
List<Server> getAllServers();
}
ILoadBalancer最重要的重要是获取所有的服务节点信息,或者是获取可访问的服务节点信息,然后通过ServerListFilter按照指定策略过滤服务节点列表,通过ServerListUpdater动态更新一组服务列表,通过IPing剔除非存活状态下的服务节点以及根据IRule从现有服务器列表中选择一个服务。
Ribbon选择一个可用服务的详细流程
通过上图可知,流程如下:
-
通过ServerList从配置文件或者注册中心获取服务节点列表信息。
-
某些情况下我们可能需要通过通过ServerListFilter按照指定策略过滤服务节点列表。
-
为了避免每次都要去注册中心或者配置文件中获取服务节点信息,我们会将过滤后的服务列表信息存到本地内存。此时如果新增服务节点或者是下线某些服务时,我们需要通过ServerListUpdater来动态更新服务列表。
-
当有些服务节点已经无法提供服务后,我们会通过IPing(心跳检测)来剔除服务。
-
最后ILoadBalancer 接口通过IRule指定的负载均衡算法去服务列表中选取一个服务。
Ribbon的使用方式
总体来说Ribbon 的使用方式分为三种
第一种,使用原生API的方式
首先我们创建一个RibbonClient工程,然后创建一个RibbonTest类:
RibbonTest
package javaer.study.RibbonTest;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.LoadBalancerBuilder;
import com.netflix.loadbalancer.RandomRule;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.reactive.LoadBalancerCommand;
import com.netflix.loadbalancer.reactive.ServerOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.Arrays;
import java.util.List;
/**
* 测试Ribbon原生Api用法
*
* @author javaMaster
* 公众号:【Java学习部落】
* @create 2020 09 11
* @Version 1.0.0
*/
@RestController
@RequestMapping("/ribbon")
public class RibbonTest {
// @Autowired
// private LoadBalancerClient loadBalancer;
@Autowired
private RestTemplate restTemplate;
@GetMapping("/test")
public String getMsg() {
//使用Ribbon原生API调用服务
//手动创建服务列表,当然也可以从注册中心中获取到服务列表
List<Server> serverList = Arrays.asList(new Server("localHost", 7777),
new Server("localHost", 8888),
new Server("localHost", 9999));
BaseLoadBalancer baseLoadBalancer =
LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
//设置负载均衡策略IRule,默认使用轮询,此处我们设置为随机策略
baseLoadBalancer.setRule(new RandomRule());
for (int i = 0; i < 10; i++) {
String result = LoadBalancerCommand.<String>builder().withLoadBalancer(baseLoadBalancer).build()
.submit(new ServerOperation<String>() {
public Observable<String> call(Server server) {
try {
String addr = "http://" + server.getHost() + ":" + server.getPort();
System.out.println("当前调用的服务地址为:" + addr);
return Observable.just("");
} catch (Exception e) {
return Observable.error(e);
}
}
}).toBlocking().first();
}
}
}
启动项目,访问 http://localhost:8008/ribbon/test,运行结果如下:
因为我们设置的IRule是随机策略,所以我们看到访问结果是从服务列表随机获取服务地址进行访问。
第二种,当我们整合了Spring-Cloud时,我们就可以使用Ribbon + RestTemplate来实现负载均衡。
因为我们要实现通过Ribbon + RestTemplate通过指定的负载均衡的策略去选取某一个服务进行调用,所以我们先来创建一个订单服务OrderService。
首先我们在配置文件中添加配置信息;
//指定服务名称
spring.application.name=order-service
//指定EurekaServer的访问地址
eureka.client.serviceUrl.defaultZone=http:
//localhost:8761/eureka/
接下来创建一个OrderController
package javaer.study.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 订单服务控制类
*
* @author javaMaster
* 公众号:【Java 学习部落】
* @create 2020 09 12
* @Version 1.0.0
*/
@RestController
public class OrderController {
@Value("${server.port}")
private String port;
/**
* 返回一条消息
*/
@GetMapping("/test")
public String test() throws InterruptedException {
Thread.sleep(3000);
return "调用服务的地址的端口为: " + port;
}
}
最后我们在启动类上加上@EnableEurekaClient注解;
package javaer.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class OrderserviceApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(OrderserviceApplication.class).web(WebApplicationType.SERVLET).run(args);
}
}
代码部分完成,然后我们启动上一篇文章中搭建的EurekaServer服务。
启动成功后,我们访问 http://localhost:8761/,结果如下,我们发现此时并有服务注册到Eureka注册中心。
然后我们在下图处分别配置7777,8888,9999三个端口启动。
然后我们刷新之前打开的 http://localhost:8761/ 页面,我们发现此时已经有三个服务名为order-service,端口号分别7777,8888,9999的服务注册了进来:
好了,多个服务已经搭建好了,接下来,我们就要通过Ribbon+RestTemplate的方式从Eureka注册中心中获取服务列表,并通过负载均衡策略访问指定的服务节点。
第一步,我们依旧使用RibbonClient工程,我们创建一个RestTemplateConfig类来配置RestTemplate实例。
RestTemplateConfig
package javaer.study.RibbonTest;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate配置类
*
* @author javaMaster
* 公众号:【Java学习部落】
* @create 2020 09 14
* @Version 1.0.0
*/
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
眼尖的同学肯定已经发现了,我们添加了一个@LoadBalanced注解,添加了该注解后,我们就不需要使用IP+端口的形式去调用服务了,我们可以直接使用服务名并且自带负载均衡功能去调用服务。
最后我们修改RibbonTest代码:
package javaer.study.RibbonTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* 测试Ribbon原生Api用法
*
* @author javaMaster
* 公众号:【Java 学习部落】
* @create 2020 09 11
* @Version 1.0.0
*/
@RestController
public class RibbonTest {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/test")
public String getMsg() {
String msg = restTemplate.getForObject("http://order-service/test", String.class);
return msg;
}
}
最后我们启动RibbonClient项目,由于我们并没有设置负载均衡策略,所以默认使用轮询策略来调度服务。
项目启动成功后,我们访问 http://localhost:8008/ribbon/test,刷新三次页面我们,访问结果依次如下:
可能你的访问顺序并不是按照我这个顺序来的,但是一定是三个端口循环调用。
第三种,使用Ribbon+Fegin,这种方式后续会有专文讲解,此处不做多演示。
Ribbon 饥饿加载(eager-load)模式
我们在搭建完springcloud微服务时,经常会发生这样一个问题:我们服务消费方调用服务提供方接口的时候,第一次请求经常会超时,再次调用就没有问题了。
为什么会这样?
主要原因是Ribbon进行客户端负载均衡的Client并不是在服务启动的时候就初始化好的,而是在调用的时候才会去创建相应的Client,所以第一次调用的耗时不仅仅包含发送HTTP请求的时间,还包含了创建RibbonClient的时间,这样一来如果创建时间速度较慢,同时设置的超时时间又比较短的话,从而就会很容易发生请求超时的问题。
解决方法
既然超时的原因是第一次调用时还需要创建RibbonClient,那么我们能不能提前创建RibbonClient呢?
既然我们都能想到,那么SpringCloud开发者肯定也能想到。
所以我们可以通过设置下面两个属性来提前创建RibbonClient:
//开启Ribbon的饥饿加载模式
ribbon.eager-load.enabled=true
//指定需要饥饿加载的服务名
ribbon.eager-load.clients=cloud-shop-userservice
Ribbon 总结
本文介绍了Ribbon的使用场景,介绍了Ribbon和Nginx的区别,从Ribbon的整体架构入手,详细介绍了Ribbon的五大组件IRule,IPing,ServerList,ServerListFilter,ServerListUpdater。并且详细说明的负载均衡器的核心接口ILoadBalancer。以及Ribbon的使用方式和Ribbon的饥饿加载模式。
Ribbon负载均衡是SpringCloud生态系统中不可缺少的一环,也是面试中经常会出现的高频面试题。
原创不易,如果大家喜欢,赏个分享点赞在看三连吧。和大家一起成为这世界上最优秀的人。