什么是Ribbon
SpringCloud Ribbon是基于Netfix Ribbon实现的一套客户端负载均衡算法和远程服务调用的工具,Ribbon是Netfix公司发布的开源项目,主要功能是提供客户端的负载均衡算法及远程服务调用。Ribbon客户端组件提供了一系列完善的配置项(超时连接,重试机制等…),只要在配置文件中列出LoadBalancer的机器,Ribbon会自动帮助你基于某种规则(轮询)去远程调用,那么使用Ribbon很容易实现自定义的负载均衡策略。
客户端负载均衡和服务端负载均衡
- 服务端负载均衡,是在服务的消费者和提供者之间使用单独的负载均衡,例如:Nginx,F5等
- 客户端负载均衡,是在微服务调用接口时从服务治理(注册中心)获取服务提供者信息列表后缓存到JVM本地,在服务调用时根据负载策略选取一个服务,例如:Ribbon。
- 实际上我们在SpringCloud Eureka中使用RestTemplate+Loadbalanced实现了负载均衡。
实际上Eureka中就集成了Ribbon和LoadBalanced,所以我们在使用Eureka作为服务发现时,不需要单独引入Ribbon和LoadBalanced
Ribbon常用方法
GET请求方式
- getForObject
通过HttpMessageConverterExtractor对HTTP的请求响应体body内容进行对象转换,实现请求直接返回包装好的对象内容。
//配置eureka服务名称,通过服务名在euraka集群服务列表获取服务
//必须配置负载均衡(RestTemplate),否则在服务名称下存在多个服务实体,否则无法确定到服务提供者
private final static String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
CommonResult commonResult = restTemplate
.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class, id);
return commonResult;
}
- forEntity
该方法返回的是ResponseEntity,该对象是Spring对HTTP请求响应的封装,其中主要存储了HTTP的几个重要元素,比如HTTP请求状态码的枚举对象HttpStatus(也就是我们常说的404、500这些错误码)、在它的父类HttpEntity中还存储着HTTP请求的头信息对象HttpHeaders以及泛型类型的请求体对象。
@GetMapping("/consumer/payment/get2/{id}")
public CommonResult<Payment> getPaymentById2(@PathVariable("id") Long id) {
ResponseEntity<CommonResult> forEntity = restTemplate
.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class, id);
if(forEntity.getStatusCode().is2xxSuccessful()){
return forEntity.getBody();
}else{
return new CommonResult(444,"操作失败");
}
}
post请求方式
- postForObject
跟getForObject的类型类似,它的作用就是简化postForEntity的后续处理,返回json数据对象。
@GetMapping("/consumer/payment/create")
public CommonResult<Payment> create(Payment payment) {
CommonResult commonResult = restTemplate
.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
return commonResult;
}
- postForEntity
跟getForEntity的类型类似,它的作用就是简化postForEntity的后续处理,返回json数据对象。
@GetMapping("/consumer/payment/create2")
public CommonResult<Payment> create2(Payment payment) {
ResponseEntity<CommonResult> postEntity = restTemplate
.postForEntity(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
if(postEntity.getStatusCode().is2xxSuccessful()){
log.info("返回消息对象Header:"+postEntity.getHeaders()+"返回消息实体信息:"+postEntity.getBody());
return postEntity.getBody();
}else{
return new CommonResult(444,"操作失败");
}
}
实现负载均衡的核心组件IRule
相信很多人看到这里会有疑问,RestTemplate明明是Spring封装用于远程调用的API,又是如何实现负载均衡的呢?这里我们就要说到LoadBlanced注解了,有印象的应该知道我们在配置RestTemplate时,加入了@LoadBlanced注解从而有了负载均衡的能力,这里就要说到LoadBalanced核心功能IRule
IRule会根据用户指定的算法策略,来实现负载均衡功能,具体的负载均衡策略:
策略 | 说明 |
---|---|
com.netfix.loadbalancer.RoundRobinRule | 轮询 |
com.netfix.loadbalancer.RandomRule | 随机 |
com.netfix.loadbalancer.RetryRule | 先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内重试,获取可用服务 |
WeightResponseTimeRule | 对RoundRobinRule进行扩展,根据响应速度越快权重越大 |
BestAvailableRule | 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 |
AvalivilityFilterRule | 先过滤故障实例,再选择并发较小的实例 |
ZoneAvoidanceRule | 默认规则,符合判断服务所在区域的性能和服务可用性选择具体服务实例 |
使用自定义的负载均衡策略
Ribbon官网
Ribbon可以使用独立的负载均衡策略,也可以与其他模块公用,下面我们配置自定义的负载均衡策略
-
在启动类包同级创建Package;避免被@ComponentScan扫描到(如果是在启动类包里,则是模块公用)
-
配置自定义的Rule,这里使用的是随机策略
/**
* @author 张江丰
* @version 18:50
*
*/
@Configuration
public class MySelfRule {
//自定义loadbalanced核心组件IRule的负载均衡模式;给指定的Ribbon配置负载均衡,不能放在项目启动类包下,否则就是所有Ribbon共享负载策略
@Bean
public IRule myRule(){
return new RandomRule();
}
}
- 启动类添加RibbonClient注解;指定连接服务服务实例名及自定义使用的负载均衡策略,可以配置多个
/**
* @author 张江丰
* @version 20:55
* @EnableEurekaClient 声明Eureka客户端
* @RibbonClient 声明Ribbon客户端;name:服务实例名称;configuration: 自定义负载均衡规则
*
*/
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class,args);
}
}
- 测试效果,分别启动Eureka集群2;微服务消费者1;微服务提供者*2
负载均衡调用测试,可见服务提供者8001;8002随机出现
手写轮询算法,使用自己的负载策略
- 负载均衡算法
服务接口被第几次请求%服务集群总数量=实际调用服务器下标
我们服务集群数为2,分别为8001,8002,那么按照轮询算法
请求接口数为1–>1%2=1(对应服务集群下标为1);则8002提供服务
请求接口数为2–>2%2=0(对应服务集群下标为0);则8001提供服务
请求接口数为3–>3%2=1(对应服务集群下标为1);则8002提供服务 - RoundRobinRule源码解析
主要关注choose方法,首先从lb.getReachableServers()方法获取可访问的服务;lb.getAllServers()获取所有服务;nextServerCyclicCounter 原子类计算服务访问次数(默认值为0),通过访问次数%服务集群总数获取实际服务下标
利用compareAndSet(expect,update)乐观锁自旋比较并替换,更新内存中服务访问次数nextServerCyclicCounter 值,返回服务下标next,(Server)allServers.get(nextServerIndex)获取具体提供服务的对象并返回。
public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);
public RoundRobinRule() {
this.nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
this.setLoadBalancer(lb);
}
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;
}
public Server choose(Object key) {
return this.choose(this.getLoadBalancer(), key);
}
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
手写轮询算法
- 在服务集群8001,8002分别添加访问方法,返回服务集群的端口号
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/lb")
public String getPaymentLB() {
return serverPort;
}
- 去掉RestTemplate对象的@Loadblanced注解,不使用自带的负载均衡策略
/**
* @author 张江丰
* @version 20:34
* RestTemplate提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务的模板类,是spring提供的用于访问Rest服务的客户端模板工具集。
*/
@Configuration
public class ApplicationContextConfig {
@Bean
// @LoadBalanced //开启RestTemplate的负载均衡,Ribbon轮训“顺序”调用集群服务
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
- 在服务消费方(调用服务)创建自定义负载策略接口,参数为服务提供者集群信息
/**
* @author 张江丰
* @version ${Date} 10:00
*/
public interface LoadBalancer {
//这个接口主要是为了获取当前访问的这个服务集群中有多少台服务器实例
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
- 编写自己的负载均衡逻辑,参考RoundRobinRule源代码
/**
* @author 张江丰
* @version 10:01
*
*/
@Component
public class MyLB implements LoadBalancer {
//原子类保存到内存的值,用于计算服务访问次数
private AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 通过getAndIncrement()计算获取当前服务访问次数
* getAndIncrement() % serviceInstances.size()=服务访问次数%服务集群实例=提供服务下标
* serviceInstances.get(index)返回具体服务实例
* @param serviceInstances 服务集群实例集合
* @return
*/
@Override
public ServiceInstance instances(List< ServiceInstance > serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
//乐观锁比较并替换,每次访问将atomicInteger++
public final int getAndIncrement() {
int current;
int next;
do {
current = this.atomicInteger.get();
//三元运算,如果服务访问次数>int值MAX,则从0开始计算,否则每次访问++
next = current >= 2147483647 ? 0 : current + 1;
} while(!this.atomicInteger.compareAndSet(current,next));
System.out.println("****当前第几次访问,次数next: " + next);
return next;
}
}
- 服务消费方(服务调用),服务启动类指定Ribbon使用自己MyLB的负载策略
/**
* @author 张江丰
* @version 20:55
* @EnableEurekaClient 声明Eureka客户端
* @RibbonClient 声明Ribbon客户端;name:服务实例名称;configuration: 自定义负载均衡规则(MyLB是自己的负载策略)
* @EnableDiscoveryClient 服务发现客户端
*
*/
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MyLB.class)
@EnableDiscoveryClient
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class,args);
}
}
- controller进行服务调用测试,使用自定义的负载策略
@GetMapping("/consumer/payment/lb")
public CommonResult<String> getLb(){
//1.通过discoveryClient服务发现获取指定名称的微服务实例
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
//2.调用自己的负载策略接口,返回具体的服务实例
ServiceInstance instances1 = loadBalancer.instances(instances);
//3.restTemplate调用服务实例返回信息
ResponseEntity<String> forEntity = restTemplate
.getForEntity(instances1.getUri() + "/payment/lb", String.class, CommonResult.class);
if(forEntity.getStatusCode().is2xxSuccessful()){
return new CommonResult(200,"当前提供服务端口:"+forEntity.getBody());
}else{
return new CommonResult(444,"操作失败");
}
}
通过自定义负载策略,会轮流调用微服务提供者8001,8002端口,实现自定义的轮询
服务消费者(调用方),在自定义负载策略,打印日志访问次数信息
2020-08-18 10:36:24.661 INFO 15724 --- [p-nio-80-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-08-18 10:36:24.662 INFO 15724 --- [p-nio-80-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-08-18 10:36:24.670 INFO 15724 --- [p-nio-80-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 8 ms
****当前第几次访问,次数next: 1
****当前第几次访问,次数next: 2
****当前第几次访问,次数next: 3
****当前第几次访问,次数next: 4
****当前第几次访问,次数next: 5
****当前第几次访问,次数next: 6
****当前第几次访问,次数next: 7
****当前第几次访问,次数next: 8
****当前第几次访问,次数next: 9