SpringCloud 详解(六)

发布于 2022-04-16  133 次阅读


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 的可用性选择服务器。

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

自定义负载均衡器

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);
    }
}

修改配置类

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,会发现端口已经不再是互相切换,而是随机的。

默认的轮询策略原理

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

//源码
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;
}

手动实现轮询策略

注释掉原先的策略

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();
    }
}

实现轮询策略

package ml.guest997.config;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;

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

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

添加订单 Controller

@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 模块下的控制台也会输出当前的访问次数。