一、概述
LB,即负载均衡(Load Balance),在微服务或分布式集群中经常用的一种应用。负载均衡简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,LVS,硬件 F5等。相应的在中间件,例如:dubbo和SpringCloud中均给我们提供了负载均衡,SpringCloud的负载均衡算法可以自定义。
负载均衡分为集中式LB和进程内LB:
集中式LB,即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5;也可以是软件,如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;
进程内LB,将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时、重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。
Ribbon官方文档
二、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>
2、修改主配置文件appliation.yml:增加eureka的配置,服务消费者端不提供服务,不必将应用注册至Eureka
eureka:
client:
register-with-eureka: false #服务消费者端不提供服务,不必将应用注册至Eureka
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
3、给添加至IOC容器的RestTemplate对象开启负载均衡功能:消费者端是使用restTemplate对象来调用服务接口的,给restTemplate使用@LoadBalanced注解,这样在使用restTemplate对象时就会基于负载均衡去调用服务接口
@Configuration
public class ConfigBean
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
4、在主启动类上加@EnableEurekaClient注解,开启eureka的服务注册和发现:主要是在eureka上发现服务
@SpringBootApplication
@EnableEurekaClient
public class DeptConsumer80_App {
public static void main(String[] args) {
SpringApplication.run(DeptConsumer80_App.class, args);
}
}
5、修改Controler中服务接口的域名地址为Eureka中注册的应用名称:说明restTemplate在调用服务的时候会通过eureka查找注册的应用
@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"; //服务接口地址为eureka中注册的服务id
@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);
}
}
6、测试:启动eureka服务端(集群)、eureka客户端(服务提供者),最后启动消费者端,访问:http://localhost:8080/consumer/dept/get/1
eureka服务端:
测试结果
至此为止似乎并没有Ribbon什么事(其实不然,@LoadBalanced注解的使用就涉及到Ribbon),只是完完整整搭建了一个基于SpringCloud Eureka的分布式服务体系。在这个体系中:
①服务提供者端(也是Eureka的一个客户端)将服务以应用为单位注册至Eureka
②服务消费者端(Eureka的另一个客户端)通过在Eureka中注册的应用名称发现服务应用,然后通过Restful接口的形式调用发现的应用提供的接口
本质上而言,似乎仅仅是服务提供方通过Eureka变换了一下应用的访问域名,访问服务提供者端接口时由通过原来的localhost或者ip地址访问变为了通过我们自定义的应用名称访问,在访问的时候中间隔了一层Eureka,Eureka会将应用名称映射至实际的ip地址和端口,这样我们在通过RestTemplate访问服务提供方的服务时就并必关心ip和端口了,只需要知道自定义的应用名称即可。
这也就可以明白为什么SpringCloud中的服务是以应用为单位注册至Eureka,而不像Dubbo向Zookeeper中注册服务是以接口为单位的,因为SpringCloud的服务是以Restful风格进行暴露的,只要知道了应用的名称也就知道了应用的访问地址,也就可以访问该应用下的所有Restful接口。
但是如果不使用@LoadBalanced注解,在访问消费者端时就会报错:
java.net.UnknownHostException: MICROSERVICECLOUD-DEPT
这说明@LoadBalanced的作用就是让RestTemplate通过负载均衡到Eureka中查询注册的应用,也就是说SpringCloud的负载均衡是基于Eureka或者说是基于服务注册和发现的。这个从@LoadBalanced的源码中的注释也可看出来:使RestTemplate可以使用负载均衡客户端,而此时这个客户端正是ribbon,这里把ribbon的依赖注释掉程序也能正常运行,这是因为Eureka的依赖中已经包含了ribbon的相关依赖
/**
* Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
* @author Spencer Gibb
*/
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
那如何做的负载均衡呢?不妨做一个猜测:其实就是注册至Eureka的多个应用具有相同的应用名称,RestTemplate识别应用是根据应用名称识别的,若识别到多个,则在这多个应用之间做负载均衡。
这里似乎可以看出来,在SpringCloud中要想使用Eureka就必须使用负载均衡,因为只有通过负载均衡才会以应用名称的方式调用服务方在Eureka中注册的应用(@LoadBalanced的作用),从而才能调取到服务方的接口;同样的要想使用负载均衡也必须使用Eureka,因为应用名称的注册是通过Eureka完成的。
三、Ribbon负载均衡
1、架构说明:
Ribbon在工作时分成两步:
①选择 EurekaServer,它优先选择在同一个区域内负载较少的Server
②根据用户指定的策略(默认为轮询),再从Server取到的服务注册列表中选择一个地址(Ribbon提供了多种策略,比如轮询、随机和根据响应时间加权)
流程:
①多个服务提供者将应用注册至Eureka(相同的服务使用相同的应用名称)
②服务消费者从Eureka获取服务应用列表
③服务消费者端根据负载均衡策略发送请求至具体的某一台服务提供者机器
2、示例
①参考microservicecloud-provider-dept-8001,新建两个工程,分别命名为microservicecloud-provider-dept-8002、microservicecloud-provider-dept-8003
②修改这两个工程的主配置文件:主要是改变端口、数据库和Eureka的实例名称(该名称不能相同),注意一定要使服务提供者中的这三个应用的应用名称相同,保证对外暴露统一的服务实例名(这样才能保证注册至Eureka时,这三个应用实例对应的是同一个微服务,也就是说Eureka区分微服务时是根据应用的名称区分的,区分微服务的实例时是根据示例名称区分的)
spring:
application:
name: microservicecloud-dept
eureka:
instance:
instance-id: microservicecloud-dept800X #服务应用在eureka注册时的实例,不能相同
③为新建的两个工程新建对应的数据库和表:
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());
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());
④启动测试:先启动Eureka集群,再启动3个服务提供者,最后启动消费者,多访问几次http://localhost/consumer/dept/list,观察结果(可以看到负载均衡默认为轮询机制)
总结
:Ribbon其实是一个软负载均衡的客户端组件,他可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个示例。
四、Ribbon的核心组件IRule
1、IRule接口
Ribbon通过IRule根据特定算法从服务列表中选取一个要访问的服务,自定义负载均衡算法时只需要实现IRule接口:
public interface IRule{
//该方法定义了具体选择哪个服务器,即该方法的实现是负载均衡算法的本质
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
2、Ribbon为我们提供的算法实现有:
①RoundRobinRule——轮询
②RandomRule——随机
③AvailabilityFilterRule——会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问
④WeightedResponseTimeRule——根据平均响应时间计算所有服务的权重,响应时间越快服务权重越大被选中的几率越大,应用刚启动时如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够时会切换至WeightedResponseTimeRule
⑤RetryRule——先按照RoundRobinRule的策略获取服务,在连续获取某个服务都失败时会在指定时间内重试其他可用的服务
⑥BestAvailableRule——会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
⑦ZoneAvoidanceRule——默认规则,复合判断Server所在区域的性能和Server的可用性选择服务器
3、算法的切换:只需要将相应的实现加入到IOC容器即可,默认使用的是RoundRobinRule
@Configuration
public class ConfigBean
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
@Bean
public IRule myRule(){
return new RetryRule();//使用重试的负载均衡策略
}
}
那能不能将多个实现加入到IOC容器呢?当有多个IRule接口的实现时会怎么选择呢?答案是不可以向IOC容器中添加多个IRule接口的实现,否则会报错:
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.netflix.loadbalancer.IRule' available: expected single matching bean but found 2: myRule,myRule1
也就是说一个应用中只能选择一个负载均衡策略,这也是符合逻辑的,当然使用哪一个我们可以在@Bean的方法中做一些逻辑判断:
@Bean
public IRule myRule() {
if (条件)
return new RetryRule();
else
return new BestAvailableRule();
}
4、自定义负载均衡策略:
①消费者端主启动类加@RibbonClient注解,name属性表示该自定义策略针对哪个微服务起作用,configuration属性表示的是哪个类完成具体的负载均衡算法:在启动该微服务的时候就能去加载我们自定义的Ribbon配置类,从而完成自定义负载均衡
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "MICROSERVICECLOUD-DEPT", configuration = MySelfRule.class)
public class DeptConsumer80_App {
public static void main(String[] args) {
SpringApplication.run(DeptConsumer80_App.class, args);
}
}
②编写自定义配置类,需要特别注意的是官方文档明确给出了警告:这个自定义配置类不能放在@ComponentScan所扫描的包以及其子包下(即不能放在主启动类所在的包及其子包下,因此我们需要新建一个包来放该配置类),否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,也就达不到特殊化定制的目的了(这里所谓的特殊指定的针对某一个微服务,即①中的name属性指定的微服务,如果想使所有的微服务都采用自定义的负载均横算法,也可以使用算法的切换的方式直接在SpringBoot应用的IOC容器中注册自己义的bean即可)
@Configuration
public class MySelfRuleConfig {
@Bean
public IRule myRule(){
return new RandomRule();//此时已然是使用了Ribbon提供的随机算法,我们可以自定义返回该对象达到自定义负载均衡的目的
}
}
这说明@RibbonClient注解可以把其他的配置类作为另外一个IOC容器导入到应用中,相当于加载了两个完全不相干的Spring的beans配置文件,此时应用中会有两个IOC容器。
③自定义负载均衡算法:轮询,每台机器连续5次
配置类修改:
@Configuration
public class MySelfRuleConfig {
@Bean
public IRule myRule(){
return new MySelfRule();//自定义负载均衡算法
}
}
算法类MySelfRule的编写:编写该类的目的就是确定如何获取到具体的某一台机器进行调用,即重写choose(),该类不需要加任何注解
public class MySelfRule extends AbstractLoadBalancerRule {
private Integer totalAccess = 0;
private Integer currentIndex = 0;
@Override
public Server choose(Object key) {// 主要是重写该方法完成负载均衡的自定义
// 该组件由容器注入,见父类
ILoadBalancer loadBalancer = getLoadBalancer();
if (loadBalancer == null)
return null;
Server server = null;// 具体的某一台服务器
while (server == null) {
if (Thread.interrupted())
return null;
// 获取与微服务应用对应的所有注册至Eureka的服务提供者列表(Eureka实例)
List<Server> allServers = loadBalancer.getAllServers();
// 获取所有与微服务应用对应的可用的服务提供者列表
List<Server> reachableServers = loadBalancer.getReachableServers();
if (allServers.size() == 0)
return null;
if (totalAccess < 5) {
// 获取到具体的某台机器
server = reachableServers.get(currentIndex);
totalAccess++;
} else {
totalAccess = 0;
currentIndex++;
if (currentIndex >= reachableServers.size())
currentIndex = 0;
}
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive())
return server;
server = null;
Thread.yield();
}
return server;
}
@Override
public void initWithNiwsConfig(IClientConfig config) {
// TODO Auto-generated method stub
}
}