二、SpringCloud服务调用

CY 2019年03月17日 494次浏览

Ribbon

Ribbon用来做负载均衡和服务调用,网上的教程很多:

【基于配置文件】Spring Cloud Ribbon:负载均衡的服务调用 这篇文章使用了配置文件的方式来配置了负载均衡策略,以及介绍了几种com.netflix.loadbalancer.IRule的实现类

【基于编码】Ribbon配置负载均衡策略 这篇文章使用了编程的方式和配置文件的方式来配置了负载均衡策略

总结一下这两篇文章(其实为了防止链接无法使用)

使用配置文件的方式

RestTemplate就不多介绍了,里面的几种方法可以看API来自行理解。

上篇文章中说到的注解@LoadBalanced其实就已经使用到了Ribbon做负载均衡,因为上篇文章中导入的注册中心客户端默认就导入了Ribbon

默认导入了Ribbon

所以使用Ribbon的时候就不需要再刻意去导入Ribbon的相关依赖了。

全局配置Ribbon

ribbon:
  # 服务请求连接超时时间(毫秒)
  ConnectTimeout: 1000
  # 服务请求处理超时时间(毫秒)
  ReadTimeout: 3000
  # 对超时请求启用重试机制
  OkToRetryOnAllOperations: true
  # 切换重试实例的最大个数
  MaxAutoRetriesNextServer: 1
  # 切换实例后重试最大次数
  MaxAutoRetries: 1
  # 修改负载均衡算法
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

这些超时时间后面再做具体的介绍

局部配置Ribbon

局部的意思是,给某个微服务配置它专属的负载均衡算法,针对于Consumer访问多个Provider的情况。

配置方式和上面基本一样:

cloud-provider-paymanet:
  ribbon:
    # 省略配置,和上面一样
    ...

默认拥有的负载均衡算法

com.netflix.loadbalancer.RandomRule:从提供服务的实例中以随机的方式;

com.netflix.loadbalancer.RoundRobinRule:以线性轮询的方式,就是维护一个计数器,从提供服务的实例中按顺序选取,第一次选第一个,第二次选第二个,以此类推,到最后一个以后再从头来过;

com.netflix.loadbalancer.RetryRule:在RoundRobinRule的基础上添加重试机制,即在指定的重试时间内,反复使用线性轮询策略来选择可用实例;

com.netflix.loadbalancer.WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择;

com.netflix.loadbalancer.BestAvailableRule:选择并发较小的实例;

com.netflix.loadbalancer.AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例;

com.netflix.loadbalancer.ZoneAwareLoadBalancer:采用双重过滤,同时过滤不是同一区域的实例和故障实例,选择并发较小的实例。

使用编码的方式

局部配置Ribbon

编写配置文件,不能写在启动类包中,也不能写在子包中,确保不能被启动类扫描到,否则就变成了全局配置了。

负载均衡配置包的位置

@Configuration
public class CustomLoadBalance {
    @Bean
    public IRule random() {
        return new RandomRule();
    }
}

然后需要在启动类上面引用一下这个配置:

@SpringBootApplication
@EnableDiscoveryClient
@RibbonClients({
        @RibbonClient(name = "cloud-provider-paymanet", configuration = CustomLoadBalance.class)
})
public class OrderApplication {

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

可以看到上面用到了@RibbonClient注解,这个注解规定cloud-provider-paymanet微服务使用CustomLoadBalance负载均衡配置

配置代码在启动类的包或其子包下

如果不想要专门建立一个包去做负载均衡的配置,就要在启动类的子包下去做负载均衡配置,那么可以在@ComponentScan上做些手脚

@SpringBootApplication
@EnableDiscoveryClient
@RibbonClients({
        @RibbonClient(name = "cloud-provider-paymanet", configuration = CustomLoadBalance.class)
})
@ComponentScan(excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION, value = {ExcludeAnnotation.class})
})
public class OrderApplication {

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

上面使用了excludeFilters来规定了扫描排除的规则,只要是使用了ExcludeAnnotation注解的类都会被扫描的时候排除掉

@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.TYPE)
public @interface ExcludeAnnotation {
}

这样也就可以放心的写负载均衡配置类了。

不使用注册中心的Ribbon

有时候特别小的项目都不需要使用注册中心,所以可以在配置文件中写死服务地址:

cloud-provider-paymanet:
  ribbon:
    listOfServers: localhost:8001

多个地址之间用逗号,分隔。

负载均衡算法的原理

以下内容来源于尚硅谷周阳老师的讲解。

com.netflix.loadbalancer.RoundRobinRule这个类涉及到了一个自旋锁的使用,研究了很长时间,其实目的就是防止过快的改变AtomicInteger中的值,才使用了自旋锁,如果发现值被改变,也就是compareAndSet方法返回了false,那么就循环再来一次改变,这样就可以避免高并发情况下轮询算法失误的问题,也就是轮询算法因为高并发压力,导致好几次都负载到同一台机器上。

认真分析以下源码:

public class RoundRobinRule extends AbstractLoadBalancerRule {

    // 用来控制下一次轮询下标的一个原子Integer
    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() {
        // 构造初始化AtomicInteger
        nextServerCyclicCounter = new AtomicInteger(0);
    }

    public RoundRobinRule(ILoadBalancer lb) {
        this();
        setLoadBalancer(lb);
    }

    public Server choose(ILoadBalancer lb, Object key) {
        // 如果lb为空,意味着没有办法做负载均衡
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }

        Server server = null;
        int count = 0;
        // 这里最多循环10次,也就是如果没有获取可用服务成功的话,会最多尝试10次
        while (server == null && count++ < 10) {
            // 获取所有的可用服务
            List<Server> reachableServers = lb.getReachableServers();
            // 获取所有的服务
            List<Server> allServers = lb.getAllServers();
            int upCount = reachableServers.size();
            int serverCount = allServers.size();

            // 如果可用的服务和所有的服务都是0,那么就警告一下,并且负载均衡失败
            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }

            // 这里是一个公式 当前访问计数 % 总服务数量 = 服务的下标
            // 也就是轮询算法的基本原理
            int nextServerIndex = incrementAndGetModulo(serverCount);
            // 获取轮询下标对应的服务
            server = allServers.get(nextServerIndex);

            // 如果没有获取到服务,当前线程就礼让一下,让别的线程执行
            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }

            // 如果服务存活,并且服务准备就绪,意味着服务可用,就负载到当前的服务上
            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }

            // Next.
            // 把服务重置为null,期待下一次的执行,重新选服务。
            server = null;
        }

        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

    /**
     * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
     *
     * @param modulo The modulo to bound the value of the counter.
     * @return The next value.
     */
    private int incrementAndGetModulo(int modulo) {
        // 这里用到了自旋锁,目的就是防止高并发情况下抢着累加计数,导致多次负载到同一个服务上
        for (;;) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }

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

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

如果要自定义自己的负载均衡算法,那么只需要像源码一样实现IRule接口就可以了,下面来通过代码演示一下轮询算法的基本原理,以及负载均衡的基本工作方式。不用官方的IRule接口,而是自己去模拟最核心的流程。

手动实现一个负载均衡

原理其实已经明白了,使用负载均衡算法,从可用的服务里面选出一个服务,然后通过RestTemplate去访问这个服务就可以了。

在注册中心那一章节中,认识了一个类叫做DiscoveryClient,这个类中就可以列举出所有可用的微服务。

在手动实现之前,请一定要注释掉@LoadBalanced注解,这样才能真正的看到效果。

实现自己的负载均衡策略:

/**
 * 自定义负载均衡算法
 */
@Component
public class CustomLoadBalance {

    @Resource
    private DiscoveryClient discoveryClient;

    private AtomicInteger nextCounter = new AtomicInteger(0);

    public URI loadBalance() {

        // 获取所有的微服务实例
        List<ServiceInstance> instances = discoveryClient.getInstances("cloud-provider-paymanet");

        // 当前的值
        int current;
        // 下一次的值
        int next;
        // 自旋锁来进行赋值
        do {
            current = nextCounter.get();
            next = current + 1;
        } while (!nextCounter.compareAndSet(current, next));

        // 每一次累加后 % 实例数量 = 当前使用实例下标
        return instances.get(next % instances.size()).getUri();
    }
}

使用了最简单的代码写出了核心的原理,使用的时候需要这样:

@Resource
private CustomLoadBalance customLoadBalance;

@GetMapping("payLoad")
public String payLoad() {
    URI uri = customLoadBalance.loadBalance();
    return restTemplate.getForObject(uri + "/payInfo", String.class);
}

然后启动服务,访问浏览器,可以看到之前两个微服务80018002轮询切换

OpenFeign

Feign已经停止了更新,直接使用OpenFeign就好了。

OpenFeign其实可以和DubboService一样的理解,就是封装了RibbonRestTemplateDubbo中两个服务共用一个接口就可以实现接口的RPC调用,OpenFeign和这个原理都是一样的。

OpenFeign微服务的调用

既然是微服务的调用,那么一定是Consumer方的操作

一样的步骤,首先要导入Maven坐标:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

接下来需要配置Feign要到哪个注册中心去寻找服务(以Consul为例):

spring:
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        # 就不要把Consumer注册到注册中心了
        register: false

紧接着需要在启动类上添加一个注解来启用OpenFeign的扫描

@SpringBootApplication
// 启用OpenFeign的扫描功能
@EnableFeignClients
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

@EnableFeignClients注解可以扫描所有带有@FeignClient注解的接口

接下来就要写一个带有@FeignClient注解的接口:

// 注解中的value指定了一个微服务的名字
@FeignClient("cloud-provider-paymanet")
public interface OpenFeignService {
    // 使用get方法访问目标微服务中的payInfo接口
    @GetMapping("payInfo")
    String payInfo();
}

然后在controller中使用这个接口:

@Resource
private OpenFeignService openFeignService;

@GetMapping("feign")
public String feign() {
    return openFeignService.payInfo();
}

到这里,启动这个服务,就可以在访问http://localhost/feign了。

可以看到编码的样子和DubboService非常的类似。

OpenFeign超时设置

先看一个问题然后再理解这个超时设置:

Provider方提供一个执行时间比较长的方法:

@GetMapping("payTimeout")
public String payTimeout() {
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "返回了一个信息!!!" + serverPort + " --> " + UUID.randomUUID().toString();
}

可以看到这个方法中延迟了3秒钟。

然后再去Consumer方调用这个服务,发现页面上就会报错了:

OpenFeign超时

这个错误是OpenFeign读取服务超时了,OpenFeign默认为1秒超时,所以才会报这个错。

可以通过配置来避免这样的错误。

因为OpenFeign也是使用了Ribbon,其实配置也就是配置的是Ribbon

ribbon:
  ReadTimeout: 5000
  ConnectTimeout: 5000

这样之后就不会报错了。

OpenFeign日志打印

OpenFeign有四种日志类型

NONE,无记录(DEFAULT)。
BASIC,只记录请求方法和URL以及响应状态代码和执行时间。
HEADERS,记录基本信息以及请求和响应标头。
FULL,记录请求和响应的头文件,正文和元数据。

首先需要一个配置类,来配置OpenFeign的日志级别:

@Configuration
public class FeignConfig {
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

这个配置类把OpenFeign的日志级别设置为了FULL,就会显示很详细的日志。

但是光这样是不够的,需要设置SpringBoot的日志打印级别:

logging:
  level:
    com.cy.consumer.service.OpenFeignService: debug

然后启动Consumer访问接口,就可以看到这些日志打印了。

摘录一下日志:

2020-03-07 13:17:09.865 DEBUG 26876 --- [p-nio-80-exec-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#payInfo] ---> GET http://cloud-provider-paymanet/payInfo HTTP/1.1
2020-03-07 13:17:09.866 DEBUG 26876 --- [p-nio-80-exec-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#payInfo] ---> END HTTP (0-byte body)
2020-03-07 13:17:09.873 DEBUG 26876 --- [p-nio-80-exec-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#payInfo] <--- HTTP/1.1 200 (6ms)
2020-03-07 13:17:09.873 DEBUG 26876 --- [p-nio-80-exec-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#payInfo] connection: keep-alive
2020-03-07 13:17:09.873 DEBUG 26876 --- [p-nio-80-exec-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#payInfo] content-length: 34
2020-03-07 13:17:09.873 DEBUG 26876 --- [p-nio-80-exec-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#payInfo] content-type: text/plain;charset=UTF-8
2020-03-07 13:17:09.874 DEBUG 26876 --- [p-nio-80-exec-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#payInfo] date: Sat, 07 Mar 2020 05:17:09 GMT
2020-03-07 13:17:09.874 DEBUG 26876 --- [p-nio-80-exec-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#payInfo] keep-alive: timeout=60
2020-03-07 13:17:09.874 DEBUG 26876 --- [p-nio-80-exec-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#payInfo] 
2020-03-07 13:17:09.874 DEBUG 26876 --- [p-nio-80-exec-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#payInfo] 返回了一个信息!!!8002
2020-03-07 13:17:09.874 DEBUG 26876 --- [p-nio-80-exec-4] c.cy.consumer.service.OpenFeignService   : [OpenFeignService#payInfo] <--- END HTTP (34-byte body)