第一章 Ribbon介绍

1.1、什么是Ribbon

Ribbon 是一个基于 HTTP 和 TCP 的客服端负载均衡工具,它是基于 Netflix Ribbon 实现的。它不像 Spring Cloud 服务注册中心、配置中心、API 网关那样独立部署,但是它几乎存在于每个 Spring Cloud 微服务中。包括 Feign 提供的声明式服务调用也是基于该 Ribbon 实现的。

Ribbon 默认提供很多种负载均衡算法,例如轮询、随机等等,甚至包含自定义的负载均衡算法。

1.2、为啥用Ribbon

Ribbon 提供了一套微服务的负载均衡解决方案。目前业界主流的负载均衡方案可分成两类:

· 集中式负载均衡(服务器负载均衡),即在 consumer 和 provider 之间使用独立的负载均衡设施(可以是硬件,如 F5,也可以是软件,如 nginx),由该设施负责把访问请求通过某种策略转发至 provider;

· 进程内负载均衡(客户端负载均衡),将负载均衡逻辑集成到 consumer,consumer 从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的 provider。Ribbon 属于后者,它只是一个类库,集成于 consumer 进程,consumer 通过它来获取 provider 的地址。

第二章 Ribbon入门案例

2.1、项目准备

我们接下来的所有操作均是在Eureka最后完成的工程上进行操作,相关代码请到配套资料中寻找。

2.2、添加服务提供者

既然我们要做服务的负载均衡,那肯定需要有很多服务,一个服务再怎么负载均衡还是他自己,因此,我们需要在原工程的基础上在添加一个服务提供者,配置基本和service-provider8001差不多,只不过端口不一样而已,接下来会给出具体不一样的配置。

(1)修改启动类为ServiceProvider8002Application

@EnableEurekaClient

@SpringBootApplication

public class ServiceProvider8002Application {

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

}

(2)修改配置文件application.yaml

server:

 port: 8002

spring:

 application:
 #该名称在集群模式下应该保持一致
 name: `service`-`provider`

度量指标监控与健康检查

management:

 endpoints:
 web:
 exposure:
 #开启 shutdown 端点访问
 include: `shutdown`
 endpoint:
 #开启 shutdown 实现优雅停服
 shutdown:
 enabled: true

eureka:

 instance:
 #是否使用 ip 地址注册
 prefer-ip-address: true
 #该实例注册到服务中心的唯一ID
 instance-id: `$`{`spring.cloud.client.ip`-`address`}:`$`{`server.port`}
 client:
 #设置服务注册中心地址
 service-url:
 defaultZone: `http`:`//root`:`123456@eureka`-`server7001.com`:`7001/eureka/`,`http`:`//root`:`123456@eureka`-`server7002.com`:`7002/eureka/`

(3)分别修改service-provider8001service-provider8002的控制层ProductController 代码,都加入一段新的控制器方法,代码如下:

service-provider8001的ProductController

@RequestMapping("/provider/product/findByPid")

public String findByPid(@RequestParam("pid") Integer pid) {

 return "8001 findByPid ...";

}

service-provider8002的ProductController

@RequestMapping("/provider/product/findByPid")

public String findByPid(@RequestParam("pid") Integer pid) {

 return "8002 findByPid ...";

}

(4)启动注册中心

  1. 启动eureka-server7001
  2. 启动eureka-server7002

(5)启动服务提供者

  1. 启动service-provider8001
  2. 启动service-provider8002

(6)查看服务是否注册到注册中心,登录账户为root,登录密码为123456

在浏览器打开地址

2.3、修改服务消费者

2.3.1、方式一:LoadBalancerClient

(1)修改service-consumer9001ProductController控制器代码,在里边新增以下代码

// Ribbon 负载均衡器

// import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;

@Autowired

private LoadBalancerClient loadBalancerClient;

@RequestMapping("/consumer/product/findByPid")

public String findByPid(@RequestParam("pid") Integer pid) {

 //从注册中心获取一个SERVICE-PROVIDER服务的实例对象
 `ServiceInstance si` = `loadBalancerClient`.choose("SERVICE-PROVIDER");
 if (`si` == `null`) {
 return `null`;
 }
 `String findByPidUrl` = `si`.getUri() + "/provider/product/findByPid" + "?pid=" + `pid`;
 `String product` = `restTemplate`.getForObject(`findByPidUrl`, `String`.class);
 return `product`;
 //这里只是做模拟返回了一个字符串,当然你也可以返回一个商品对象,这取决于你

}

(2)启动service-consumer9001,然后打开浏览器输入地址访问

2.3.2、方式二:@LoadBalanced

(1)修改service-consumer9001ServiceConsumer9001Application启动类代码,在里边新增以下代码

@EnableEurekaClient

@SpringBootApplication

public class ServiceConsumer9001Application {

 public static void main(`String`[] `args`) {
 `SpringApplication`.run(`ServiceConsumer9001Application`.class);
 }
 @Bean
 @LoadBalanced // Ribbbon 负载均衡注解,加上这个注解,restTemplate默认就有负载均衡的能力了
 public `RestTemplate` restTemplate(){
 return new RestTemplate();
 }

}

(2)修改service-consumer9001ProductController控制器代码,在里边新增以下代码

@RequestMapping("/consumer/product/findByPid2")

public String findByPid2(@RequestParam("pid") Integer pid) {

 `String findByPidUrl` = "http://SERVICE-PROVIDER/provider/product/findByPid" + "?pid=" + `pid`;
 `String product` = `restTemplate`.getForObject(`findByPidUrl`, `String`.class);
 `System`.`out`.println(`product`);
 return `product`;

}

(3)重启service-consumer9001,然后打开浏览器输入地址访问

第三章 Ribbon负载均衡策略

3.1、轮询策略(默认)

策略对应类名:RoundRobinRule

实现原理:轮询策略表示每次都顺序取下一个 provider,比如一共有 5 个 provider,第 1 次取第 1 个,第 2 次取第 2 个,第 3 次取第 3 个,以此类推。

3.2、权重轮询策略

策略对应类名:WeightedResponseTimeRule

实现原理:

· 根据每个 provider 的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性越低。

· 原理:一开始为轮询策略,并开启一个计时器,每 30 秒收集一次每个 provider 的平均响应时间,当信息足够时,给每个 provider 附上一个权重,并按权重随机选择 provider,高权越重的 provider 会被高概率选中。

3.3、随机策略

策略对应类名:RandomRule

实现原理:从 provider 列表中随机选择一个。

3.4、最少并发数策略

策略对应类名:BestAvailableRule

实现原理:选择正在请求中的并发数最小的 provider,除非这个 provider 在熔断中。

3.5、重试策略

策略对应类名:RetryRule

实现原理:其实就是轮询策略的增强版,轮询策略服务不可用时不做处理,重试策略服务不可用时会重新尝试集群中的其他节点。

3.6、可用性敏感策略

策略对应类名:AvailabilityFilteringRule

实现原理:过滤性能差的 provider

· 第一种:过滤掉在 Eureka 中处于一直连接失败的 provider。

· 第二种:过滤掉高并发(繁忙)的 provider。

3.7、区域敏感性策略

策略对应类名:ZoneAvoidanceRule

实现原理:

· 以一个区域为单位考察可用性,对于不可用的区域整个丢弃,从剩下区域中选可用的 provider。

· 如果这个 ip 区域内有一个或多个实例不可达或响应变慢,都会降低该 ip 区域内其他 ip 被选中的权重。

第四章 Ribbon负载均衡设置

4.1、全局替换

在启动类或配置类中注入负载均衡策略对象,所有服务请求均使用该策略。

@EnableEurekaClient

@SpringBootApplication

public class ServiceConsumer9001Application {

 public static void main(`String`[] `args`) {
 `SpringApplication`.run(`ServiceConsumer9001Application`.class);
 }
 @Bean
 @LoadBalanced // Ribbbon 负载均衡注解,加上这个注解,restTemplate默认就有负载均衡的能力了
 public `RestTemplate` restTemplate(){
 return new RestTemplate();
 }
 @Bean
 public `RandomRule` randomRule() {
 return new RandomRule();
 }

}

重启消费者然后多次访问:http://localhost:9001/consumer/product/findByPid2?pid=1,结果如下:

4.2、局部替换

请注释掉刚才的全局设置,避免影响结果,修改配置文件指定服务的负载均衡策略。格式:服务应用名`.ribbon.NFLoadBalancerRuleClassName`

负载均衡策略

service-provider 为调用的服务的名称

service-provider:

 ribbon:
 NFLoadBalancerRuleClassName: `com.netflix.loadbalancer.RandomRule`

重启消费者然后多次访问:结果如下:

第五章 Ribbon手写负载均衡

5.1、负载均衡算法

5.2、轮询策略源码

核心:自旋锁,private int incrementAndGetModulo(int modulo)方法用到了自旋锁

定义:
自旋锁(spin lock)是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。线程反复检查域名交易锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。

作用:
自旋锁避免了进程上下文的调度开销。线程一直处于用户态,减少了用户态到内核态的开销与损耗(减少了上下文切换)。

适用场景:
1、多线程
2、使用者占有锁的时间短

轮询源码:

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`) {
 }

}

5.3、手写负载均衡

(1)创建一个插件包名字叫:com.caochenlei.plugin

(2)在这个插件包中新建类:MyLoadBalanced

(3)手写负载均衡器来替代默认的负载均衡器注解@LoadBalanced,源码如下:

@Component

public class MyLoadBalanced {

 private `AtomicInteger atomicInteger` = new AtomicInteger(0);
 //获取服务提供者实例下标
 public `ServiceInstance` instances(`List`<ServiceInstance> `serviceInstances`) {
 int `index` = getAndIncrement() % `serviceInstances`.size();
 return `serviceInstances`.get(`index`);
 }
 //使用自旋锁进行累加操作
 private final int getAndIncrement() {
 int `current`;
 int `next`;
 do {
 `current` = this.`atomicInteger`.get();
 `next` = `current` >= `Integer`.`MAX_VALUE` ? 0 : `current` + 1;
 //第一个参数是期望值,第二个参数是修改值
 } while (!this.`atomicInteger`.compareAndSet(`current`, `next`));
 `System`.`out`.println("访问次数:" + `next`);
 return `next`;
 }

}

(4)在控制器类ProductController中新增如下测试代码:

@Autowired

private MyLoadBalanced loadBalanced;

@RequestMapping("/consumer/product/findByPid3")

public String findByPid3(@RequestParam("pid") Integer pid) {

 //获取注册中心服务列表
 `List`<ServiceInstance> `instances` = `discoveryClient`.getInstances("SERVICE-PROVIDER");
 if (`CollectionUtils`.isEmpty(`instances`)) {
 return `null`;
 }
 //使用自定义负载均衡器
 `ServiceInstance si` = `loadBalanced`.instances(`instances`);
 `String findByPidUrl` = `si`.getUri() + "/provider/product/findByPid" + "?pid=" + `pid`;
 `String product` = `restTemplate`.getForObject(`findByPidUrl`, `String`.class);
 `System`.`out`.println(`product`);
 return `product`;

}

(5)注释配置文件中配置的随机策略

负载均衡策略

SERVICE-PROVIDER 为调用的服务的名称

SERVICE-PROVIDER:

ribbon:

NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

(6)注释Ribbon默认的负载均衡器注解

@EnableEurekaClient

@SpringBootApplication

public class ServiceConsumer9001Application {

 public static void main(`String`[] `args`) {
 `SpringApplication`.run(`ServiceConsumer9001Application`.class);
 }
 @Bean
 //@LoadBalanced // Ribbbon 负载均衡注解,加上这个注解,restTemplate默认就有负载均衡的能力了
 public `RestTemplate` restTemplate(){
 return new RestTemplate();
 }

// @Bean

// public RandomRule randomRule() {

// return new RandomRule();

// }

}


云中寄锦书
12 声望0 粉丝

引用和评论

0 条评论