SpringCloud 详解(六)

Ribbon

基于 Netflix Ribbon 实现的一套客户端负载均衡的工具。负载均衡(Load Balance - LB)就是将用户请求通过特定策略分配到多个服务上,从而达到系统的高可用(HA)。

与 Nginx 的不同之处

Nginx 是服务器负载均衡,客户端所有请求都会交给 Nginx,然后由 Nginx 实现请求转发。即负载均衡是由服务端实现的。
Ribbon 则是本地负载均衡,在调用微服务接口时,会在注册中心上获取注册信息服务列表,然后缓存到本地 JVM,从而在本地实现远程服务调用。

集中式 LB

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

进程内 LB

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

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

RestTemplate 请求方法

  • getForObject() / getForEntity():GET 请求
  • postForObject() / postForEntity():POST 请求

ForObject 和 ForEntity 的区别在于

  • ForObject():返回对象为响应体中数据转化成的对象,可以理解为 json。
  • ForEntity():返回对象为 ResponseEntity 对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。

自带的负载策略

  • RoundRobinRule:轮询
  • RandomRule:随机
  • RetryRule:先按照轮询的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务。
  • WeightedResponseTimeRule:对轮询的扩展,响应速度越快的实例选择权重越大,越容易被选择。
  • BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。
  • AvailabilityFilteringRule:先过滤掉故障实例,再选择并发量较小的实例。
  • ZoneAvoidanceRule:复合判断 server 所在区域的性能和 server 的可用性选择服务器。

负载策略替换(订单模块)

自定义负载均衡器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package ml.guest997.config;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

public class CustomLoadBalancerConfiguration {
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String property = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
//返回随机负载均衡器
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(property, ServiceInstanceListSupplier.class), property);
}
}

修改配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package ml.guest997.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
@LoadBalancerClient(name = "PAYMENT",configuration = CustomLoadBalancerConfiguration.class) //配置自定义负载均衡器
public class ApplicationContextConfig {
@Bean
@LoadBalanced //配置负载均衡
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}

测试

分别启动 Eureka-Server、Payment、Payment2 和 Order 模块后,浏览器多次访问:127.0.0.1/consumer/payment/add?serial=Guest005,会发现端口已经不再是互相切换,而是随机的。

默认的轮询策略原理

接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启后接口计数从头开始

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
//源码
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) { //没有实例返回空响应对象
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + serviceId);
}
return new EmptyResponse();
}
// TODO: enforce order?
int pos = Math.abs(this.position.incrementAndGet()); //参数表示的是 position + 1,获取绝对值就是接口第几次请求数。incrementAndGet 方法的具体实现在下面。
ServiceInstance instance = instances.get(pos % instances.size()); //就是通过上面的公式获取实例下标
return new DefaultResponse(instance);
}

//主要是为了确保并发安全
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2); //这个方法是系统实现的
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); //这个方法也是系统实现的
return var5;
}

手动实现轮询策略

注释掉原先的策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package ml.guest997.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
//@LoadBalancerClient(name = "PAYMENT",configuration = CustomLoadBalancerConfiguration.class)
public class ApplicationContextConfig {
@Bean
// @LoadBalanced //配置负载均衡
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}

实现轮询策略

1
2
3
4
5
6
7
8
9
package ml.guest997.config;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;

public interface LoadBalancer {
ServiceInstance getInstance(List<ServiceInstance> serviceInstances);
}
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
package ml.guest997.config;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class MyLB implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);

public final int getAndIncrement() {
int before, current;
do {
before = atomicInteger.get(); //获取之前的访问次数,如果是第一次进行访问,就是0
current = before >= 2147483647 ? 0 : before + 1; //这个数字是 Integer 的最大值,如果将要超出这个数值就重置为0,否则+1
} while (!atomicInteger.compareAndSet(before, current)); //为了并发安全,调用这个方法。将 before 在内存中偏移量为 x 位置的值与期望值 current 作比较,相等就把 current 赋值给偏移量为 x 位置的值并返回 true
System.out.println("当前是第几次访问:" + current);
return current;
}

@Override
public ServiceInstance getInstance(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size(); //取余得到下标
return serviceInstances.get(index);
}
}

添加支付 Controller

1
2
3
4
@GetMapping(value = "/payment/lb")
public String getPaymentLB() {
return serverPort;
}

添加订单 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Resource
private LoadBalancer loadBalancer;
@Resource
private DiscoveryClient discoveryClient;

@GetMapping(value = "/consumer/payment/lb")
public String getPaymentLB() {
List<ServiceInstance> instances = discoveryClient.getInstances("PAYMENT"); //获取 id 为 PAYMENT 的全部服务实例
if (instances == null instances.size() <= 0) {
return null;
}
ServiceInstance serviceInstance = loadBalancer.getInstance(instances); //通过调用自定义的负载均衡器方法获取具体的服务实例对象
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}

测试

分别启动 Eureka-Server、Payment、Payment2 和 Order 模块后,浏览器多次访问:127.0.0.1/consumer/payment/lb,会发现端口又变回互相切换的了。

Order 模块下的控制台也会输出当前的访问次数。