SpringCloud Ribbon学习整理

准备与说明

  1. 源码地址:https://github.com/hlmk/microservicecloud.git
  2. springcloud版本:Dalston.SR1
  3. springboot版本:1.5.9.RELEASE
  4. jdk:1.8
  5. 开发工具:Eclipse Java EE IDE for Web Developers
  6. 本篇博客来自尚硅谷SpringCloud视频学习整理的笔记

概述

是什么

SpringCloud Ribbon是基于Netflix Ribbon实现的一套客户端 负载均衡的工具。

简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。

能干吗

LB,即负载均衡(Load Balance),在微服务或分布式集群中经常用的一种应用。
负载均衡简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA。
常见的负载均衡有软件Nginx、LVS、硬件F5等。
相应的在中间件,例如:dubbo和SpringCloud中均给我们提供了负载均衡,SpringCloud的负载均衡算法可以自定义。

  1. 集中式LB
    即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;
  2. 进程内LB
    将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
    Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

官网资料

https://github.com/Netflix/ribbon/wiki/Getting-Started

Ribbon配置初步

  1. microservicecloud-consumer-dept-80客户端模块的pom文件中添加以下包
	<!-- ribbon 负载均衡 -->
  	<dependency>
  		<groupId>org.springframework.cloud</groupId>
  		<artifactId>spring-cloud-starter-eureka</artifactId>
  	</dependency>
  	<dependency>
  		<groupId>org.springframework.cloud</groupId>
  		<artifactId>spring-cloud-starter-ribbon</artifactId>
  	</dependency>
  	<dependency>
  		<groupId>org.springframework.cloud</groupId>
  		<artifactId>spring-cloud-starter-config</artifactId>
  	</dependency>
  	
  1. 修改application.yml 追加Eureka的服务注册地址
#追加如下配置
eureka:
  client:
    register-with-eureka: false
    service-url: 
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka
  1. 对ConfigBean进行新注解@LoadBalanced 获得Rest时加入Ribbon的配置
@Configuration
public class ConfigBean { //boot  --> spring 	applicationContext.xml 	--- @configuration配置   configBean = applicationContext.xml
		
	@Bean
	@LoadBalanced	//Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端 负载均衡的工具。
	public RestTemplate getRestTemplate() {
		return new RestTemplate();
	}
	
	@Bean
	public IRule myRule() {
//		return new RoundRobinRule()//轮询
//		return new RandomRule();//达到的目的,用我们重新选择的随机算法替代默认的轮询。(随机)
		return new RetryRule();//先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务。 
	}
	
}
  1. 主启动类DeptConsumer80_App添加@EnableEurekaClient
@SpringBootApplication
@EnableEurekaClient
//@RibbonClient(name="MICROSERVICECLOUD-DEPT",configuration=MySelfRule.class)//在微服务启动的时候就能够去加载我们自定义的Ribbon配置类,从而使配置生效。 
public class DeptConsumer80_App {

	public static void main(String[] args) {
		SpringApplication.run(DeptConsumer80_App.class, args);
	}
	
}
  1. 修改DeptController_Consumer客户端访问类
    修改使其通过微服务名称去访问微服务private static final String REST_URL_PREFIX = "http://MICROSERVICECLOUD-DEPT";
@RestController
public class DeptController_Consumer {
	
//	private static final String REST_URL_PREFIX = "http://localhost:8001";
	private static final String REST_URL_PREFIX = "http://MICROSERVICECLOUD-DEPT";
	
	/*
	 * RestTemplate提供了多种便捷访问远程Http服务的方法
	 * 是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集
	 * 
	 * 官网地址:https://docs.spring.io/spring-framework/docs/4.3.7.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html
	 * 
	 * 使用:使用restTemplate访问restful接口非常的简单粗暴无脑。
	 * 		(url, requestMap, ResponseBean.class)这三个参数分别代表REST请求地址、请求参数、HTTP响应转换被转换成的对象类型
	 * 
	 */
	@Autowired
	private RestTemplate restTemplate;
	
	@RequestMapping(value="/consumer/dept/add")
	public boolean add(Dept dept) {
		return restTemplate.postForObject(REST_URL_PREFIX + "/dept/add", dept, Boolean.class);
	}
	
	@RequestMapping(value="/consumer/dept/get/{id}")
	public Dept get(@PathVariable("id")Long id) {
		return restTemplate.getForObject(REST_URL_PREFIX + "/dept/get/" + id, Dept.class);
	}
	
	@SuppressWarnings("unchecked")
	@RequestMapping(value="/consumer/dept/list")
	public List<Dept> list() {
		return restTemplate.getForObject(REST_URL_PREFIX + "/dept/list", List.class);
	}
	
	@RequestMapping(value="/consumer/dept/discovery")
	public Object discovery() {
		return restTemplate.getForObject(REST_URL_PREFIX + "/dept/discovery", Object.class);
	}

}

  1. 先启动3个eureka集群后,再启动microservicecloud-provider-dept-8001并注册进eureka
    SpringCloud Ribbon学习整理

  2. 启动microservicecloud-consumer-dept-80

  3. 测试
    http://localhost/consumer/dept/get/1
    SpringCloud Ribbon学习整理
    http://localhost/consumer/dept/list
    SpringCloud Ribbon学习整理
    http://localhost/consumer/add?dname=大数据
    SpringCloud Ribbon学习整理

Ribbon负载均衡

  1. 架构说明
    SpringCloud Ribbon学习整理
    Ribbon在工作时分成两步
    第一步先选择EurekaServer,它优先选择在同一个区域内负载较少的server
    第二步再根据用户指定的策略,在server取到的服务注册列表中选择一个地址。
    其中ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。

  2. 参考microservicecloud-provider-dept-8001,新建两份,分别命名为8002、8003

  3. 新建8002、8003数据库,各自微服务分别连各自的数据库
    8002脚本

DROP DATABASE IF EXISTS cloudDB02;

CREATE DATABASE cloudDB02 CHARACTER SET UTF8;

USE cloudDB02;

create table dept(
	deptno bigint not null primary key auto_increment,
	dname varchar(60),
	db_source varchar(60)
);

insert into dept(dname,db_source) values('开发部',DATABASE());
insert into dept(dname,db_source) values('人事部',DATABASE());
insert into dept(dname,db_source) values('财务部',DATABASE());
insert into dept(dname,db_source) values('市场部',DATABASE());
insert into dept(dname,db_source) values('运维部',DATABASE());

8003脚本

DROP DATABASE IF EXISTS cloudDB03;

CREATE DATABASE cloudDB03 CHARACTER SET UTF8;

USE cloudDB03;

create table dept(
	deptno bigint not null primary key auto_increment,
	dname varchar(60),
	db_source varchar(60)
);

insert into dept(dname,db_source) values('开发部',DATABASE());
insert into dept(dname,db_source) values('人事部',DATABASE());
insert into dept(dname,db_source) values('财务部',DATABASE());
insert into dept(dname,db_source) values('市场部',DATABASE());
insert into dept(dname,db_source) values('运维部',DATABASE());
  1. 修改8002、8003各自的yml
    8002yml
    需要修改的配置
    1. 端口
    2. 连接的数据库
      不能修改的配置
    3. 微服务的名称(必须一致,否则负载均衡会出问题)

8003yml
需要修改的配置
1. 端口
2. 连接的数据库
不能修改的配置
1. 微服务的名称(必须一致,否则负载均衡会出问题)

备注
端口
数据库连接
SpringCloud Ribbon学习整理
对外暴露统一的微服务实例名
SpringCloud Ribbon学习整理
5. 启动三个eureka集群配置区
6. 启动三个dept微服务并各自测试通过
http://localhost:8001/dept/list
http://localhost:8002/dept/list
http://localhost:8003/dept/list
SpringCloud Ribbon学习整理
7. 启动microservicecloud-consumer-dept-80
8. 客户端通过ribbon完成负载均衡并访问上一步的dept微服务
9. 总结:ribbon其实就是一个软负载均衡的客户端组件,它可以和其它所需请求的客户端结合使用,和eureka结合只是其中的一个实例。

Ribbon核心组件IRule

IRule:根据特定算法中从服务列表中选取一个要访问的服务
RoundRobinRule:轮询
RandomRule:随机
AvailabilityFilteringRule:会先过滤由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问。
WeightedResponseTimeRule:根据平均响应时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越高。刚启动时如果统计信息不足,则使用RoundRibbonRule策略,等统计信息足够,会切换到WeightedResponseTimeRule
RetryRule:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务
BestAvailableRule:会先过滤掉由于多次访问故障而处于断路跳闸状态的服务,然后选择一个并发量最小的服务
ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器

指定轮询策略的核心方法
在myRule方法中指定要执行的负载均衡策略

@Configuration
public class ConfigBean { //boot  --> spring 	applicationContext.xml 	--- @configuration配置   configBean = applicationContext.xml
		
	@Bean
	@LoadBalanced	//Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端 负载均衡的工具。
	public RestTemplate getRestTemplate() {
		return new RestTemplate();
	}
	
	@Bean
	public IRule myRule() {
//		return new RoundRobinRule()//轮询
//		return new RandomRule();//达到的目的,用我们重新选择的随机算法替代默认的轮询。(随机)
		return new RetryRule();//先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务。 
	}
	
}

Ribbon自定义

  1. 修改microservicecloud-consumer-dept-80
  2. 主启动类添加@RibbonClient
    在启动该微服务的时候就能去加载我们自定义Ribbon配置类,从而使配置生效,比如:
    @RibbonClient(name="MICROSERVICECLOUD-DEPT",configuration=MySelfRule.class)
  3. 注意配置细节
    官方文档明确的给出了警告:
    这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有Ribbon客户端所共享,也就是说我们到不到特殊化定制的目的了。

也就是说,主启动类不能扫描到自定义的配置类,否则不会生效。

自定义配置类代码

@Configuration
public class MySelfRule {

	@Bean
	public IRule myRule() {
		return new RandomRule();//ribbon默认是轮询,我自定义为随机
//		return new RandomRule_ZY();
	}
	
}
  1. 步骤
    1. 新建package com.cht.myrule包并在其下新建自定义Robbin规则类
    2. 修改主启动类
      添加注解:@RibbonClient(name="MICROSERVICECLOUD-DEPT",configuration=MySelfRule.class)
    3. 测试 http://localhost/consumer/dept/list

自定义规则深度解析
问题:依旧轮询策略,但是加上新需求,每个服务器要求被调用5次,也即以前是每台机器一次,现在是每台机器5次

解析源码:https://github.com/Netflix/ribbon/blob/master/ribbon-loadbalancer/src/main/java/com/netflix/loadbalancer/RandomRule.java

IRule架构图
SpringCloud Ribbon学习整理

自定义的轮询策略代码

/**
 * 自定义负载均衡算法,当前服务调用5次后,轮询调用下一个服务执行5次
 * @author CHT
 *
 */
public class RandomRule_ZY extends AbstractLoadBalancerRule {

	private int total = 0;			//总共被调用的次数,目前要求每台被调用5次
	private int currentIndex = 0;	//当前提供服务的机器号
	
	
	
    /**
     * Randomly choose from all living servers
     */
    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) {
                /*
                 * No servers. End regardless of pass, because subsequent passes
                 * only get more restrictive.
                 */
                return null;
            }

//            int index = rand.nextInt(serverCount);
//            server = upList.get(index);

            if(total < 5) {
            	server = upList.get(currentIndex);
            	total++;
            }else {
            	total = 0;
            	currentIndex++;
            	if(currentIndex >= upList.size()) {
            		currentIndex = 0;
            	}
            } 
            
            
            if (server == null) {
                /*
                 * The only time this should happen is if the server list were
                 * somehow trimmed. This is a transient condition. Retry after
                 * yielding.
                 */
                Thread.yield();
                continue;
            }

            if (server.isAlive()) {
                return (server);
            }

            // Shouldn't actually happen.. but must be transient or a bug.
            server = null;
            Thread.yield();
        }

        return server;

    }

	@Override
	public Server choose(Object key) {
		return choose(getLoadBalancer(), key);
	}

	@Override
	public void initWithNiwsConfig(IClientConfig clientConfig) {
		// TODO Auto-generated method stub
		
	}
}