0%

SpringCloud(五)Ribbon负载均衡

概述

是什么

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

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

能干嘛

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

集中式LB

在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方

进程内LB

将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器.

Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址.

官方资料

Ribbon


Ribbon初步配置

以 之前的 clouddemo-consumer工程为例,理解 Ribbon是一套客户端的负载均衡工具

  1. 添加Ribbon组件依赖(ribbon依赖eureka客户端,eureka又依赖config)
1
2
3
4
5
6
7
8
9
10
11
12
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
  1. 添加 eureka.client配置,找服务先找 eureka
1
2
3
4
5
6
7
server.port: 80

eureka:
client:
serviceUrl:
defaultZone: http://eureka1.com:7001/eureka/,http://eureka2.com:7002/eureka/,http://eureka3.com:7003/eureka/
register-with-eureka: false #不是服务提供方,不需要注册
  1. RestTemplate远程访问工具的获取添加 @LoadBalanced
1
2
3
4
5
6
7
8
9
@Configuration
public class BeanConfiguration {

@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}

Ribbon是一套客户端的负载均衡工具浮出水面了:这里客户端clouddemo-consumer工程,负载均衡通过 @LoadBalanced

  1. ribbon集成在 eureka客户端,因此还需添加 @EnableEurekaClient
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package top.zhenganwen.clouddemoconsumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class ClouddemoConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(ClouddemoConsumerApplication.class, args);
}
}
  1. 将原先通过 localhost:8001直接访问服务端改为通过服务名到 eureka找服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package top.zhenganwen.clouddemoconsumer.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import top.zhenganwen.clouddemo.entity.Dept;

import java.util.List;

@RestController
@RequestMapping("dept")
public class DeptConsumerController {

// private static final String URL_PREFIX= "http://localhost:8001";
private static final String URL_PREFIX= "http://CLOUDDEMO-DEPT";

@Autowired
private RestTemplate restTemplate;

@PostMapping("add")
public Boolean add(Dept dept) {
return restTemplate.postForObject(URL_PREFIX + "/dept/add", dept, Boolean.class);
}
@GetMapping("list")
public List<Dept> list() {
return restTemplate.getForObject(URL_PREFIX + "/dept/list", List.class);
}

@GetMapping("get/{id}")
public Dept get(@PathVariable Long id) {
return restTemplate.getForObject(URL_PREFIX + "/dept/get/" + id, Dept.class);
}
}
  1. 启动三个 eureka,然后启动 clouddemo-provider,最后启动 clouddemo-consumer

  2. 测试 clouddemo-consumer

    1. localhost/dept/list:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    [
    {
    deptno: 1,
    dname: "人事部",
    db_source: "test"
    },
    {
    deptno: 2,
    dname: "技术部",
    db_source: "test"
    },
    {
    deptno: 3,
    dname: "公关部",
    db_source: "test"
    },
    {
    deptno: 4,
    dname: "销售部",
    db_source: "test"
    }
    ]

    查看控制台日志:

    1
    2
    3
    4
    5
    6
    2018-07-30 20:09:58.415  INFO 9064 --- [p-nio-80-exec-2] c.netflix.loadbalancer.BaseLoadBalancer  : Client: CLOUDDEMO-DEPT instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=CLOUDDEMO-DEPT,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
    2018-07-30 20:09:58.422 INFO 9064 --- [p-nio-80-exec-2] c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
    2018-07-30 20:09:58.441 INFO 9064 --- [p-nio-80-exec-2] c.netflix.config.ChainedDynamicProperty : Flipping property: CLOUDDEMO-DEPT.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
    2018-07-30 20:09:58.442 INFO 9064 --- [p-nio-80-exec-2] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client CLOUDDEMO-DEPT initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=CLOUDDEMO-DEPT,current list of Servers=[192.168.25.1:8001],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
    },Server stats: [[Server:192.168.25.1:8001; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
    ]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@6ff11bec

    从中可以捕获到负载均衡和服务路由痕迹

Ribbon和Eureka整合后Consumer可以直接调用服务而不用再关心地址和端口号


Ribbon负载均衡

架构图

Ribbon在工作时分成两步:

  • 第一步选择Eureka Server,它优选选择在同一个区域内负载较少的server.
  • 第二步再根据用户指定策略,从server取到的服务注册列表中选择一个地址.

其中Ribbon提供了多种策略:比如轮询,随机,根据响应时间加权.其中轮询是用户不指定策略时的默认执行策略。

我们将在已有架构基础之上再加两个服务:clouddemo-provider2clouddemo-provider3,通过 Ribbon使得 clouddemo-consumerCLOUDDEMO-DEPT服务的访问在 providerprovider2provider3三个服务实例之间做一个均衡:

实操

  1. 为新增的两个微服务创建各自独立的数据库 dept_db2dept_db3,体会每个微服务可以有自己独立的数据存储,也是为了后面负载均衡效果演示。

    1. dept_db2:
    1
    2
    3
    4
    5
    6
    7
    8
    DROP TABLE IF EXISTS `dept`;
    CREATE TABLE `dept` (
    `deptno` bigint(20) NOT NULL AUTO_INCREMENT,
    `dname` varchar(255) DEFAULT NULL,
    `db_source` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`deptno`)
    ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
    INSERT INTO `dept` VALUES ('1', '人事部', 'dept_db2');

    2.dept_db3

    1
    2
    3
    4
    5
    6
    7
    8
    DROP TABLE IF EXISTS `dept`;
    CREATE TABLE `dept` (
    `deptno` bigint(20) NOT NULL AUTO_INCREMENT,
    `dname` varchar(255) DEFAULT NULL,
    `db_source` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`deptno`)
    ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
    INSERT INTO `dept` VALUES ('1', '人事部', 'dept_db3');
  2. clouddemo-provider为模板创建clouddemo-provider2clouddemo-provider3两个子模块,以clouddemo-provider2为例:

    1. 复制 pom中的依赖
    2. 复制代码和配置文件
    3. 修改 application.yml
      1. 修改其中的端口号为 8002
      2. spring.application.name还是为 clouddemo-dept不要改变
      3. 修改数据源url为 jdbc:mysql://localhost:3306/dept_db2
      4. 修改该服务实例在 eureka上注册的id:instance-id: clouddemo-dept-8002

    clouddemo-provider3参照此步骤创建

  3. 依次启动 eureka集群、3个provider服务实例测试:

    1. 访问 eureka1.com:7001发现服务注册成功:

    1. 启动 clouddemo-consumer,访问 localhost/dept/get/1并刷新多次次,根据db_source属性值发现数据分别是轮询testdept_db2dept_db3数据库查出的数据:

      1. 第一次
      1
      { deptno: 1, dname: "人事部", db_source: "dept_db3" }
      1. 第二次
      1
      { deptno: 1, dname: "人事部", db_source: "dept_db2" }
      1. 第三次
      1
      { deptno: 1, dname: "人事部", db_source: "test" }
      1. 第四次
      1
      { deptno: 1, dname: "人事部", db_source: "dept_db3" }

现在可以发现 @LoadBalanced的作用了吧,这里的负载均衡策略我们没有提供,因此Ribbon使用了默认的轮询策略。

总结:Ribbon其实就是一个软负载均衡的客户端组件,
他可以和其他所需请求的客户端结合使用,和eureka结合只是其中一个实例


Ribbon核心组件IRule

IRule:根据特定算法中从服务列表中选取一个要访问的服务

官方提供

Ribbon为我们实现好的算法如下:

  • RoudRobinRule,轮询
  • RandomRule,随机
  • AvailabilityFilteringRule,会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问
  • WeightResponseTimeRule根据平均响应时间算所有服务的权重,响应时间越快服务权重越大被选中 的概率越高.刚启动时如果统计信息不足,则使用RoudRobinRule策略,等统计信息足够,会切换到WeightedResponseTimeRule
  • RetryRule,先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务
    • 例如,当 providerprovider2provider3正常运行时,采用轮询策略。此时如果 provider2宕机了, provider2还是会被轮询到(客户端会抛异常),但是在指定时间内发现多次重试均失败之后,会将 provider2从轮询列表中剔除。
  • BestAvailableRule,会过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
  • ZoneAvoidanceRule,默认规则,复合判断server所在区域的性能和server的可用性选择服务器

Spring Cloud Ribbon默认为我们装载了轮询策略,如果我们没有显示的声明,则采取轮询策略,如果需要更换成以上策略中的任意一个,则只需声明对应的 bean纳入Spring 管理即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.zhenganwen.clouddemoconsumer.cfg;

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;

@Configuration
public class BeanConfiguration {

@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}

@Bean
public IRule getRule(){
return RandomRule(); //调整策略为随机访问
}
}

自定义实现

特定的微服务特定的策略

上述切换成官方提供的策略只需配置对应的 bean,但是如果我只想对 CLOUDDEMO-DEPT这一个微服务指定特定的策略,而其他微服务采用默认策略该如何呢?

  1. 创建一个提供策略的配置类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package top.zhenganwen.rule;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyRule {

@Bean
public IRule getRule() {
return new RandomRule();
}
}
  1. 在启动类上添加 @RibbonClient
    1. value指定特定的服务
    2. configuration指定一个配置类,该类提供了对该特定服务使用的策略(此例中:对 CLOUDDEMO-DEPT服务使用随机访问策略)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package top.zhenganwen.clouddemoconsumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import top.zhenganwen.rule.MyRule;

@SpringBootApplication
@EnableEurekaClient
@RibbonClient(value = "CLOUDDEMO-DEPT",configuration = MyRule.class)
public class ClouddemoConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(ClouddemoConsumerApplication.class, args);
}
}

注意:官方警告,configuration指定的配置类(此例中是 MyRule)不能在 @ComponentScan所在包及其子包下。由于启动类的 @SpringBootApplication包含了该注解,因此上述的 MyRule没有在启动类所在包及其子包下。

最后别忘了访问 localhost/dept/get/1测试,观察 db_source属性值的变动规律

自定义Rule

需求:依旧使用轮询策略,但是要求每个服务器被调用5此后再轮询。即原来是一人一次过,改为一人5次过。

首先参考一下 RandomRule的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class RandomRule extends AbstractLoadBalancerRule {
Random rand = new Random();

public RandomRule() {
}

@SuppressWarnings({"RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE"})
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.rand.nextInt(serverCount);
server = (Server)upList.get(index);

if (server == null) {
Thread.yield();
} else {
if (server.isAlive()) {
return server;
}

server = null;
Thread.yield();
}
}

return server;
}
}

public Server choose(Object key) {
return this.choose(this.getLoadBalancer(), key);
}

public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}

可以看出 choose(ILoadBalancer lb, Object key)是定义策略的核心,随机算法选出服务实例只有两句:

1
2
int index = this.rand.nextInt(serverCount);
server = (Server)upList.get(index);

因此我们只需参考 RandomRule,修改其中算法部分即可写出定义的 每五次轮询策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package top.zhenganwen.rule;

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;

public class MyPollRule extends AbstractLoadBalancerRule {

private int total; //记录同一个实例被连续访问的词数
private int currentIndex; //记录当前被轮询的实例的索引

public MyPollRule() {
total = 0;
currentIndex = 0;
}

@SuppressWarnings({"RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE"})
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.rand.nextInt(serverCount);
// server = (Server)upList.get(index);

//如果连续访问没有超过5次,则返回当前正在轮询的服务实例,并将连续访问次数递增
if (total < 5) {
server = upList.get(currentIndex);
total++;
}else {
//如果连续访问达到5次,则索引递增,访问次数归零
currentIndex++;
total = 0;
//如果当前轮询服务实例是列表中的最后一个,则将索引归零
if (currentIndex >= serverCount) {
currentIndex = 0;
}
//这里server还是为null,因此进入while循环取下一个服务实例重新对访问次数计数
}

if (server == null) {
Thread.yield();
} else {
if (server.isAlive()) {
return server;
}

server = null;
Thread.yield();
}
}

return server;
}
}

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

@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}

最后我们需要注册该 bean以启用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package top.zhenganwen.rule;

import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyRule {

@Bean
public IRule getRule() {
return new MyPollRule();
}
}

测试 localhost/dept/get/1,连续访问查看策略是否生效(观察 db_source属性值的变化)

鼓励一下~