SpringCloud与微服务-第5章-服务容错(Hystrix)

Spring Cloud Hystrix断路器快速入门


在微服务架构中,我们将系统拆分成了很多服务单元,各单元的应用间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同
的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题
会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会因等待出现故障的依赖方响应形成任务积压,最
终导致自身服务的瘫痪。


目标:

在本章中,您将学习:

  • 如何创建请求命令
  • 如何进行异常处理
  • 如何实现 turbine 集群监控
  • 原理分析
  • 断路器原理
  • 依赖隔离
  • 使用详解
  • 属性详解
  • 监控面板

服务雪崩

微服务架构下,会存在服务之间相互依赖调用的情况,当某个服务不可用时,很容易因为服务之间的依赖关系使故障扩大,甚至造成整个系统不可用的情况,这种现象称为服务雪崩效应


w:36em


  • 如上图所示,为服务雪崩效应发生的过程,首先是服务正常状态,当客户端对服务 A 发起请求,服务 A 依赖了服务 B,服务 B 又依赖了服务 C,当所有服务都处于正常状态时,整个请求链路是通畅的,结果会很快返回给客户端。

  • 如果这时服务 C 发生故障或出现性能问题,就会出现延迟,刚开始时延迟较小,随着时间的推移,延迟会越来越大,服务 B 对服务 C 的调用就会堵塞,服务 C 此时已经疲惫不堪。

  • 由于请求都堵在服务 C 上,服务 B 作为调用方,却迟迟等不到服务 C 的结果,服务 A 对服务 B 的请求又源源不断的发送过来,最终导致服务 B 的资源耗尽,从正常状态变成不正常状态,再也无法及时响应服务 A 的请求结果。

  • 依此类推,最终服务 A 也会被拖垮,导致整个系统不可用,这个过程就是服务雪崩效应。如果能从最开始的小问题进行预防,就不会出现后面的级联效果,本章我们将讨论如何通过对服务进行容错降级来保证系统的可用性。


比如在一个电商网站中,我们可能会将系统拆分成用户、订单、库存、积分、 评论等一系列服务单元。用户创建一个订单的时候,客户端将调用订单服务的创建订单接口,此时创建订单接口又会向库存服务来请求出货(判断是否有足够库存来出货)。此时若库存服务因自身处理逻辑等原因造成响应缓慢,会直接导致创建订单服务的线程被挂起,以等待库存申请服务的响应,在漫长的等待之后用户会因为请求库存失败而得到创建订单失败的结果。如果在高并发情况之下,因这些挂起的线程在等待库存服务的响应而未能释放,使得后续到来的创建订单请求被阻塞,最终导致订单服务也不可用。


服务雪崩产生原因

服务雪崩产生的原因肯定是服务提供者出了问题才导致后面的雪崩问题,在实际应用中无法预料服务提供者可能会出现什么样的问题,我们只能分析一些比较常见的问题。

  • 服务提供者方面
    由于某些代码问题导致 CPU 飙升,将资源耗尽等,比如服务器出现问题,磁盘出问题,导致数据读写特别慢,还比如说出现慢 SQL, 亦或请求量太大了已经超出了系统本身的承受能力, 又或者出现死循环, 死锁, 线程池配置等问题导致服务响应时间过长或报错。

  • 服务消费者这方面
    比如同步调用等待结果导致资源耗尽,另外一些服务消费者可能同时也是服务提供者。


服务雪崩的解决方案

既然分析了一些比较常见的会导致服务雪崩的问题,那么就需要出对应的策略来解决这些问题。当然对于代码的 Bug 问题,我们可以通过测试、Code Review 等方式来避免,对于慢 SQL 这种问题,我们需要去做数据库性能优化。对于服务器硬件故障问题,我们可以加大运维粒度,通过监控等手段来提前预防。

而对于服务提供者方面,对于这种请求量超出承受能力的问题,我们可以进行扩容来支持高并发或者进行限流,自己能处理多少请求就处理多少,处理不了的请求直接拒绝,这样才不会将自己拖垮。服务消费者方面,我们需要做的就是资源隔离,快速失败,这也是最有效的方式,当我们发现被调用方迟迟不响应出现问题的时候,就不要再继续发起调用请求了,此时应该停止并返回一个友好的响应(服务降级),等待被调用方恢复后再发起调用。


Hystrix 快速入门

类似实现限流、熔断以及降级等功能如果需要我们自己来实现的话,可能需要在每个调用的地方都要做一些逻辑处理并判断要不要发起调用,如果这样就太麻烦了。

好在 Spring Cloud Hystrix 就是专门处理此类场景问题的:核心功能有服务降级、服务熔断、服务限流、服务隔离、请求缓存、请求合并等。

断路器模式源于 Martin Fowler 的 CircuitBreaker 一文。“断路器”本身是一种开关装置, 用于在电路上保护线路过载,当线路中有电器发生短路时,“断路器”能够及时切断故障电 路,防止发生过载、发热甚至起火等严重后果。


在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待,这样就不会使得线程因调用故障服务而被长时间占用不释放,避免了故障在分布式系统中的蔓延。


Spring Cloud Netflix Hystrix 实现了断路器,线程隔离等一系列服务保护功能。该框架的目的在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix 具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等的强大功能。


  • 服务降级:当某个服务单元发生故障之后,通过断路器的故障监控,向调用方返回一个错误响应,而不是长时间的等待,这样就不会使得线程因调用故障服务而被长时间占用不释放,避免了故障在分布式系统中的蔓延。

    • 服务熔断: 是指调用方访问服务时通过断路器做代理进行访问,断路器会持续观察服务返回的成功、失败的状态,当失败超过设置的阈值时断路器打开,请求就不能真正地访问到服务了。
  • 线程和信号隔离:通过线程池的方式来为依赖服务调用提供隔离,避免因依赖服务故障导致的线程阻塞,从而导致整个系统的线程资源耗尽。还可以通过信号量(SEMAPHORE)的方式来实现, 信号量的方式相比于线程池的方式,它不会创建线程,而是通过计数器来实现线程的隔离。

SEMAPHORE: 信号量,是一种计数信号,用来控制同时访问特定资源的线程数量,或者同时执行某个指定操作的数量。读作: /ˈseməfɔːr/。


  • 请求缓存: 在 Hystrix 中,请求缓存是通过请求命令的 execute()queue() 方法来实现的,它们都会将请求结果缓存起来,当下次请求相同的命令时,就会直接从缓存中获取结果,而不是再次发送请求。

  • 请求合并:在 Hystrix 中,请求合并是通过请求命令的 queue() 方法来实现的,它会将多个请求合并为一个请求,然后再发送给依赖服务。

  • 服务监控:Hystrix 通过实现 HystrixCommandMetrics 和 HystrixThreadPoolMetrics 来实现对服务的监控,它们分别用于监控命令的执行情况和线程池的执行情况。


接下来,我们就从一个简单示例开始对 Spring Cloud Hystrix 的学习。


对消费端进行改造


1.在消费端引入 Hystrix 的启动依赖


    org.springframework.cloud
    spring-cloud-starter-netflix-hystrix



    org.springframework.cloud
    spring-cloud-starter-netflix-hystrix-dashboard



    org.springframework.boot
    spring-boot-starter-actuator

2.改造启动类

在消费侧启动类上添加@EnableCircuitBreaker 注解,启动断路器功能。

@MapperScan("com.niit.order.mapper")
@SpringBootApplication
@EnableCircuitBreaker //启用断路器功能
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

断路器的作用是当某个服务单元发生故障之后,通过断路器的故障监控,向调用方返回一个错误响应,而不是长时间的等待,这样就不会使得线程因调用故障服务而被长时间占用不释放,避免了故障在分布式系统中的蔓延。也叫做服务降级。


2.新增 Controller

在消费端新增一个 Controller,使用 RestTemplate 模拟 HTTP 请求


@RestController
@RequestMapping("/hystrix")
public class HystrixTestController {

   @Autowired
   private RestTemplate restTemplate;

    @GetMapping("/{param}")
    @HystrixCommand(fallbackMethod = "showAppInfoFallback")
    public String showAppInfo(@PathVariable("param") String param) {
        //通过RestTemplate查询用户信息 使用服务名代替ip和端口号
        String url = "http://userservice/hystrix/"+param;
        return restTemplate.getForObject(url, String.class);
    }

    //回调方法 - 服务降级 - 当前方法调用失败时,调用该方法
    public String showAppInfoFallback(@PathVariable("param") String param) {
        return "服务器繁忙,请稍后再试o(╥﹏╥)o";
    }
}

上述代码中,@HystrixCommand 中的 fallbackMethod 属性代表着当前方法的回调方法,属性值就是该 Controller 下的方法名.


对服务端进行改造


由于当前是快速入门的 demo,我们就只用简单定义一下 Controller 即可,提供一个新的对外开放的 API 即可。因此,可以声明一个新的 Controller,借由这个 Controller 来观察输出结果。

@Slf4j
@RestController
@RequestMapping("/hystrix")
public class HystrixProviderController {
    //自动注入端口号的值,方便区分查看应用
    @Value("${server.port}")
    private String port;

    @GetMapping("/{param}")
    public String showInfo(@PathVariable("param")String param){
        return "当前应用的端口是: " + port+ "传递过来的参数是: "+ param;
    }
}

启动要求


1.启动 Nacos

以本地(windows 系统)单机模式启动,在 Nacos 安装目录 bin 处打开命令窗口:

startup.cmd -m standalone

2.启动消费端

修改配置:

ribbon.staticServerList:
  userservice:
    - http://localhost:9020
    - http://localhost:9021
    - http://localhost:9022

并分别启动三个服务实例,端口号分别为 9020,9021,9022

实际测试的时候, 此配置无效


3.启动服务端

我们已经掌握了负载均衡的相关知识,可启动多个服务端,本案例将启动三个服务实例进行演示。

所有项目启动成功后,端口号如下图所示:

bg right fit


测试及结果

1.正常访问

本案例访问路径为: http://localhost:8080/consumer/hystrix/100 ,多次发送 Get 请求,可以看到如下三种可能的结果:

当前应用的端口是: 9020; 传递过来的参数是: 100;
当前应用的端口是: 9021; 传递过来的参数是: 100;
当前应用的端口是: 9022; 传递过来的参数是: 100;

2.服务器宕机

如果ribbon.staticServerList配置是有效的, 那么假如我们停掉 9022 端口的实例,那么刷新之后,将可能会出现以下这样的三种情境:

当前应用的端口是: 9020; 传递过来的参数是: 100;
当前应用的端口是: 9021; 传递过来的参数是: 100;
服务器繁忙,请稍后再试o(╥﹏╥)o

实际情况是, ribbon.staticServerList配置是无效的, 估计这个配置已经被废弃了, 因为使用静态的服务列表是无法感知动态同步示例的变化的, 所以实际上也不推荐使用这个配置.

为了模拟降级的发生, 可以干脆停止所有的实例, 这样一定会出现服务降级了:

服务器繁忙,请稍后再试o(╥﹏╥)o

3.服务端超时

除了通过断开具体的服务实例来模拟某个节点无法访问的情况之外,我们还可以模拟一下服务阻塞(长时间未响应)的情况。我们对 userservice 的/HystrixProviderController控制器并做一些修改,具体如下:


@Slf4j
@RestController
@RequestMapping("/hystrix")
public class HystrixProviderController {
    /**
     * 自动注入端口号的值,方便区分查看应用
     */
    @Value("${server.port}")
    private String port;

    @GetMapping("/{param}")
    public String showInfo(@PathVariable("param") String param) throws InterruptedException {
        //让处理线程等待几秒钟
        long begin = System.currentTimeMillis();
        int sleepTime = new Random().nextInt(2000);
        Thread.sleep(sleepTime);
        long end = System.currentTimeMillis();
        System.out.println("阻塞时间: " + (end-begin)+"毫秒");
        return "当前应用的端口是: " + port
                + "; 传递过来的参数是: "
                + param + "; 共花费执行时间: "
                + (end - begin) + " 毫秒";
    }
}

通过 Thread.sleep()函数可让/hello 接口的处理线程不是马上返回内容,而是在阻塞几秒之后才返回内容。由于 Hystrix 默认超时时间为 1000 毫秒,所以这里采用了 02000 的随机数以让处理过程有一定概率发生超时来触发断路器:

阻塞时间: 865毫秒
当前应用的端口是: 9020; 传递过来的参数是: 100; 共花费执行时间: 865 毫秒
阻塞时间: 1655毫秒
服务器繁忙,请稍后再试o(╥﹏╥)o

前面演示的是因为超时而触发服务降级的情况,那么如果是因为服务端抛出异常而触发服务降级呢?这里我们将userservice的/hystrix/{param}接口逻辑改为抛出异常:

    @GetMapping("/{param}")
    public String showInfo(@PathVariable("param")String param){
        // 制造异常
        int i = 1/0;
        return "当前应用的端口是: " + port+ "传递过来的参数是: "+ param;
    }

这样在测试的时候, 你会发现, 无论你发送多少次请求, 都会立即返回服务器繁忙,请稍后再试o(╥﹏╥)o, 并且没有任何停顿, 说明服务降级已经生效了。这时的服务降级是因为服务端抛出异常而触发的, 而不是因为超时而触发的。


注意:在该入门案例中,消费端的 application.yml 配置文件中不要设置自定义的超时时间。而是使用默认的超时时间.(1秒钟)


原理分析(How It Works)

通过上面的快速入门示例,我们对 Hystrix 的使用场景和使用方法已经有了一个基础的认识。接下来我们通过解读 Netflix Hystrix 官方的流程图来详细了解一下:当一个请求调用了相关服务依赖之后 Hystrix 是如何工作的。


bg fit


1. 构造一个HystrixCommandHystrixObservableCommand对象

第一步是构造一个HystrixCommandHystrixObservableCommand对象来表示您对依赖项发出的请求。向构造函数传递发出请求时需要的任何参数。代码中我们可以简单的通过使用@HystrixCommand注解实现.

Hystrix 采用了命令模式来实现对服务调用操作的封装。而这两个 Command 对象分别针对不同的应用场景。

  • HystrixCommand:用在依赖的服务返回单个操作结果的时候。
    • 用于包装将执行潜在风险功能(通常意味着通过网络进行服务调用)的代码,具有容错和延迟、统计信息和性能指标捕获、断路器和隔板功能。此命令本质上是一个阻塞命令,但如果与 observe() 一起使用,则会提供一个监听器模式的非阻塞实现。
  • HystrixObservableCommand:用在依赖的服务返回多个操作结果的时候。
    • 作用和HystrixCommand相同, 不同的是此命令应用于纯非阻塞调用模式。此命令的调用方将订阅 run() 方法返回的 Observable。

如何给一个接口构造一个HystrixCommandHystrixObservableCommand对象呢?我们可以通过继承HystrixCommand类或HystrixObservableCommand来实现,也可以通过@HystrixCommand注解来实现。


如何给一个接口构造一个HystrixObservableCommand对象呢?我们可以通过继承HystrixObservableCommand类来实现,也可以通过@HystrixCommand注解来实现。


设计模式之命令模式介绍

Hystrix 利用命令模式来实现对服务调用操作的封装。命令模式是一种数据驱动的设计模式,它属于行为型模式。在命令模式中,我们可以将请求封装成一个对象,以便使用不同的请求、队列或日志来参数化其他对象。命令模式也支持可撤销的操作。通过使用命令模式可以实现“行为请求者”与“行为实现者”的解耦,以便使两者可以适应变化。


下面的示例是对命令模式的简单实现:


  • 行为接收者类: Receiver, 负责具体的业务逻辑处理,它是真正执行命令的对象。

    public class Receiver {
      public void action(){
          //执行真正的业务逻辑
          System.out.println("Receiver 执行命令...");
      }
    }

  • 命令接口: ICommand, 它定义了一个命令对象应具备的一系列命令操作,比如 execute ()undo()redo()等。当命令操作被调用的时候就会触发接收者去做具体命令对应的业务逻辑。

    public interface ICommand {
      /**
      * 执行方法
      */
      void execute();
    }

  • 命令实现类: ConcreteCommand, 它是抽象命令类的具体实现类,它是一个具体的命令对象通过调用接收者的相关操作来实现业务逻辑。

    public class ConcreteCommand implements ICommand {
    
      private Receiver receiver; // 组合接收者对象
    
      public ConcreteCommand(Receiver receiver){
          this.receiver = receiver;
      }
    
      @Override
      public void execute() {
          this.receiver.action();
      }
    }

  • 命令调用者类: Invoker, 它是命令模式的核心,它负责调用命令对象执行请求,相关的方法叫做行动方法。它可以持有一个或多个命令对象,它在需要的时候可以去调用命令对象的执行方法来执行请求,它通过这种方式来实现间接调用请求接收者的相关操作来实现业务操作。

    public class Invoker {
      private ICommand  command; // 组合命令对象
    
      public void setCommand(ICommand command) {
          this.command = command;
      }
      public void action(){
          this.command.execute();
      }
    }

  • 客户端: Client, 它是命令模式的使用者,它通过调用者来执行命令。

    public class Client {
      public static void main(String[] args) {
          Receiver receiver = new Receiver();
          ConcreteCommand command = new ConcreteCommand(receiver);
          Invoker invoker = new Invoker();
          invoker.setCommand(command);
          //客户端通过调用者来执行命令
          invoker.action();
      }
    }

w:30em


从上面的示例中,我们可以看到,调用者 Invoker 与操作者 Receiver 通过 Command 命令接口实现了解耦。对于调用者来说,我们可以为其注入多个命令操作,比如新建文件、 复制文件、删除文件这样三个操作,调用者只需在需要的时候直接调用即可,而不需要知道这些操作命令的具体对象是来自哪些操作者。

Invoker 和 Receiver 的关系非常类似于“请求 - 响应”模式,所以它比较适用于实现记录日志、撤销操作、队列请求等。


在下面这些情况下应考虑使用命令模式。

  • 使用命令模式作为“回调(CallBack)”在面向对象系统中的替代。“CallBack”讲的便是先将一个函数登记上,并不马上执行, 然后在以后需要的时候再调用此函数。
  • 需要在不同的时间指定请求、将请求排队。一个命令对象和原先的请求发出者可以有不同的生命周期。换言之,原先的请求发出者可能己经不在了,而命令对象本身仍然是活动的(通过命令接口解耦命令调用方和命令操作方)。这时命令的接收者可以是在本地,也可以在网络的另外一个地址。命令对象可以在序列化之后传送到另外一台机器上去。
  • 系统需要支持命令的撤销。命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用 undo()方法,把命令所产生的效果撤销掉。
  • 命令对象还可以提供 redo()方法,以供客户端在需要时再重新实施命令效果。
  • 如果要将系统中所有的数据更新到日志里,以便在系统崩溃时,可以根据日志读回所有的数据更新命令,重新调用 executed 方法一条一条执行这些命令,从而恢复系统在崩溃前所做的数据更新。

2. Hystrix 命令模式

有四种方法可以执行 HystrixCommandHystrixObservableCommand,通过使用 Hystrix 命令对象的以下四种方法之一:

bg right fit


  • 执行 HystrixCommand
    • execute() — 同步执行,从依赖的服务返回一个单一的结果对象,或是在发生错误的时候抛出异常。
    • queue() — 异步执行,直接返回一个 Future 对象,其中包含了服务执行结束时要返回的单一结果对象。
  • 执行 HystrixObservableCommand,通过使用

    • observe() — 返回来自依赖项的响应的 Observable 对象 ,它代表了操作的多个结果,它是一个 hot Observable
    • toObservable() — 同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个 cold Observable
    K             value   = command.execute();
    Future     fValue  = command.queue();
    Observable ohValue = command.observe();         //hot observable
    Observable ocValue = command.toObservable();    //cold observable

execute() - 同步执行

@HystrixCommand(fallbackMethod = "fallbackMethod")
public String strConsumer() {
 ResponseEntity result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
 return result.getBody();
}

fallbackMethod —— 回调方法,在服务调用异常、断路器打开、线程池/请求队列/信号量占满时会走回调逻辑。必须和服务方法定义在同一个类中,对修饰符没有特定的要求,定义为 private、 protected、 public 均可。


queue() - 异步执行

@HystrixCommand(fallbackMethod = "fallbackMethod", ignoreExceptions = {IllegalAccessException.class})
public Future asyncStrConsumer() {

 Future asyncResult = new AsyncResult() {
  @Override
  public String invoke() {
   ResponseEntity result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
   return result.getBody();
  }
 };
 return asyncResult;
}

ignoreExceptions 表示抛出该异常时不走降级回调逻辑,忽略此异常。


observe () 执行方式

 @HystrixCommand(observableExecutionMode = ObservableExecutionMode.EAGER)
 protected Observable construct() {
  ResponseEntity result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
  return Observable.just(result.getBody());
 }

ObservableExecutionMode.EAGER —— 代表使用 observe() 执行方式,返回的是一个 hot Observable,即便没有订阅者,它也会立即执行。


toObservable() 执行方式

@HystrixCommand(observableExecutionMode = ObservableExecutionMode.LAZY)
protected Observable construct() {
 ResponseEntity result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
 return Observable.just(result.getBody());
}

ObservableExecutionMode.LAZY —— 代表使用 toObservable() 执行方式,返回的是一个 cold Observable,只有在有订阅者时才会执行。


这里返回的 Observable 对象如何理解呢?我们可以把它理解为“事件源”或是“被观察者”,与其对应的 Subscriber 对象,可以理解为“订阅者”或是“观察者”。这两个对象是 RxJava 响应式编程的重要组成部分。


在 Hystrix 的底层实现中大量地使用了 RxJava, RxJava 是一个在 Java 虚拟机上使用可观测的序列来组成异步的、基于事件的程序的库。主要用于事件处理、数据集操作、异步任务以及基于回调的架构。

为了更容易地理解后续内容,在这里 对 RxJava 的观察者-订阅者模式做一个简单的入门介绍。


  • Observable 用来向订阅者 Subscriber 对象发布事件,Subscriber 对象则在接收到事件后对其进行处理,而在这里所指的事件通常就是对依赖服务的调用。
  • 一个 Observable 可以发出多个事件,直到结束或是发生异常。
  • Observable 对象每发出一个事件,就会调用对应观察者 Subscriber 对象的 onNext()方法。
  • 每一个 Observable 的执行,最后一定会通过调用 Subscriber.onCompleted()或者 Subscriber.onError()来结束该事件的操作流。

下面我们通过一个简单的例子来直观理解一下:

// 1. 创建一个被观察者 Observable
Observable observable = Observable.create(new Observable.OnSubscribe() {
    // 2. 在被观察者 Observable 被订阅时,回调该方法
    @Override
    public void call(Subscriber subscriber) {
        // 3. 发送事件:onNext()
        subscriber.onNext("Hello RxJava");
        subscriber.onNext("I am Jeff");
        // 4. 发送事件:onCompleted()
        subscriber.onCompleted();
    }
});
// 5. 创建一个观察者 Observer
Observer observer = new Observer() {
    // 6. 注册观察者时,回调该方法
    @Override
    public void onCompleted() {
        // 7. 观察者接收完事件后,回调该方法
    }

    // 6. 注册观察者时,回调该方法
    @Override
    public void onError(Throwable e) {
        // 7. 发生错误时,回调该方法
    }
    // 6. 注册观察者时,回调该方法
    @Override
    public void onNext(String str) {
        // 7. 接收到事件时,回调该方法
        System.out.println("Subscriber: " + str);
    }
};
// 8. 通过订阅(Subscribe)连接观察者和被观察者
observable.subscribe(observer);

在该示例中,创建了一个简单的事件源 observable, —个对事件传递内容输出的订阅者 subscriber,通过 observable.subscribe (subscriber)来触发事件的发布。

调用过程如下:

  1. 主线程调用 Observable.create() 方法,创建一个被观察者 Observable
  2. 被观察者调用 Observable.subscribe() 方法,订阅观察者 Subscriber
  3. 被观察者调用观察者的 Subscriber.onNext() 方法,让观察者接收到事件
  4. 这时观察者调用通过 next() 方法输出事件内容
  5. 被观察者调用 Subscriber.onCompleted() 方法,用来通知观察者事件已经结束
  6. 这时观察者调用 complete() 方法,用来结束事件流.

这在设计模式上一般称为观察者模式,也叫发布-订阅模式: 定义了对象之间的一对多的依赖关系,让多个观察者对象同时监听某一个主题对象(被观察者),这个主题对象在状态发生变化时,会通知所有观察者对象执行更新方法。

特点: 观察者和被观察者之间是松散耦合的,观察者和被观察者之间建立了一套触发机制。


在这里我们对于 Hystrix 的事件源 observable 提到了两个不同的概念:Hot ObservableCold Observable, 别对应了上面 command.observe ()command.toObservable ()的返回对象。

  • Hot Observable:
    Hot Observable 是指在订阅之前就开始发射数据的 Observable。observe()方法返回的 Hot Observable 代表了一个可观察的事件流,它可以被订阅,以便在事件流中接收数据。
  • Cold Observable:
    和 Hot Observable 的区别是, Cold Observable 是 toObservable()方法返回的, 并且只有在订阅之后才开始发射数据。

3. 结果是否被缓存

若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以 Observable 对象的形式返回。

启用缓存的办法: 在创建 HystrixCommand 对象时,通过 HystrixCommandProperties.Setter().withRequestCacheEnabled(true)来启用缓存功能。


4. 断路器是否打开

在命令结果没有缓存命中的时候,Hystrix 在执行命令前需要检查断路器是否为打开状态.

  • 如果断路器是打开的,那么 Hystrix 不会执行命令,而是转接到 fallback 处理逻辑(对应下面第 8 步)。
  • 如果断路器是关闭的,那么 Hystrix 会继续检查是否有可用资源来执行命令。(关于断路器的具体实现细节,后续会做更加详细的分析。)

5. 线程池/请求队列/信号量是否占满

如果与命令相关的线程池和请求队列,或者信号量(不使用线程池的时候)己经被占满,那么 Hystrix 也不会执行命令,而是转接到 fallback 处理逻辑(对应下面第 8 步)。需要注意的是,这里 Hystrix 所判断的线程池并非容器的线程池,而是每个依赖服务的专有线程池。Hystrix 为了保证不会因为某个依赖服务的问题影响到其他依赖服务而采用了“舱壁模式”(Bulkhead Pattern)来隔离每个依赖的服务。关于依赖服务的隔离与线程池相关的内容见后续详细介绍。


6. 执行HystrixObservableCommand.construct()HystrixCommand.run()

Hystrix 会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。

  • HystrixCommand.run(): 返回一个单一的结果,或者抛出异常。
  • HystrixObservableCommand.construct(): 返回一个 Observable 对象来发射多个结果,或通过 onError 发送错误通知。

如果 run()construct()方法的执行时间超过了命令设置的超时阈值,当前处理线程将会抛出一个 TimeoutException (如果该命令不在其自身的线程中执行,则会通过单独的计时线程来抛出)。在这种情况下,Hystrix 会转接到 fallback 处理逻辑(第 8 步)。 同时,如果当前命令没有被取消或中断,那么它最终会忽略 run()或者 construct()方法的返回。

如果命令没有抛出异常并返回了结果,那么 Hystrix 在记录一些日志并采集监控报告之后将该结果返回。在使用 run()的情况下,Hystrix 会返回一个 Observable,它发射单个结果并产生 onCompleted 的结束通知;而在使用 construct() 的情况下,Hystrix 会 直接返回该方法产生的 Observable 对象。


7. 计算断路器的健康度

Hystrix 会将“成功”、“失败”、“拒绝”、“超时”等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进 行“熔断/短路”,直到恢复期结束。若在恢复期结束后,根据统计数据判断如果还是未达到健康指标,就再次“熔断/短路”。


8. fallback 处理

当命令执行失败的时候,Hystrix 会进入 fallback 尝试回退处理,我们通常也称该操作为“服务降级”。而能够引起服务降级处理的情况有下面几种:

  • 当前命令处于“熔断/短路”状态,断路器是打开的时候。
  • 当前命令的线程池、请求队列或者信号量被占满的时候。
  • HystrixObservableCommand.construct() HystrixCommand.run() 抛出异常的时候。

在服务降级逻辑中,我们需要实现一个通用的响应结果,并且该结果的处理逻辑应当是从缓存或是根据一些静态逻辑来获取,而不是依赖网络请求获取。如果一定要在降级逻辑中包含网络请求,那么该请求也必须被包装在 HystrixCommand 或是 HystrixObservableCommand 中,从而形成级联的降级策略,而最终的降级逻辑一定不是一个依赖网络请求的处理,而是一个能够稳定地返回结果的处理逻辑。


在 HystrixCommand 和 HystrixObservableCommand 中实现降级逻辑时还略有不同:

  • 当使用 HystrixCommand 的时候,通过实现 HystrixCommand.getFallback()来实现服务降级逻辑。
  • 当使用 HystrixObservableCommand 的时候,通过 HystrixObservableCommand.resumeWithFallback()实现服务降级逻辑,该方法会返回一个 Observable 对象来发射一个或多个降级结果。

当命令的降级逻辑返回结果之后,Hystrix 就将该结果返回给调用者。当使用 HystrixCommand.getFallback()的时候,它会返回一个 Observable 对象,该对象会发射 getFallback()的处理结果。而使用 HystrixObservableCommand.resumeWithFallback ()实现的时候,它会将 Observable 对象直接返回。


如果我们没有为命令实现降级逻辑或者降级处理逻辑中抛出了异常,Hystrix 依然会返回一个 Observable 对象,但是它不会发射任何结果数据,而是通过 onError 方法通知命令立即中断请求,并通过 onError ()方法将引起命令失败的异常发送给调用者。实现一个有可能失败的降级逻辑是一种非常糟糕的做法,我们应该在实现降级策略时尽可能避免失败的情况。


降级执行失败时, Hystrix 会根据不同的执行方法做出不同的处理。

  • execute(): 抛出异常。
  • queue():正常返回 Future 对象,但是当调用 get()来获取结果的时候会抛出异常。
  • observe():正常返回 Observable 对象,当订阅它的时候,将立即通过调用订阅者的 onError 方法来通知中止请求。
  • toObservable(): 正常返回 Observable 对象,当订阅它的时候,将通过调用订阅者的 onError 方法来通知中止请求。

  1. 返回成功的响应
    当 Hystrix 命令执行成功之后,它会将处理结果直接返回或是以 Observable 的形式 返回。而具体以哪种方式返回取决于之前第 2 步中我们所提到的对命令的 4 种不同执行方 式,下图中总结了这 4 种调用方式之间的依赖关系。我们可以将此图与在第 2 步中对前两 者源码的分析联系起来,并且从源头 toObservable ()来开始分析。

bg fit


  • execute() - 以与 queue()相同的方式获取一个 Future,然后在这个 Future 上调用 get()来获取由 Observable 发出的单个值
  • queue() - 将 Observable 转换为一个 BlockingObservable,以便它可以被转换为一个 Future,然后返回这个 Future
  • observe() - 在 toObservable()产生原始 Observable 之后立即订阅它,让命令能够马上开始异步执行,并返回一个 Observable 对象,当调用它的 subscribe 时,将重新产生结果和通知给订阅者。
  • toObservable() - 返回最原始的 Observable,必须通过订阅它才会真正触发命令的执行流程。

断路器原理

断路器在 HystrixCommand 和 HystrixObservableCommand 执行过程中起到了举足轻重的作用,它是 Hystrix 的核心部件。

断路器有三种状态:


bg right fit

  • 关闭状态(Closed):断路器关闭,所有请求会被发送到服务端。
  • 打开状态(Open):当触发熔断阈值后,断路器打开,所有请求都不会被发送到服务端,直接返回错误。
  • 半开状态(Half-Open):断路器半开,部分请求会被发送到服务端,如果请求成功,断路器关闭,如果请求失败,断路器打开。
    • 当断路器打开后,会启动一个定时器,定时器到期后,断路器进入半开状态。默认情况下,断路器会在 5 秒后进入半开状态。
    • 当断路器处于半开状态时,如果请求成功,断路器关闭,如果请求失败,断路器打开。

默认触发熔断并造成断路器打开的条件是:

  1. 快照时间窗(默认 10 秒)
  2. 请求总数阈值(默认 20)
  3. 错误百分比阈值(默认 50%)

涉及断路器的三个参数:快照时间窗、请求总数阀值、错误百分比阀值。

  1. 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的 10 秒。它是一个滚动时间窗. 每过一段时间,比如 10 秒,就会统计最近 10 秒内的请求总数,如果请求数量超过了阀值,就会根据错误百分比来判断是否需要打开断路器。

  2. 请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为 20,意味着在 10 秒内,如果该 hystrix 命令的调用次数不足 20 次,即使所有的请求都超时或其他原因失败,断路器都不会打开。

  3. 错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了 30 次调用,如果在这 30 次调用中,有 15 次发生了超时异常,也就是超过 50%的错误百分比,在默认设定 50%阀值情况下,这时候就会将断路器打开。


配置断路器参数:

@HystricCommand(fallbackMethod = "fallback", commandProperties = {
    @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 是否开启断路器
    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 请求总数阀值
    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), // 错误百分比阀值
})

底层原理 (了解)

那么断路器是如何决策熔断和记录信息的呢?

下图是 Netflix Hystrix 官方文档中关于断路器的详细执行逻辑,可以帮助我们理解断路器的工作流程。

https://github.com/Netflix/Hystrix/wiki/


bg fit


  1. 假设大量的请求数量超过了 HystrixCommandProperties.circuitBreakerRequestVolumeThreshold() 的阈值(默认 20);或者在滚动时间窗口内,失败的请求数量占总请求数量的百分比超过了 HystrixCommandProperties.circuitBreakerErrorThresholdPercentage() 的阈值(默认 50%),那么断路器将会被打开,此时所有的请求都会被拒绝(失败), 熔断器将会从闭合状态变成打开状态;

  1. 在熔断器处于打开状态的期间,所有对这个依赖进行的调用都会短路,即不进行真正的依赖调用,快速失败;
    在等待(冷却)的时间超过 HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds() 的值(默认 5 秒)后,断路器将会进入半开状态,此时断路器将会允许一个请求去调用依赖,如果这个请求调用成功,断路器将会闭合, 后续依赖调用可正常执行。,如果这个请求调用失败,断路器将会继续打开,继续等待下一次的半开状态(冷却)。

  1. 这里 10 1-second "buckets" 的意思是,每 1 秒,会统计最近 10 秒内的请求总数,如果请求数量超过了阀值,就会根据错误百分比来判断是否需要打开断路器。

这个桶数可以通过 HystrixCommandProperties.metricsRollingStatisticalWindowBuckets() 来配置,默认为 10 个桶,每个桶 1 秒,所以默认的快照时间窗为 10 秒。

现在我们来看看断路器 HystrixCircuitBreaker 的定义:

public interface HystrixCircuitBreaker {
    boolean allowRequest();

    boolean isOpen();

    void markSuccess();

    public static class NoOpCircuitBreaker implements HystrixCircuitBreaker {...}

    public static class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {...}

    public static class Factory {...}

可以看到它的接口定义并不复杂,主要定义了三个断路器的抽象方法。

  • allowRequest()
    每个 Hystrix 命令的请求都通过它判断是否被执行。
  • isOpen()
    返回当前断路器是否打开。
  • markSuccess()
    用来闭合断路器。

另外还有三个静态类。

  • 静态类 Factory 中维护了一个 Hystrix 命令与 HystrixCircuitBreaker 的关系集合:

    private static ConcurrentHashMap circuitBreakersByCommand = new ConcurrentHashMap();

    其中 String 类型的 key 通过 HystrixCommandKey 定义,每一个 Hystrix 命令需要有一个 key 来标识,同时一个 Hystrix 命令也会在该集合中 找到它对应的断路器 HystrixCircuitBreaker 实例。


  • 静态类 NoOpCircuitBreaker 定义了一个什么都不做的断路器实现,它允许所有请求,并且断路器状态始终闭合(允许请求通过)。

  • 静态类 HystrixCircuitBreakerlmpl 是断路器接口 HystrixCircuitBreaker 的实现类,在该类中定义了断路器的 4 个核心对象。

    • HystrixCommandProperties properties:断路器对应 HystrixCommand 实例的属性对象,它的详细内容我们将在后续章节做具体的介绍。
    • HystrixCommandMetrics metrics:用来让 HystrixCommand 记录各类度量指标的对象。
    • AtomicBoolean circuitOpen:断路器是否打开的标志,默认为 false。
    • AtomicLong circuitOpenedOrLastTestedTime:断路器打开或是上一次测试的时间 戳。

​HystrixCircuitBreakerlmplHystrixCircuitBreaker 接口的各个方法实现如下所示。


  • isOpen ():判断断路器的打开/关闭状态。
    public boolean isOpen() {
        if (this.circuitOpen.get()) {
            return true;
        } else {
            HealthCounts health = this.metrics.getHealthCounts();
            if (health.getTotalRequests() < (long)(Integer)this.properties.circuitBreakerRequestVolumeThreshold().get()) {
                return false;
            } else if (health.getErrorPercentage() < (Integer)this.properties.circuitBreakerErrorThresholdPercentage().get()) {
                return false;
            } else if (this.circuitOpen.compareAndSet(false, true)) {
                this.circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
                return true;
            } else {
                return true;
            }
        }
    }

详细逻辑如下所示。

  • 如果断路器打开标识为 true,则直接返回 true,表示断路器处于打开状态。否则,就从度量指标对象 metrics 中获取 HealthCounts 统计对象做进一步判断(该 对象记录了一个滚动时间窗内的请求信息快照,默认时间窗为 10 秒)。
  • 如果它的请求总数(QPS)在预设的阈值范围内就返回 false,表示断路器处于未打开状态。该阈值的酉己置参数为 circuitBreakerRequestVolumeThreshold, 默认值为 20
  • 如果错误百分比在阈值范围内就返回 false,表示断路器处于未打开状态。该阈值的配置参数为 circuitBreakerErrorThresholdPercentage,默认值 为 50
  • 如果上面的两个条件都不满足,则将断路器设置为打开状态(熔断/短路)。同时,如果是从关闭状态切换到打开状态的话,就将当前时间记录到上面提到的circuitOpenedOrLastTestedTime 对象中。

  • allowRequest():判断请求是否被允许,这个实现非常简单。
public boolean allowRequest() {
    if ((Boolean)this.properties.circuitBreakerForceOpen().get()) {
        return false;
    } else if ((Boolean)this.properties.circuitBreakerForceClosed().get()) {
        this.isOpen();
        return true;
    } else {
        return !this.isOpen() || this.allowSingleTest();
    }
}

执行逻辑如下所示。

  • 先根据配置对象 properties 中的断路器判断强制打开或关闭属性是否被设置。如果强制打开,就 直接返回 false,拒绝请求。如果强制关闭,它会允许所有请求,但是同时也会调用 isOpen ()来执行断路器的计算逻辑,用来模拟断路器打开/关闭的行为。

  • 在默认情况下,断路器并不会进入这两个强制打开或关闭的分支中去,而是通过!isOpen()|| allowSingleTest()来判断是否允许请求访问。!isOpen ()之前己经介绍过,用来判断和计算当前断路器是否打开,如果是断开状态就不允许请求。那么 allowSingleTest()是用来做什么的呢?


public boolean allowSingleTest() {
    long timeCircuitOpenedOrWasLastTested = this.circuitOpenedOrLastTestedTime.get();

    return
      this.circuitOpen.get()
      &&
      System.currentTimeMillis() >
         timeCircuitOpenedOrWasLastTested
         + (long)(Integer)this.properties.circuitBreakerSleepWindowInMilliseconds().get()
      &&
      this.circuitOpenedOrLastTestedTime.compareAndSet(
        timeCircuitOpenedOrWasLastTested,
        System.currentTimeMillis()
      );
}

执行逻辑如下所示:

  • allowSingleTest()的实现中我们可以看到,这里使用了在 isOpen()函数中当断路器从闭合到打开时候所记录的时间戳。当断路器在打开状态的时候,这里会判断 断开时的时间戳 + 配置中的 circuitBreakerSleepWindowInMilliseconds 时间是否小于当前时间,是的话,就将当前时间更新到记录断路器打开的时间对象 circuitOpenedOrLastTestedTime 中,并且允许此次请求。简单地说,通过 circuitBreakerSleepWindowInMilliseconds 属性设置了一个断路器打幵之后的休眠时间(默认为 5 秒),在该休眠时间到达之后,将再次允许请求尝试访问,此时断路器处于“半开”状态,若此时请求继续失败,断路器又进入打开状态,并继续等待下一个休眠窗口过去之后再次尝试;若请求成功,则将断路器重新置于关 闭状态。所以通过 allowSingleTest()isOpen()方法的配合,实现了断路器打开和关闭状态的切换。
  • markSucces():该函数用来在“半开路”状态时使用。若 Hystrix 命令调用成功, 通过调用它将打开的断路器关闭,并重置度量指标对象。

依赖隔离

Hystrix 提供了两种资源隔离策略:线程和信号量。

  • 在线程隔离中,HystrixCommand 将在单独的线程上执行,并且并发请求受线程池中线程数量的限制。
  • 在信号量隔离中,HystrixCommand 将在调用线程上执行,开销相对较小,并且请求受信号量数量的限制 。

“舱壁模式”对于熟悉 Docker 的读者一定不陌生,Docker 通过“舱壁模式”实现进程 的隔离,使得容器与容器之间不会互相影响。而 Hystrix 则使用该模式实现线程池的隔离, 它会为每一个依赖服务创建一个独立的线程池,这样就算某个依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的依赖服务。


bg fit


  • 总之,通过对依赖服务实现线程池隔离,可让我们的应用更加健壮,不会因为个别依赖服务出现问题而引起非相关服务的异常。同时,也使得我们的应用变得更加灵活,可以在不停止服务的情况下,配合动态配置刷新实现性能配置上的调整。

虽然线程池隔离的方案带来如此多的好处,但是很多使用者可能会担心为每一个依赖服务都分配一个线程池是否会过多地增加系统的负载和开销。对于这一点,使用者不用过于担心,因为这些顾虑也是大部分工程师们会考虑到的,Netflix 在设计 Hystrix 的时候,认为线程池上的开销相对于隔离所带来的好处是无法比拟的。同时,Netflix 也针对线程池的 开销做了相关的测试,以用结果打消 Hystrix 实现对性能影响的顾虑。


下图是 Netflix Hystrix 官方提供的一个 Hystrix 命令的性能监控图,该命令以每秒 60 个请求的速度(QPS)对一个单服务实例进行访问,该服务实例每秒运行的线程数峰值为 350 个。


bg fit


每种情况都有两个曲线,一个是未使用线程池隔离,一个是使用了线程池隔离。根据图表统计,不难得出如下结论:

比较情况 未使用线程池隔离 使用了线程池隔离 耗时差距
中位数 2ms 2ms 2ms
90 百分位 5ms 8ms 3ms
99 百分位 28ms 37ms 9ms

对于大多数 Netflix 用例而言,使用线程池隔离的开销已被认为是可以接受的,因为可以实现弹性优势。


虽然对于大部分的请求我们可以忽略线程池的额外开销,而对于小部分延迟本身就非常小的请求(可能只需要 lms),那么 9ms 的延迟开销还是非常昂贵的。Hystrix 为此设计了另外的解决方案:信号量

在 Hystrix 中除了可使用线程池之外,还可以使用信号量来控制单个依赖服务的并发度,信号量的开销远比线程池的开销小,但是它不能设置超时和实现异步访问。所以,只有在依赖服务是足够可靠的情况下才使用信号量。


在 HystrixCommand 和 HystrixObservableCommand 中有两处支持信号量的使用:

  • 命令执行:如果将隔离策略参数 execution.isolation.strategy 设置为 SEMAPHORE, Hystrix 会使用信号量替代线程池来控制依赖服务的并发。(默认值为 THREAD)。
  • 降级逻辑:当 Hystrix 尝试降级逻辑时,它会在调用线程中使用信号量。信号量的默认值为 10,我们也可以通过动态刷新配置的方式来控制并发线程的数量。 对于信号量大小的估算方法与线程池并发度的估算类似。仅访问内存数据的请求一般耗时 在 lms 以内,性能可以达到 5000 RPS(RPS 指每秒的请求数),这样级别的请求可以将信号量 设置为 1 或者 2,我们可以按此标准并根据实际请求耗时来设置信号量。

这两种策略之间的选择取决于您的使用情况。如果您每秒有大量请求,则可以考虑使用信号量。其次,使用信号量时,命令将在调用者的线程内执行。这意味着并发调用并未完全与其他调用隔离(不像使用线程时)。

一般来说,在认为开销足够小的情况下,Netflix 在实践中通常更喜欢单独线程的隔离优势而选择线程的隔离策略。


使用详解和属性详解

使用详解

在“快速入门”一节中我们己经使用过 Hystrix 中的核心注解@HystrixCommand 通过它创建了 HystrixCommand 的实现,同时利用 fallback 属性指定了服务降级的实现 方法。接下来我们将详细介绍 @HystrixCommand 注解的使用方法。


创建请求命令

Hystrix 命令就是我们之前所说的 HystrixCommand,它用来封装具体的依赖服务调用逻辑。我们可以通过@HystrixCommand 注解来更为优雅地实现 Hystrix 命令的定义:


@GetMapping("/{param}")
@HystrixCommand(fallbackMethod = "showAppInfoFallback")
public String showAppInfo(@PathVariable("param") String param) {
    String url = "http://userservice/hystrix/"+param;
    return restTemplate.getForObject(url, String.class);
}
  • 当触发回调函数执行条件时,将执行 fallbackMethod 属性所定义的 名字为showAppInfoFallback的函数。

定义服务降级

fallbackMethod —— Hystrix 命令执行失败时使用的回退方法, 用来实现服务降级,在服务调用异常、断路器打开、线程池/请求队列/信号量占满时会走回调逻辑。必须和服务方法定义在同一个类中,对修饰符没有特定的要求,定义为 private、 protected、 public 均可, 如:

/**
 * 服务降级方法
 * @param param 源方法传入的参数
 * @return 和源方法的返回值类型保持一致
 */
public String showAppInfoFallback(String param) {
    return "服务器繁忙,请稍后再试o(╥﹏╥)o" + param;
}

  • 在上面的例子中,假如 showAppInfoFallback 方法实现的并不是一个稳定逻辑(有可能会再次进行网络请求),它依然可能会发生异常,那么我们也可以为它添加@HystrixCommand 注解以生成 Hystrix 命令,同时使用 fallbackMethod 来指定服务降级逻辑,比如:

@RestController
@RequestMapping("/hystrix")
public class HystrixConsumerController {

   @Autowired
   private RestTemplate restTemplate;

    @GetMapping("/{param}")
    @HystrixCommand(fallbackMethod = "showAppInfoFallback")
    public String showAppInfo(@PathVariable("param") String param) {
        String url = "http://userservice/hystrix/"+param;
        return restTemplate.getForObject(url, String.class);
    }

    /**
     * 服务降级方法
     * @param param 源方法传入的参数
     * @return 和源方法的返回值类型保持一致
     */
    @HystrixCommand(fallbackMethod = "showAppInfoFallbackEnd")
    public String showAppInfoFallback(String param) {
        String url = "http://userservice/hystrix/"+param+"abc";
        return restTemplate.getForObject(url, String.class);
    }

    /**
     * 终极服务降级方法
     * @param param 源方法传入的参数
     * @return 和源方法的返回值类型保持一致
     */

    public String showAppInfoFallbackEnd(String param) {
        return "最终降级方法,逻辑稳定..." + param;
    }
}

值得注意的是,定义服务降级方法时,最终要降级到一个逻辑稳定的方法上,避免这个方法再发生任何的异常情况。这样的设计对于系统来说才是有意义的,可以确保服务稳定性。


在实际使用时,我们需要为大多数执行过程中可能会失败的 Hystrix 命令实现服务降级逻辑,但是也有一些情况可以不去实现降级逻辑,如下所示。

  • 执行写操作的命令:当 Hystrix 命令是用来执行写操作而不是返回一些信息的时候,通常情况下这类操作的返回类型是 void 或是为空的 Observable,实现服务降级的意义不是很大。当写入操作失败的时候,我们通常只需要通知调用者即可。

  • 执行批处理或离线计算的命令:当 Hystrix 命令是用来执行批处理程序生成一份报告或是进行任何类型的离线计算时,那么通常这些操作只需要将错误传播给调用者,然后让调用者稍后重试而不是发送给调用者一个静默的降级处理响应。

不论 Hystrix 命令是否实现了服务降级,命令状态和断路器状态都会更新,并且我们可以由此了解到命令执行的失败情况。


异常处理

异常传播

在使用注册配置实现 Hystrix 命令时,它还支持忽略指定异常类型功能,只需要通过设置@HystrixCommand 注解的 ignoreExceptions 参数,比如:



@HystrixCommand(fallbackMethod = "showAppInfoFallbackEnd",
                ignoreExceptions = {BadRequestException.class})
public String showAppInfo(String param) {
    String url = "http://userservice/hystrix/"+param+"abc";
    return restTemplate.getForObject(url, String.class);
}

如上面代码的定义,当 showAppInfo 方法抛出了类型为 BadRequestException 的异常时 Hystrix 会将它包装在 HystrixBadRequestException 中抛出,这样就不会触发后续的 fallback 逻辑。


异常获取

当 Hystrix 命令因为异常(除了 HystrixBadRequestException 的异常)进入服务降级逻辑之后,往往需要对不同异常做针对性的处理,那么我们如何来获取当前抛出的异常呢?
注解配置方式的实现非常简单,只需要在 fallback 实现方法的参数中增加 Throwable e 对象的定义,这样在方法内部就可以获取触发服务降级的具体异常内容了,比如:


@RestController
@RequestMapping("/hystrix")
public class HystrixConsumerController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/{param}")
    @HystrixCommand(fallbackMethod = "showAppInfoFallback")
    public String showAppInfo(@PathVariable("param") String param) {
        throw new RuntimeException("showAppInfo fail");
    }

    /**
     * 服务降级方法
     *
     * @param param     源方法传入的参数
     * @param exception 接收到的异常信息
     * @return 和源方法的返回值类型保持一致
     */
    public String showAppInfoFallback(String param,Throwable exception) {
        return param+"====" + exception.getMessage();
    }
}

命令名称、分组以及线程划分

当我们使用 HystrixCommand 注解的时候,通过设置@HystrixCommand 注解的 CommandKeygroupKey 以及 threadPoolKey 属性即可设置命令名称、分组以及线程池划分,比如我们可以像下面这样进行设置:

@GetMapping("/{param}")
@HystrixCommand(fallbackMethod = "showAppInfoFallback",
  ignoreExceptions = {IOException.class, FileNotFoundException.class},
  commandKey = "showAppInfo",
  groupKey = "userGroup",
  threadPoolKey = "showAppInfoThread"
)
public String showAppInfo(@PathVariable("param") String param) {
    //通过RestTemplate查询用户信息 使用服务名代替ip和端口号
    String url = "http://userservice/hystrix/" + param;
    return restTemplate.getForObject(url, String.class);
}

Hystrix 会根据组来组织和统计命令的告警、仪表盘等信息。
默认情况下,Hystrix 会让相同组名的命令使用同一个线程池。
通常情况下,尽量通过 HystrixThreadPoolKey 的方式来指定线程池的划分,而不是通过组名的默认方式实现划分,因为多个不同的命令可能从业务逻辑上来看属于同一个组,但是往往从实现本身上需要跟其他命令进行隔离。


另外我们可以通过命令名称和线程池进行 Hystrix 的一些针对具体命令和线程池的相关配置,比如我们可以像下面这样进行设置:

hystrix:
  command:
    showAppInfo:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 1000
  threadpool:
    showAppInfoThread:
      coreSize: 10
      maxQueueSize: 100
      queueSizeRejectionThreshold: 50

请求缓存

当系统用户不断增长时,每个微服务需要承受的并发压力也越来越大。在分布式环境下,通常压力来自于对依赖服务的调用,因为请求依赖服务的资源需要通过通信来实现,这样的依赖方式比起进程内的调用方式会引起一部分的性能损失,同时 HTTP 相比于其他 高性能的通信协议在速度上没有任何优势,所以它有些类似于对数据库这样的外部资源进 行读写操作,在高并发的情况下可能会成为系统的瓶颈。既然如此,我们很容易地可以联想到,类似数据访问的缓存保护是否也可以应用到依赖服务的调用上呢?


答案显而易见,在高并发的场景之下,Hystrix 中提供了请求缓存的功能,我们可以方便地开启和使用请求缓存来优化系统,达到减轻高并发时的请求线程消耗、降低请求响应时间的效果。

Hystrix 的请求缓存可以通过注解的方式进行配置实现。注解配置的定义实现同 JSR 107 的定义非常相似,但由于 Hystrix 不需要独立外置的缓存系统来支持,所以没有 JSR 107 的定义那么复杂,它只提供了三个专用于请求 缓存的注解。


注解 描述 属性
@CacheResult 该注解用来标记请求命令返回的结果应该被缓存,它必须与@HystrixCommand 注解结合使用 cacheKeyMethod
@CacheRemove 该注解用来让请求命令的缓存失效,失效的缓存根据定义的 Key 决定 commandKey,cacheKeyMethod
@CacheKey 该注解用来在请求命令的参数上标记,使其作为缓存的 Key 值,如果没有标注则会使用所有参数。如果同时还使用了@CacheResult@CacheRemove 注解的 cacheKeyMethod 方法指定缓存 Key 的生成,那么该注解将不会起作用 value

JSR 107 是 Java 缓存 API 的定义,也被称为 JCache。它定义了一系列开发人 员使用的标准化 Java 缓存 API 和服务提供商使用的标准 SPI。下面我们从几个方面的实例来看看这几个注解的具体使用方法。


缓存生命周期

缓存生命周期是从HystrixRequestContext.initializeContext(); 初始化开始, 到HystrixRequestContext#shudown();结束。

初始化HystrixRequestContext

1、在每个用到请求缓存的Controller方法的第一行加上如下代码:

//初始化Hystrix请求上下文
@Cleanup("shutdown") HystrixRequestContext.initializeContext();

@Cleanup 是 lombok注解, 可以自动生成释放资源的代码,默认是调用资源的 close() 方法,也可以自己指定释放的方法

2、使用Filter方式:

在启动类加入@ServletComponentScan注解, 指定

创建HystrixRequestContextServletFilter.java,实现Filter接口,在doFilter方法中添加方法1中的那一行代码,并在一次请求结束后关掉这个上下文

@WebFilter(filterName = "hystrixRequestContextServletFilter",urlPatterns = "/*",asyncSupported = true)

public class HystrixRequestContextServletFilter implements Filter {    
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //初始化Hystrix请求上下文
        HystrixRequestContext context = HystrixRequestContext.initializeContext();        try {                      
            chain.doFilter(request, response);//请求正常通过
        } finally {                           
            context.shutdown();//关闭Hystrix请求上下文
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }    @Override
    public void destroy() {

    }
}

在应用启动类上开启包扫描:

@ServletComponentScan(basePackages = {"com.niit.order.filter"})

有了过滤器后, 每个Controller方法中的HystrixRequestContext.initializeContext();代码就可以去掉了.

如果没有为 HystrixRequestContext 进行初始化,那么系统将报如下错误:

image-20220722220530907

这样, Hystrix 的缓存设置是针对单次请求的: 因为每次请求来之前都执行了 HystrixRequestContext.initializeContext();进行初始化,每请求一次 controller 就会走一次 filter,上下文又会初始化一次,前面缓存的就失效了,又得重新来。所以如果测试缓存,得在 Controller 请求中多次调用那个加了缓存的 HystrixCommand 命令。

Hystrix 的书上写的是:在同一用户请求的上下文中,相同依赖服务的返回数据始终保持一致。在当次请求内对同一个依赖进行重复调用,只会真实调用一次。在当次请求内数据可以保证一致性。

测试请求缓存

通过注解为请求命令开启缓存功能非常简单,如下例所示,我们只需添加@CacheResult 注解即可。当该依赖服务被调用并返回 User 对象时,Hystrix 会将该结果置入请求缓存中,而 它的缓存 Key 值默认会使用所有的参数,也就是这里 String 类型的 id 值。若要为请求命令指定具体的缓存 Key 生成规则,我们可以使用@CacheResult 的 cacheKeyMethod 属性来指定具体的生成函数;也可以通过使 @CacheKey 注解 在方法参数中指定用于组装缓存 Key 的元素。这里我们使用 @CacheKey 注解(由于只有一个参数, 这个注解其实也可以直接省略):

    @CacheResult
    public String sayHello(@CacheKey String msg) {
        log.info("未走缓存 ↓");
        return  msg;
    }
    // fallback
    public String hiConsumerFallBack(String msg) {
        return "hi," + msg + ",sorry,error!";
    }

    /**
     * 使用注解请求缓存 方式1
     *
     * @CacheResult 标记这是一个缓存方法,结果会被缓存
     */

    @HystrixCommand
    @CacheRemove(commandKey = "testCommand")
    public String sayGoodbye(@CacheKey String msg) {
        log.info("清除缓存 ↓");
        return msg;
    }

    // test say hello
    @RequestMapping(value = "/hello/test", method = RequestMethod.GET)
    String sayHelloTest() {
        // @Cleanup  HystrixRequestContext hystrixRequestContext = HystrixRequestContext.initializeContext();
        log.info(sayHello("123")); 
        log.info(sayHello("123"));
        log.info(sayHello("321"));
        log.info(sayGoodbye("123"));
        log.info(sayHello("123"));
        log.info(sayHello("321"));
        return "测试";
    }

测试

未走缓存 ↓
123
未走缓存 ↓
123
未走缓存 ↓
321
清除缓存 ↓
123
未走缓存 ↓
123
未走缓存 ↓
321

发现所有调用都没有走缓存.

实践中发现, 必须通过实例来调用标记缓存的方法缓存才会生效, 也就是说必须通过new 或者自动装配的方式先获取标记缓存的方法所在类的实例, 然后通过实例调用缓存的方法, 缓存才可以生效.

修改如下:

把缓存方法放到service层:

@Slf4j
@Service("orderServiceCache")
public class OrderServiceCacheImpl {

    // LOGGER
    private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceCacheImpl.class);

    @Resource
    private RestTemplate restTemplate;

    @CacheResult(cacheKeyMethod = "getMsg")
    public String sayHello(@CacheKey String msg) {
        log.info("未走缓存 ↓");
        return  msg;
    }

    @HystrixCommand
    @CacheRemove(commandKey = "testCommand")
    public String sayGoodbye(@CacheKey String msg) {
        log.info("清除缓存 ↓");
        return msg;
    }
}

测试方法:

@Slf4j // lombok
@RestController
@RequestMapping("/consumer")
public class HystrixConsumerController03 {
    ...

    @RequestMapping(value = "/hello/test", method = RequestMethod.GET)
    String sayHelloTest() {
        // @Cleanup  HystrixRequestContext hystrixRequestContext = HystrixRequestContext.initializeContext();
        log.info(orderServiceCache.sayHello("123")); // 首次调用123不走缓存
        log.info(orderServiceCache.sayHello("123")); // 第二次调用123走缓存
        log.info(orderServiceCache.sayHello("321")); // 首次调用312不走缓存
        log.info(orderServiceCache.sayGoodbye("123")); // 清除123缓存
        log.info(orderServiceCache.sayHello("123"));  // 123缓存已清除,不走缓存
        log.info(orderServiceCache.sayHello("321")); //  321走缓存
        return "测试";
    }

测试接口:

GET http://localhost:9010/consumer/hello/test

输出:

未走缓存 ↓
123
123
未走缓存 ↓
321
清除缓存 ↓
123
未走缓存 ↓
123
321

使用cacheKeyMethod 定义缓存 Key

当使用注解来定义请求缓存时,若要为请求命令指定具体的缓存 Key 生成规则,我们可以使用@CacheResult 的 cacheKeyMethod 属性来指定具体的生成函数;也可以通过使 @CacheKey 注解 在方法参数中指定用于组装缓存 Key 的元素。

需要注意,@CacheKey 注解的优先级比 cacheKeyMethod 的优先级低,如果己经使用 cacheKeyMethod 指定缓存 Key 的生成函数,那么@CacheKey 注解不会生效。


使用 cacheKeyMethod 方法的示例如下,它通过在Hystrix命令的同一个类中定义一个专门生成 Key 的方法,并用@CacheResult 注解的 cacheKeyMethod 方法来指定它即可。

@Slf4j
@Service("orderServiceCache")
public class OrderServiceCacheImpl {

    @Resource
    private RestTemplate restTemplate;

    /**
     * 使用注解请求缓存 方式1
     *
     * @CacheResult 标记这是一个缓存方法,结果会被缓存
     */
    @CacheResult(cacheKeyMethod = "getCacheKey")
    @HystrixCommand(commandKey = "commandKey1")
    public Integer openCacheByAnnotation1(Long id) {
        //此次结果会被缓存
        return restTemplate.getForObject("http://orderservice/hystrix/cache/randomInt", Integer.class);
    }
    /**
     * 第一种方法没有使用@CacheKey注解,而是使用这个方法进行生成cacheKey的替换办法
     * 这里有两点要特别注意:
     * 1、这个方法的入参的类型必须与缓存方法的入参类型相同,如果不同被调用会报这个方法找不到的异常
     * 2、这个方法的返回值一定是String类型
     */
    public String getCacheKey(Long id) {
        return String.valueOf(id);
    }

}
缓存清理

在之前的例子中,我们己经通过 @CacheResult 注解将请求结果置入 Hystrix 的请求缓存之中。如果对缓存的内容进行修改或者删除操作,那么此时请求缓存中的结果与实际结果就会产生不一致(缓存中的结果实际上己经过期失效了),所以我们需要在这类操作上对失效的缓存进行清理。在 Hystrix 的注解配置中, 可以通注解来实现失效缓存的清理,因此需要再加上这个方法:


    /**
     * 使用注解清除缓存 方式1
     *
     * @CacheRemove 必须指定commandKey才能进行清除指定缓存
     */
    @CacheRemove(commandKey = "commandKey1", cacheKeyMethod = "getCacheKey")
    @HystrixCommand
    public void flushCacheByAnnotation1(Long id) {
        log.info("请求缓存已清空!");
        //这个@CacheRemove注解直接用在更新方法上效果更好
    }

需要注意的是,@CacheRemove 注解的 commandKey 属性是必须要指定的,它用来指明需要使用请求缓存的请求命令,因为只有通过该属性的配置,Hystrix 才能找到正确的请求命令缓存位置。

测试控制器

@RestController
@RequestMapping("/hystrix/cache/")
public class HystrixCacheTest {
    // LOGGER
    private static final Logger LOGGER = LoggerFactory.getLogger(HystrixCacheTest.class);

    @Resource
    private OrderServiceCacheImpl service;

    /**
     * 为了请求测试Hystrix请求缓存提供的返回随机数的接口
     */
    @GetMapping("randomInt")
    public Integer getRandomInteger(){
        Random random = new Random();
        int randomInt = random.nextInt(99999);
        return randomInt;
    }

    /**
     * 注解方式请求缓存,第一种
     */
    @GetMapping("/cacheAnnotation1")
    public void openCacheByAnnotation1(){
        //初始化Hystrix请求上下文, 也可以添加过滤器
        // HystrixRequestContext.initializeContext();
        //访问并开启缓存
        Integer result1 = service.openCacheByAnnotation1(1L);
        Integer result2 = service.openCacheByAnnotation1(1L);
        LOGGER.info("first request result is:{} ,and secend request result is: {}", result1, result2);
        //清除缓存
        service.flushCacheByAnnotation1(1L);
        //再一次访问并开启缓存
        Integer result3 = service.openCacheByAnnotation1(1L);
        Integer result4 = service.openCacheByAnnotation1(1L);
        LOGGER.info("first request result is:{} ,and secend request result is: {}", result3, result4);
    }
}

测试:

GET http://localhost:9010/hystrix/cache/cacheAnnotation1

日志输出:

image-20240413233357924


请求合并

微服务架构中的依赖通常通过远程调用实现,而远程调用中最常见的问题就是通信消耗与连接数占用。在高并发的情况之下,因通信次数的增加,总的通信时间消耗将会变得不那么理想。同时,因为依赖服务的线程池资源有限,将出现排队等待与响应延迟的情况。所以在资源有限并且短时间内会产生高并发请求的时候,为避免连接不够用而引起的延迟可以考虑使用请求合并器的方式来处理和优化优化这两个问题,Hystrix 提供了 HystrixCollapser 来实现请求的合并,以减少通 信消耗和线程数的占用。


HystrixCollapser 实现了在 HystrixCommand 之前放置一个合并处理器,将处于一个很短的时间窗(默认 10 毫秒)内对同一依赖服务的多个请求进行整合并以批量方式 发起请求的功能(服务提供方也需要提供相应的批量实现接口)。通过 HystrixCollapser 的封装,开发者不需要关注线程合并的细节过程,只需关注批量化服务和处理。
在快速入门的例子中,我们使用@HystrixCommand 注解优雅地实现了 HystrixCommand 的定义,那么对于请求合并器是否也可以通过注解来定义呢?答案是肯定的!


使用注解进行请求合并
@GetMapping("/findUser/{id}")
@HystrixCollapser(
    batchMethod = "findAll", collapserProperties = {
        @HystrixProperty(name = "timerDelayInMilliseconds", value = "100")
    })
public User findUser(@PathVariable("id") String id) {
    return null;
}

@HystrixCommand()
public List findAll(List ids) {
    User[] forObject = restTemplate.getForObject("http://userservice/user/findAll/{1}", User[].class, StringUtils.join(ids, ","));
    assert forObject != null;
    return Arrays.asList(forObject);
}

我们之前己经介绍过 @HystrixCommand 了,可以看到,这里通过它定义了两个 Hystrix 命令,一个用于请求/users/{id} 接口,一个用于请求/users?ids={ids}接口。

而在请求/users/{id}接口的方法上通过@HystrixCollapser 注解为其创建了合并请求器,通过 batchMethod 属性指定了批量请求的实现方法为 findAll 方法(即请求 /users?ids={ids}接口的命令),同时通过 collapserProperties 属性为合并请求器设置了相关属性,这里使用@HystrixProperty(name=”timerDelayInMilliseconds”, value = ”100”)将合并时间窗设置为 100 毫秒。

这样通过注解简单而又优雅地实现了在/users/{id}依赖服务之前设置了一个批量请求合并器。

测试:

GET http://localhost:9010/consumer/findUser/1
GET http://localhost:9010/consumer/findUser/2
GET http://localhost:9010/consumer/findUser/3
GET http://localhost:9010/consumer/findUser/4
GET http://localhost:9010/consumer/findUser/5
GET http://localhost:9010/consumer/findUser/6

orderservice输出日志:

请求合并的ID: [1]
请求合并的ID: [2]
请求合并的ID: [3]
请求合并的ID: [4]
请求合并的ID: [5]
请求合并的ID: [6]

可见并没有发生请求合并.

为什么请求合并没有生效

需要注意:

  1. @HystrixCollapser中有scope属性,scope的取值为REQUEST, GLOBAL
    • REQUEST范围只对一个request请求内的多次服务请求进行合并
    • GLOBAL是多单个应用中的所有线程的请求中的多次服务请求进行合并。
  2. 同步请求,不会进行请求合并。只有异步请求才会发生请求合并

修改如下:

@RestController
@RequestMapping("/consumer")
public class HystrixConsumerController04 {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/findUser/{id}")
    @HystrixCollapser(
            batchMethod = "findAll", scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL ,collapserProperties = {
            @HystrixProperty(name = "timerDelayInMilliseconds", value = "3000"),
            @HystrixProperty( name = "maxRequestsInBatch", value = "100")
    })
    public User findUser(@PathVariable("id") String id) {
        return null;
    }

    @HystrixCommand
    public List findAll(List ids) {
        System.out.println("请求合并的ID: " + ids);
        User[] forObject = restTemplate.getForObject("http://userservice/user/findAll/{1}", User[].class, StringUtils.join(ids, ","));
        assert forObject != null;
        return Arrays.asList(forObject);
    }

测试, 可以使用线程池来模拟异步请求:

    public static void main(String[] args) throws InterruptedException, IOException {
        CloseableHttpClient httpClient = HttpClients.custom().setMaxConnPerRoute(100).build();
        String url = "http://localhost:9010/consumer/findUser/";
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        int requestCount = 6;
        for(int i = 1;i <= requestCount;i++){
            final int id = i;
            executorService.execute(() -> {
                try{
                    HttpGet httpGet = new HttpGet(url + id);
                    httpGet.addHeader("Content-Type",  "application/json; charset=UTF-8");
                    HttpResponse response = httpClient.execute(httpGet);
                    HttpEntity entity = response.getEntity();
                    InputStream inputStream = entity.getContent();
                    InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
                    BufferedReader bufferedReader = new BufferedReader(reader);
                    String line;
                    StringBuilder stringBuilder = new StringBuilder();
                    while ((line = bufferedReader.readLine())  != null) {
                        stringBuilder.append(line);
                    }
                    String responseBody = stringBuilder.toString();
                    System.out.println(responseBody);
                }catch (Exception e){
                    e.printStackTrace();
                }
            });
        }

        Thread.sleep(1000*10);
        executorService.shutdown();
        httpClient.close();
    }

orderservice输出日志:

请求合并的ID: [2, 6, 5, 3, 1, 4]

main方法输出日志:

{"id":6,"username":"范兵兵","address":"山东省青岛市","serverPort":"9020"}
{"id":2,"username":"文二狗","address":"陕西省西安市","serverPort":"9020"}
{"id":5,"username":"郑爽爽","address":"辽宁省沈阳市大东区","serverPort":"9020"}
{"id":1,"username":"柳岩","address":"湖南省衡阳市","serverPort":"9020"}
{"id":4,"username":"张必沉","address":"天津市","serverPort":"9020"}
{"id":3,"username":"华沉鱼","address":"湖北省十堰市","serverPort":"9020"}
...

请求合并的额外开销

虽然通过请求合并可以减少请求的数量以缓解依赖服务线程池的资源,但是在使用的时候也需要注意它所带来的额外开销:用于请求合并的延迟时间窗会使得依赖服务的请求延迟增高。比如,某个请求不通过请求合并器访问的平均耗时为 5ms,请求合并的延迟时 间窗为 10ms (默认值),那么当该请求设置了请求合并器之后,最坏情况下(在延迟时间 窗结束时才发起请求)该请求需要 15ms 才能完成。


由于请求合并器的延迟时间窗会带来额外开销,所以我们是否使用请求合并器需要根据依赖服务调用的实际情况来选择,主要考虑下面两个方面。

  • 请求命令本身的延迟。如果依赖服务的请求命令本身是一个高延迟的命令,那么可以使用请求合并器,因为延迟时间窗的时间消耗显得微不足道了。
  • 延迟时间窗内的并发量。如果一个时间窗内只有 1〜2 个请求,那么这样的依赖服务不适合使用请求合并器。这种情况不但不能提升系统性能,反而会成为系统瓶颈,因为每个请求都需要多消耗一个时间窗才响应。相反,如果一个时间窗内具有很高 的并发量,并且服务提供方也实现了批量处理接口,那么使用请求合并器可以有效减少网络连接数量并极大提升系统吞吐量,此时延迟时间窗所增加的消耗就可以忽略不计了。

属性详解

@HystrixCommand(fallbackMethod = "fallbackMethod", groupKey = "strGroupCommand", commandKey = "strCommand", threadPoolKey = "strThreadPool",
  commandProperties = {
    // 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离
    @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
    // 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数)
    @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
    // 配置命令执行的超时时间
    @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"),
    // 是否启用超时时间
    @HystrixProperty(name = "execution.timeout.enabled", value = "true"),
    // 执行超时的时候是否中断
    @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"),
    // 执行被取消的时候是否中断
    @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"),
    // 允许回调方法执行的最大并发数
    @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"),
    // 服务降级是否启用,是否执行回调函数
    @HystrixProperty(name = "fallback.enabled", value = "true"),

    // 是否启用断路器
    @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
    // 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。
    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
    // 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过 circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50, 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。
    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
    // 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后,会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态,如果成功就设置为 "关闭" 状态。
    @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"),
    // 断路器强制打开
    @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
    // 断路器强制关闭
    @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),

    // 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间
    @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"),
    // 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。
    // 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常
    @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
    // 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。
    @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
    // 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
    @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
    // 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。
    @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
    // 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,
    // 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行,
    // 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
    @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
    // 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。
    @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"),

    // 是否开启请求缓存
    @HystrixProperty(name = "requestCache.enabled", value = "true"),
    // HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中
    @HystrixProperty(name = "requestLog.enabled", value = "true"),
  },
  threadPoolProperties = {
    // 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量
    @HystrixProperty(name = "coreSize", value = "10"),
    // 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列,否则将使用 LinkedBlockingQueue 实现的队列。
    @HystrixProperty(name = "maxQueueSize", value = "-1"),
    // 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。
    // 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。
    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"),
  }
)
public String strConsumer() {
 ResponseEntity result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class);
 return result.getBody();
}

各属性配置指南 https://www.cnblogs.com/throwable/p/11961016.html

在之前介绍 Hystrix 的使用方法时,己经涉及过一些 Hystrix 属性的配置,我们可以根据实现 HystrixCommand 的不同方式将配置方法分为如下两类:

  • 当通过继承的方式实现时,可使用 Setter 对象来对请求命令的属性进行设置

  • 当通过注解的方法实现时,只需使用@HystrixCommand 中的 commandProperties 属性来设置


实际上,Hystrix 为我们提供的配置内容和配置方式远不止上面这些,它提供了非常丰富和灵活的配置方法,下面我们将以注解为例,详解介绍各项配置属性。在具体说明这些属性之前,我们需要了解一下这些属性都存在下面 4 个不同优先级别的配置(优先级由低到高)

  • 全局默认值:如果没有设置下面三个级别的属性,那么这个属性就是默认值。由于该属性通过代码定义,所以对于这个级别,我们主要关注它在代码中定义的默认值即可。

  • 全局配置属性:通过在配置文件中定义全局属性值,在应用启动时或在与 SpringCloud Config 和 Spring Cloud Bus 实现的动态刷新配置功能配合下,可以实现对“全局默认值”的覆盖以及在运行期对“全局默认值”的动态调整。


  • 实例默认值:通过代码为实例定义的默认值。通过代码的方式为实例设置属性值来覆盖默认的全局配置。

  • 实例配置属性:通过配置文件来为指定的实例进行属性配置,以覆盖前面的三个默认值。

  • 通过理解 Hystrix 4 个级别的属性配置,对设置 Hystrix 的默认值以及在线上如何根据 实际情况去调整配置非常有帮助,下面我们来具体看看它有哪些具体的属性配置。


commandProperties 属性

Command 属性主要用来控制 HystrixCommand 命令的行为。

它主要有下面 5 种不同类型的属性配置。


1.execution 配置
  • execution 配置控制的是 HystrixCommand.run()的执行。

  • execution.isolation.strategy:该属性用来设置 HystrixCommand. run ()执行的隔离策略,它有如下两个选项:

    • THREAD:通过线程池隔离的策略。它在独立的线程上执行,并且它的并发限制受线程池中线程数量的限制。
    • SEMAPHORE:通过信号量隔离的策略。它在调用线程上执行,并且它的并发限制受信号量计数的限制。

属性级别 默认值、配置方式、配置属性
全局默认值 THREAD
全局配置属性 hystrix.command.default.execution.isolation.strategy
实例默认值 可通过注解设置: @HystrixProperty(name="execution.isolation.strategy",value="THREAD")
实例配置属性 hystrix.command.[HystrixCommandKey].execution.isolation.strategy

execution.isolation.thread,timeoutInMilliseconds:该属性用来配置 HystrixCommand 执行的超时时间,单位为毫秒。当 HystrixCommand 执行时间超过该配置值之后,Hystrix 会将该执行命令标记为 TIMEOUT 并进入服务降级处理逻辑。

属性级别 默认值、配置方式、配置属性
全局配置属性 hystrix.command.default.execution<br>.isolation.thread.timeoutInMilliseconds
实例默认值 可通过注解设置, 例如@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1000")
实例配置属性 hystrix.command.[HystrixCommandKey].execution<br>.isolation.thread.timeoutInMilliseconds

execution.timeout.enabled:该属性用来配置 HystrixCommand.run()的执行是否启用超时时间。默认为 true,如果设置为 false,那么 execution.Isolation.thread.timeoutInMilliseconds 属性的配置将不再起作用。

属性级别 默认值、配置方式、配置属性
全局默认值 true
全局配置属性 hystrix.command.default.execution.timeout.enabled
实例默认值 可通过@HystrixProperty(name="execution.timeout.enabled",value="false")注解设置
实例配置属性 hystrix.command.[HystrixCommandKey].execution.timeout.enabled

execution.isolation.thread.interruptOnTimeout:该属性用来配置当 HystrixCommand.run()执行超时的时候是否要将它中断

属性级别 默认值、配置方式、配置属性
全局默认值 true
全局配置属性 hystrix.command.default.execution.isolation.thread.interruptOnTimeout
实例默认值 可通过@HystrixProperty(name="execution.isolation.thread.interruptOnTimeout",value="false")注解设置
实例配置属性 hystrix.command.[HystrixCommandKey].execution.isolation.thread.interruptOnTimeout

execution.Isolation.semaphore.maxConcurrentRequests: 当 HystrixCommand 的隔离策略使用信号量的时候,该属性用来配置信号量的大小(并发请求数)。当最大并发请求数达到该设置值时,后续的请求将会被拒绝。

属性级别 默认值、配置方式、配置属性
全局默认值 10
全局配置属性                 hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests
实例默认值 可通过注解设置:
@HystrixProperty(name="execution.isolation.semaphore.maxConcurrentRequests",value="2")
实例配置属性 hystrix.command.[HystrixCommandKey].execution.isolation.semaphore.maxConcurrentRequests

2.fallback 配置

下面这些属性用来控制 HystrixCommand.getFallback()的执行。这些属性同时适用于线程池的信号量的隔离策略。


fallback.isolation.semaphore.maxConcurrentRequests:该属性用来设置从调用线程中允许 HystrixCommand.getFallback()方法执行的最大并发请求数。当达到最大并发请求数时,后续的请求将会被拒绝并抛出异常(因为它己经没有后续的 fallback 可以被调用了)。

属性级别 默认值、配置方式、配置属性
全局默认值 10
全局配置属性                 hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests
实例默认值 可通过注解设置: @HystrixProperty(name="fallback.isolation.semaphore.maxConcurrentRequests",value="20")
实例配置属性 hystrix.command.HystrixCommandKey.fallback.isolation.semaphore.maxConcurrentRequests

fallback.enabled:该属性用来设置服务降级策略是否启用,如果设置为 false,那么当请求失败或者拒绝发生时,将不会调用 HystrixCommand.getFallback() 来执行服务降级逻辑。

属性级别 默认值、配置方式、配置属性
全局默认值 true
全局配置属性 hystrix.command.default.fallback.enabled
实例默认值 可通过注解设置@HystrixProperty(name="fallback.enabled",value="false")
实例配置属性 hystrix.command.[HystrixCommandKey].fallback.enabled

3.circuitBreaker 配置

下面这些是断路器的属性配置,用来控制 HystrixCircuitBreaker 的行为。


  • circuitBreaker.enabled:该属性用来确定当服务请求命令失败时,是否使用断路器来跟踪其健康指标和熔断请求。
属性级别 默认值、配置方式、配置属性
全局默认值 true
全局配置属性 hystrix.command.default.circuitBreaker.enabled
实例默认值 可通过注解设置: @HystrixProperty(name="circuitBreaker.enabled",value="false")
实例配置属性 hystrix.command.[HystrixCommandKey].circuitBreaker.enabled

  • circuitBreaker.requestVolumeThreshold:该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,如果滚动时间窗(默 认 10 秒)内仅收到了 19 个请求,即使这 19 个请求都失败了,断路器也不会打开。
属性级别 默认值、配置方式、配置属性
全局默认值 20
全局配置属性 hystrix.command.default.circuitBreaker.requestVolumeThreshold
实例默认值 可通过注解设置: @HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="30")
实例配置属性 hystrix.command.HystrixCommandKey.circuitBreaker.requestVolumeThreshold

  • circuitBreaker.sleepWindowInMilliseconds:该属性用来设置当断路器 打开之后的休眠时间窗。休眠时间窗结束之后,会将断路器置为“半幵”状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为“打开”状态,如果成功 就设置为“关闭”状态。
属性级别 默认值、配置方式、配置属性
全局默认值 5000
全局配置属性
实例默认值 可通过@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value="3000")注解设置
实例配置属性 hystrix.command.[HystrixCommandKey].circuitBreaker.sleepWindowInMilliseconds

  • circuitBreaker.errorThresholdPercentage:该属性用来设置断路器打开的错误百分比条件。例如,默认值为 50%的情况下,表示在滚动时间窗中,在请求数量超过 circuitBreaker.requestVolumeThreshold 阈值的前提下,如果错误请求数的百分比超过 50,就把断路器设置为“打开”状态,否则就设置为“关闭”状态。
属性级别 默认值、配置方式、配置属性
全局默认值 50
全局配置属性 hystrix.command.default.circuitBreaker.errorThresholdPercentage
实例默认值 可通过@HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value="40")注解设置
实例配置属性 hystrix.command.[HystrixCommandKey].circuitBreaker.errorThresholdPercentage

  • circuitBreaker.forceOpen:如果将该属性设置为 true,断路器将强制进入“打开”状态,它会拒绝所有请求。该属性优先于 circuitBreaker.forceClosed 属性。
属性级别 默认值、配置方式、配置属性
全局默认值 false
全局配置属性 hystrix.command.default.circuitBreaker.forceOpen
实例默认值 可通过注解设置: @HystrixProperty(name="circuitBreaker.forceOpen",value="true")
实例配置属性 hystrix.command.[HystrixCommandKey].circuitBreaker.forceOpen

  • circuitBreaker. forceClosed:如果将该属性设置为 true,断路器将强制进入 “关闭”状态,它会接收所有请求。如果 circuitBreaker.forceOpen 属性为 true,该属性不会生效。
属性级别 默认值、配置方式、配置属性
全局默认值 false
全局配置属性 hystrix.command.default.circuitBreaker.forceClosed
实例默认值 可通过注解设置: @HystrixProperty(name="circuitBreaker.forceClosed",value="true")
实例配置属性 hystrix.command.[HystrixCommandKey].circuitBreaker.forceClosed

4.metrics 配置

下面的属性均与 HystrixCommand 和 HystrixObservableCommand 执行中捕获的指标信息有关。


  • metrics.rollingstats.timelnMilliseconds:该属性用来设置滚动时间窗的长度,单位为毫秒。该时间用于断路器判断健康度时需要收集信息的持续时间。断路器在收集指标信息的时候会根据设置的时间窗长度拆分成多个“桶”来累计各度量值,每个“桶”记录了一段时间内的采集指标。例如,当采用默认值 10000 毫 秒时,断路器默认将其拆分成 10 个桶(桶的数量也可通过 metrics.rollingStats.numBuckets 参数设置),每个桶记录 1000 毫秒内的指标信息。
属性级别 默认值、配置方式、配置属性
全局默认值 10000
全局配置属性 hystrix.command.default.metrics.rollingStats.timeInMilliseconds
实例默认值 可通过注解设置: @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds",value="20000")
实例配置属性 hystrix.command.[HystrixCommandKey].metrics.rollingStats.timeInMilliseconds

注意:

该属性从 Hystrix 1.4.2 版本开始(Brixton.SR5 版本中使用了 1.5.3 版本),只有在应用初始化的时候生效,通过动态刷新配置不会产生效果,这样做是为了避免出现运行期检测数据丢失的情况。


  • metrics.rollingStats.numBuckets:该属性用来设置滚动时间窗统计指标信息时划分“桶”的数量。
属性级别 默认值、配置方式、配置属性
全局默认值 10
全局配置属性 hystrix.command.default.metrics.rollingStats.numBuckets
实例默认值 可通过注解设置 @HystrixProperty(name="metrics.rollingStats.numBuckets",value="20")
实例配置属性 hystrix.command.[HystrixCommandKey].metrics.rollingStats.numBuckets

注意:

metrics.rollingStats.timeInMilliseconds 参数的设置必须能够被 metrics.rollingStats.numBuckets 参数整除,不然将拋出异常。该参数与 metrics.rollingStats.timelnMilliseconds —样,从 Hystrix 1.4.12 版本开始(Brixton.SR5 版本中使用了 1.5.3 版本),只有在应用初始化的时候生效,通过动态刷新配置不会产生效果,这样做是为了避免出现运行期监测数据丢失的情况。


  • metrics.rollingPercentile.enabled:该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false,那么所有的概要统计都将返回-1
属性级别 默认值、配置方式、配置属性
全局默认值 true
全局配置属性 hystrix.command.default.metrics.rollingPercentile.enabled
实例默认值 可通过注解设置@HystrixProperty(name="metrics.rollingPercentile.enabled",value="false")
实例配置属性 hystrix.command.[HystrixCommandKey].metrics.rollingPercentile.enabled

  • metrics.rollingPercentile.timelnMilliseconds:该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
属性级别 默认值、配置方式、配置属性
全局默认值 60000
全局配置属性 hystrix.command.default.metrics.rollingPercentile.timelnMilliseconds
实例默认值 可通过注解设置@HystrixProperty(name="metrics.rollingPercentile.timelnMilliseconds",value="50000")
实例配置属性 hystrix.command.[HystrixCommandKey].metrics.rollingPercentile.timelnMilliseconds

注意:

该属性从 Hystrix 1.4.12 版本开始(Brixton.SR5 版本中使用了 1.5.3 版本),只有在应用初始化的时候生效,通过动态刷新配置不会产生效果,这样做是为了避免出现运行期监测数据丢失的情况。


  • metrics.rollingPercentile.numBuckets:该属性用来设置百分位统计滚动窗口中使用“桶”的数量。
属性级别 默认值、配置方式、配置属性
全局默认值 6
全局配置属性 hystrix.command.default.metrics.rollingPercentile.numBuckets
实例默认值 可通过注解设置@HystrixProperty(name="metrics.rollingPercentile.numBuckets",value="5")
实例配置属性 hystrix.command.[HystrixCommandKey].metrics.rollingPercentile.numBuckets

注意:

metrics.rollingPercentile.timelnMilliseconds 参数的设置必须能够被 metrics.rollingPercentile.numBuckets 参数整除,不然将会抛出异常。该属性从 Hystrix 1.4.12 版本开始(Brixton.SR5 版本中使用了 1.5.3 版本),只有在应用初始化的时候生效,通过动态刷新配置不会产生效果,这样做 是为了避免出现运行期监测数据丢失的情况。


  • metrics.rollingPercentile.bucketSize:该属性用来设置在执行过程中 每个“桶”中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,就从最初的位置开始重写。例如,将该值设置为 100,滚动窗口为 10 秒,若在 10 秒内一个“桶”中发生了 500 次执行,那么该“桶”中只保留最后的 100 次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
属性级别 默认值、配置方式、配置属性
全局默认值 100
全局配置属性 hystrix.command.default.metrics.rollingPercentile.bucketSize
实例默认值 可通过注解设置@HystrixProperty(name="metrics.rollingPercentile.bucketSize",value="120")
实例配置属性 hystrix.command.[HystrixCommandKey].metrics.rollingPercentile.bucketSize

注意:
该属性从 Hystrix 1.4.12 版本开始(Brixton.SR5 版本中使用了 L5.3 版本),只有在应用初始化的时候生效,通过动态刷新配置不会产生效果,这样做是为了避免出现运行期监测数据丢失的情况。


  • metrics.healthSnapshot.intervalInMilliseconds:该属性用来设置采集影响断路器状态的健康快照(请求的成功、错误百分比)的间隔等待时间。
属性级别 默认值、配置方式、配置属性
全局默认值 500
全局配置属性 hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds
实例默认值 可通过注解设置@HystrixProperty(name="metrics.healthSnapshot.intervalInMilliseconds",value="600")
实例配置属性 hystrix.command.[HystrixCommandKey].metrics.healthSnapshot.intervalInMilliseconds

5.requestContext 配置

下面这些属性涉及 HystrixCommand 使用的 HystrixRequestContext 的设置。


  • requestCache.enabled:此属性用来配置是否开启请求缓存。
属性级别 默认值、配置方式、配置属性
全局默认值 true
全局配置属性 hystrix.command.default.requestCache.enabled
实例默认值 可通过注解设置@HystrixProperty(name="requestCache.enabled",value="false")
实例配置属性 hystrix.command.[HystrixCommandKey].requestCache.enabled

  • requestLog.enabled:该属性用来设置 HystrixCommand 的执行和事件是否打印日志到 HystrixRequestLog 中。
  • collapser 属性该属性除了在代码中用 set 和配置文件配置之外,也可使用注解进行配置。可使用@HystrixCollapser 中的 collapserProperties 属性来设置:
@HystrixCollapser (batchMethod ="batch1",collapserProperties ={
    @HystrixProperty(name="timerDelayInMilliseconds", value ="20")
})

下面这些属性用来控制命令合并相关的行为。

  • maxRequestsInBatch:该参数用来设置一次请求合并批处理中允许的最大请求数
属性级别 默认值、配置方式、配置属性
全局默认值 Integer.MAX_VALUE
全局配置属性 hystrix.collapser.default.maxRequestsInBatch
实例默认值 可通过注解设置@HystrixProperty(name="maxRequestsInBatch",value="100000")
实例配置属性 hystrix.collapser.[HystrixCollapserKey].maxRequestsInBatch

  • timerDelaylnMilliseconds:该参数用来设置批处理过程中每个命令延迟的时间,单位为毫秒。
属性级别 默认值、配置方式、配置属性
全局默认值 10
全局配置属性 hystrix.collapser.default.timerDelaylnMilliseconds
实例默认值 可通过注解设置@HystrixProperty(name="timerDelaylnMilliseconds",value="20")
实例配置属性 hystrix.collapser.[HystrixCollapserKey].timerDelaylnMilliseconds

threadPool 属性

该属性除了在代码中用 set 和配置文件配置之外,还可使用注解进行配置。可使用@HystrixCommand 中的 threadPoolProperties 属性来设置,比如:

@HystrixCommand(fallbackMethod ="helloFallback" commandKey = "helloKey",threadPoolProperties = {
    @HystrixProperty(name="coreSize",value ="20")
})

下面这些属性用来控制 Hystrix 命令所属线程池的配置。


  • coreSize:该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量。
属性级别 默认值、配置方式、配置属性
全局默认值 10
全局配置属性 hystrix.threadpool.default.coreSize
实例默认值 可通过注解设置@HystrixProperty(name="coreSize",value="16")
实例配置属性 hystrix.threadpool.[hystrixThreadPoolKey].coreSize

  • maxQueueSize:该参数用来设置线程池的最大队列大小。当设置为-1 时,线程池将使用 SynchronousQueue 实现的队列,否则将使用 LinkedBlockingQueue 实现的队列。
属性级别 默认值、配置方式、配置属性
全局默认值 -1
全局配置属性 hystrix.threadpool.default.maxQueueSize
实例默认值 可通过注解设置@HystrixProperty(name="maxQueueSize",value="10")
实例配置属性 hystrix.threadpool.[hystrixThreadPoolKey].maxQueueSize

注意:

该属性只有在初始化的时候才有用,无法通过动态刷新的方式来调整。


  • queueSizeRejectionThreshold:该参数用来为队列设置拒绝阈值。通过该参数,即使队列没有达到最大值也能拒绝请求。该参数主要是对 LinkedBlocking- Queue 队列的补充,因为 LinkedBlockingQueue 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。
属性级别 默认值、配置方式、配置属性
全局默认值 5
全局配置属性 hystrix.threadpool.default.queueSizeRejectionThreshold
实例默认值 可通过注解设置@HystrixProperty(name="queueSizeRejectionThreshold",value="10")
实例配置属性 hystrix.threadpool.[hystrixThreadPoolKey].queueSizeRejectionThreshold

注意:
当 maxQueueSize 属性为-1 的时候,该属性不会生效。


活动 5.1: Hystrix 容错保护

Hystrix 仪表盘

通过之前的内容,我们己经体验到了 Spring Cloud 对 Hystrix 的优雅整合。除此之外,SpringCloud 还完美地整合了它的仪表盘组件 Hystrix Dashboard,它主要用来实时监控 Hystrix 的各项指标信息。通过 Hystrix Dashboard 反馈的实时信息,可以帮助我们快速发现系统中存在的问题,从而及时地采取应对措施。


本节中我们将在 Hystrix 入门例子的基础上,构建一个 Hystrix Dashboard 来对 Order-Service 服务实现监控。

在 Spring Cloud 中构建一个 Hystrix Dashboard 非常简单,只需要下面 4 步:

  1. 创建一个标准的 Spring Boot 工程,命名为 hystrix-dashboard。

  2. 编辑 pom.xml,具体依赖内容如下所示:




    org.springframework.cloud
    spring-cloud-starter-netflix-hystrix



    org.springframework.cloud
    spring-cloud-starter-netflix-hystrix-dashboard



    org.springframework.boot
    spring-boot-starter-actuator

  1. 为应用主类加上@EnableHystrixDashboard,启用 Hystrix Dashboard 功能。

  2. 根据实际情况修改 application.properties 配置文件,比如选择一个未被占用的端口等,此步不是必需的。

    server:
     port: 2022

到这里我们己经完成了基本配置,接下来可以启动该应用,并访问 http://localhost:2022/hystrix。可以看到如下页面:


image-20220724232410110


这是 Hystrix Dashboard 的监控首页,Hystrix Dashboard 共支持三种不同的监控方式,如下所示。

  • 默认的集群监控:通过 URL http://turbine-hostname:port/turbine.stream 开启,实现对默认集群的监控。
  • 指定的集群监控:通过 URL http://turbine-hostname:port/turbine.stream?cluster=[clusterName]开启,实现对 clusterName 集群的监控。
  • 单体应用的监控:通过 URL http://hystrix-app:port/hystrix.stream 开启,实现对具体某个服务实例的监控。

前两者都是对集群的监控,需要整合 Turbine 才能实现,这部分内容我们将在下一节中做详细介绍。在本节中,我们主要实现对单个服务实例的监控,这里我们先来实现单个服务实例的监控。


既然 Hystrix Dashboard 监控单实例节点需要通过访问实例的/hystrix. stream 接口 来实现,我们自然需要为服务实例添加这个端点,而添加该功能的步骤也同样简单,只需要下面三步,我们以 orderservice 服务(默认端口 8080)为例进行演示:


  • 在服务实例 pom.xml 中的 dependencies 节点中新增 spring-boot-starter-actuator 监控模块以开启监控相关的端点,并确保己经引入断路器的依赖 spring-cloud-starter-hystrix

    
    
      org.springframework.cloud
      spring-cloud-starter-netflix-hystrix
    
    
    
      org.springframework.boot
      spring-boot-starter-actuator
    

  • 确保在服务实例的主类中己经使用@EnableCircuitBreaker 注解,开启了断路器功能。

  • 在配置文件中开启端点功能

    #暴露全部的监控信息
    management:
    endpoints:
      web:
        exposure:
          include: "*"

    这里使用了 management.endpoints.web.exposure.include 属性来开启所有的端点,也可以通过 management.endpoints.web.exposure.exclude 属性来关闭某些端点。具体的端点信息可以参考 Spring Boot 的官方文档。"*" 表示开启所有端点,也可以使用具体的端点名称,比如 "hystrix.stream"。


到这里己经完成了所有的配置,在 Hystrix Dashboard 的首页还有另外两个参数。

  • Delay:该参数控制服务器上轮询监控信息的延迟时间,默认为 2000 毫秒,可以通过配置该属性来降低客户端的网络和 CPU 消耗。
  • Title:该参数对应了下图头部标题 Hystrix Stream 之后的内容,默认会使用具体监控实例的 URL,可以通过配置该信息来展示更合适的标题。

输入 http://localhost:8080/actuator/hystrix.stream,填写 Title,单击 Monitor Stream 按钮,可以看到如下页面。

image-20220724233454538


回到监控页面,我们来详细说说其中各元素的具体含义。

  • 可以在监控信息的左上部找到两个重要的图形信息:一个实心圆和一条曲线。
    • 实心圆:其有两种含义。通过颜色的变化代表了实例的健康程度,如下图所示,它的健康度从绿色、黄色、橙色、红色递减。该实心圆除了颜色的变化之外, 它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,我们可以在大量的实例中快速发现故障实例和高压力实例。
    • 曲线:用来记录 2 分钟内流量的相对变化,可以通过它来观察流量的上升和下降趋势。

image-20220724234005430


  • 其他一些数量指标如下图所示。
    • 实心圆: 代表了最近 10 秒内请求的总数。其颜色代表了请求的健康程度,如下图所示,它的健康度从绿色、黄色、橙色、红色递减。
    • 七种颜色: 代表了最近 10 秒内请求的状态,分别是:绿色代表成功,黄色代表超时,橙色代表拒绝,红色代表失败,灰色代表断路器打开,蓝色代表短路,紫色代表线程池拒绝。
    • 曲线: 代表了最近 10 秒内请求的相对变化,可以通过它来观察请求的上升和下降趋势。
    • Circle Breaker: 代表了最近 10 秒内断路器的打开和关闭情况,当断路器打开时,该实心圆会变成红色,当断路器关闭时,该实心圆会变成绿色。

bg right fit


通过本节内容我们己经能够使用 Hystrix Dashboard 来对单个实例做信息监控了,但是在分布式系统中,往往有非常多的实例需要去维护和监控。到目前为止,我们能做的就是通过开启多个窗口来监控多个实例,很显然这样的做法并不合理。在下一节中,我们将介绍利用 Turbine 和 HystrixDashboard 配合实现对集群的监控。


活动 5.2 : 基于图形化的 DashBoard(仪表板)监控


Turbine 集群监控

在上一节介绍 Hystrix Dashboard 的首页时,我们提到过除了可以开启单个实例的监控 页面之外,还有一个监控端点/turbine . stream 是对集群使用的。从端点的命名中,可猜测到这里我们将引入 Turbine,通过它来汇集监控信息,并将聚合后的信息提供给 Hystrix Dashboard 来集中展示和监控。


构建监控聚合服务

下面我们将在上一节内容的基础上做一些扩展,通过引入 Turbine 来聚合多个服务的监控信息,并在 Hystrix Dashboard 上面展示。

具体实现步骤如下:

  1. 创建一个标准的 Spring Boot 工程,命名为 hystrix-turbine。

  1. 编辑 pom.xml,引入相关依赖。

    
       
           org.springframework.cloud
           spring-cloud-starter-netflix-turbine
           
           
               
                   org.springframework.cloud
                   spring-cloud-starter-netflix-eureka-client
               
           
       
       
           org.springframework.cloud
           spring-cloud-starter-netflix-hystrix
       
       
           org.springframework.cloud
           spring-cloud-starter-netflix-hystrix-dashboard
       
    
       
       
           com.alibaba.cloud
           spring-cloud-starter-alibaba-nacos-discovery
       
    

  1. 创建应用主类 TurbineApplication,并使用@EnableTurbine 注解幵启 Turbine。

    @SpringBootApplication
    @EnableHystrixDashboard
    @EnableTurbine
    public class TurbineApplication {
       public static void main(String[] args) {
           SpringApplication.run(TurbineApplication.class,args);
       }
    }

  1. 在 application.yml 配置文件中加入 Nacos 和 Turbine 的相关配置

    server:
     port: 2023
    
    spring:
     application:
       name: turbine
     cloud:
       nacos:
         server-addr: localhost:8848 #nacos 服务端地址
    
    turbine:
     app-config: orderservice,userservice #监控服务列表
     aggregator:
       cluster-config: default #聚合集群名称
     cluster-name-expression: new String('default') #聚合集群名称
     combine-host-port: true
    
    hystrix:
     dashboard:
       proxy-stream-allow-list:
         - "localhost"
    
    #hystrix dashboard用,actuator暴露端口
    management:
     endpoints:
       web:
         exposure:
           include: "hystrix.stream,turbine.stream"
         cors:
           allowed-origins: "*"
           allowed-methods: "*"

  • turbine.app-config: 指定需要监控的服务列表,多个服务之间用逗号分隔。
  • turbine.cluster-name-expression : 指定聚合集群名称, 该
    参数指定了集群名称为 default,当服务数量非常多的时候,可以启动多个 Turbine 服务来构建不同的聚合集群,而该参数可以用来区分这些不同的聚合集群,同时该参数值可以在 Hystrix 仪表盘中用来定位不同的聚合集群,只需在 Hystrix Stream 的 URL 中通过 cluster 参数来指定;
  • turbine.combine-host-port: 此参数设置为 true 可以让同一主机上的服务通过主机名与端口号的组合来进行区分,默认情况下会以 host 来区分不同的服务,这 会使得在本地调试的时候,本机上的不同服务聚合成一个服务来统计。

在完成了上面的内容构建之后,我们来体验一下 Turbine 对集群的监控能力。分别启动 nacos-server、orderservice、userservice、Turbine 以及 Hystrix Dashboard。访问 HystrixDashboard,并开启对 http://localhost:2023/turbine.stream 的监控,我们可以看到如下页面:


image-20220725171915058

监控面板

在断路器原理的介绍中,我们多次提到关于请求命令的度量指标的判断。这些度量指标都是HystrixCommand 和 HystrixObservableCommand 实例在执行过程中记录的重要信息,它们除了在Hystrix 断路器实现中使用之外,对于系统运维也有非常大的帮助。 这些指标信息会以“滚动时间窗”与“桶”结合的方式进行汇总,并在内存中驻留一段时间,以供内部或外部进行查询使用,Hystrix 仪表盘就是这些指标内容的消费者之一。

通过之前的内容,我们己经体验到了 Spring Cloud 对 Hystrix 的优雅整合。除此之外,SpringCloud 还完美地整合了它的仪表盘组件 Hystrix Dashboard,它主要用来实时监控 Hystrix 的各项指标信息。通过 Hystrix Dashboard 反馈的实时信息,可以帮助我们快速发现系统中存在的问题,从而及时地采取应对措施。


本节中我们将在 Hystrix 入门例子的基础上,构建一个 Hystrix Dashboard项目 来对 Order-Service 服务实现监控。

通过actuator端点产生统计信息

微服务如果想要被监控, 需要暴露相应端点, 这里为了方便暴露了所有端点.

#暴露全部的监控信息
management:
  endpoints:
    web:
      exposure:
        include: "*"

这里使用了 management.endpoints.web.exposure.include 属性来开启所有的端点,也可以通过 management.endpoints.web.exposure.exclude 属性来关闭某些端点。具体的端点信息可以参考 Spring Boot 的官方文档。"*" 表示开启所有端点,也可以使用具体的端点名称,比如 "hystrix.stream"。

另外因为 springboot2.0起监控的默认路径不是 "/hystrix.stream", 需要在自己的项目里面手动配置映射路径:

@Configuration
public class OrderServiceConfiguration {

    @Bean
    public ServletRegistrationBean getServlet(){
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean<>(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/actuator/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");

        return registrationBean;
    }
}

以orderservice为例进行测试:

http://localhost:9010/actuator/hystrix.stream

image-20240407160515165

userservice也是一样.

创建turbine项目聚合统计信息

如果需要聚合多个微服务信息显示在面板上, 需要是创建一个turbine项目, 并进行统计信息的聚合:



    4.0.0
    com.example
    my-turbine
    0.0.1-SNAPSHOT
    my-turbine
    my-turbine

    
        com.niit
        chapter05
        1.0
    

    
        
            org.springframework.cloud
            spring-cloud-starter-netflix-turbine
            
            
                
                    org.springframework.cloud
                    spring-cloud-starter-netflix-eureka-client
                
            
        
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        
    

由于turbine需要借助nacos获取服务信息, 需要使用spring-cloud-starter-alibaba-nacos-discovery依赖.

启动类:

@SpringBootApplication
@EnableHystrix
@EnableTurbine // 开启turbine, 用于聚合监控数据从而实现集群监控
public class MyTurbineApplication {

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

}

配置:

server:
  port: 2023

spring:
  application:
    name: turbine
  cloud:
    nacos:
      server-addr: localhost:8848 #nacos 服务端地址
      discovery:
        namespace: a52af3ff-ee9b-4c83-b6af-1e9fe53258e4 # 处于同一个命名空间,才能进行监控
        ephemeral: false

turbine:
  cluster-name-expression: new String('default') #聚合集群名称, default 为默认值
  aggregator: #聚合器
    # 指定聚合哪些集群,多个使用","分割,默认为default。可使用http://.../turbine.stream?cluster={clusterConfig之一}访问
    cluster-config: default #聚合集群名称, default 为默认值
  app-config: orderservice,userservice #监控服务列表, 需要从nacos中获取
  combine-host-port: true #是否将主机名和端口号组合在一起来区分不同的实例, 默认仅使用主机名区分

访问聚合信息http://localhost:2023/turbine.stream:

image-20240407162249822

创建dashboard项目实现监控

新创建一个项目 my-dashboard , 用于展示单体项目或者聚合项目的统计信息:



    4.0.0
    com.example
    my-dashboard
    0.0.1-SNAPSHOT
    my-dashboard
    my-dashboard
    
        com.niit
        chapter05
        1.0
    

    
        
        
            org.springframework.cloud
            spring-cloud-starter-netflix-hystrix
        
        
        
            org.springframework.cloud
            spring-cloud-starter-netflix-hystrix-dashboard
        
    

启动类:

@SpringBootApplication
@EnableHystrixDashboard
public class MyDashboardApplication {

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

}

配置:

server:
  port: 2022

hystrix:
  dashboard:
    proxy-stream-allow-list: '*'  #允许任何 IP 地址访问仪表板

启动项目后, 访问:http://localhost:2022/hystrix

image-20240407162949781

这是 Hystrix Dashboard 的监控首页,Hystrix Dashboard 共支持三种不同的监控方式,如下所示。

  • 单体服务的监控:通过 URL http://hystrix-app:port/hystrix.stream 开启,实现对具体某个服务实例的监控。
  • 集群监控:通过 URL http://turbine-hostname:port/turbine.stream?cluster=[clusterName]开启,实现对 clusterName 集群的监控。

前者是对集群的监控,需要整合 Turbine 才能实现,后者是对单个服务实例的监控。

监控单体项目:

比如监控orderservice

在上面输入框输入http://localhost:9010/actuator/hystrix.stream

image-20240407163155609

监控聚合项目:

在上面输入框输入http://localhost:2023/turbine.stream, 点击Monitor Stream按钮开始监控:

image-20240413234750492

解读监控内容:

一开始是没有监控信息的, 直到请求发生:

image-20220725171915058

各个指标的含义:

bg right fit

熔断如果触发, 则可以看到红色圆圈:

  • 实心圆:其有两种含义。通过颜色的变化代表了实例的健康程度,绿色表示正常,红色表示熔断。该实心圆除了颜色的变化之外, 它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,我们可以在大量的实例中快速发现故障实例和高压力实例。

  • Circuit:表示熔断器的状态, Closed(绿字)表示熔断器闭合接收请求, Open(红字)表示熔断器打开走熔断降级流程. 正常的情况实心圆是淡绿色, 熔断器显示闭合; 当熔断发生时, 实心圆红色, 熔断器打开;当熔断器处于半开状态时, 实心圆回复淡绿色, 熔断器显示打开.

  • 曲线:代表时间窗口内流量变化,可以通过它来观察流量的上升和下降趋势。

  • 六个数字代表不同状态的请求数, 灰色百分号为失败率.

不同颜色代表的含义:

image-20240407170903329

其他文字代表的含义:

  • Cluster 集群每秒请求数

  • Host 主机每秒请求数

  • Hosts 主机数量

  • Median 响应时间的中位数, Mean 平均延迟, 90th 表示90百分位的延迟情况.

可以点击sort右边的排序字段进行排序:

image-20240407170959357

Views: 14

SpringCloud与微服务-第4章-客户端负载均衡(Ribbon)

第 4 章 客户端负载均衡(Ribbon)


负载均衡

众所周知,随着用户量的增加,应用的访问量也会随之增加,单台服务器已经远不能满足高并发的业务需求,这时就需要多台服务器组成集群来应对高并发带来的业务压力,同时也需要负均衡器来对流量进行合理分配。


负载均衡是一种基础的网络服务,它的核心原理是按照指定的负载均衡算法,将请求分配到后端服务集群上,从而为系统提供并行处理和高可用的能力。


负载均衡的方式有很多种,在 Spring Cloud 体系中,Ribbon 就是负载均衡的组件,所有的请求都是通过 Ribbon 来选取对应的服务信息的。


在活动 3.1 的试验中, 我们已经学习了如何使用 Spring Cloud Alibaba Nacos 来实现服务注册与发现。在存在多个服务实例的情况下,使用固定主机名和 ip 的方式来访问服务,会存在单点故障的问题,因此,我们需要使用负载均衡的方式来访问服务。


@Resource
private DiscoveryClient discoveryClient;//查找服务

@GetMapping("/info/{sid}")
public ResponseEntity> getCourseBySid(@PathVariable Integer sid){
    ServiceInstance serviceInstance = discoveryClient.getInstances("mycourse").get(0);
    //拼接url
    String url = "http://"+serviceInstance.getHost()+":"+serviceInstance.getPort();
    Student student = this.studentService.queryById(sid);
    //使用服务名方式获取
    Subscribe[] subscribes = restTemplate.getForObject(url+"/subscribe/info/"+sid,Subscribe[].class);
    List subscribeList = Arrays.asList(subscribes);
    //Subscribe subscribe = restTemplate.getForObject("http://localhost:8201/subscribe/info/"+sid,Subscribe.class);

    //resutl list
    List resultList = new ArrayList();
    ////使用服务名方式获取
    for (Subscribe subscribe : subscribeList) {
        Course course = restTemplate.getForObject(url+"/course/"+subscribe.getCid(),Course.class);
        if (course != null) {
            resultList.add(course);
        }
    }
    //Course course = restTemplate.getForObject("http://localhost:8201/course/"+subscribe.getCid(),Course.class);
    return ResponseEntity.ok(resultList);
}

如上面代码所示, 我们可以通过服务发现客户端 DiscoveryClient 来获取服务实例,然后通过服务实例的主机名和端口号来拼接访问服务的 url。这里我们始终使用第一个服务实例,这样就会存在单点故障的问题。当然,我们也可以在这里实现不同的算法来选择服务实例,但是这样会增加我们的开发工作量。


Spring Cloud Ribbon 是 Spring Cloud Netflix 项目下的一个子项目,它是一个基于 HTTP 和 TCP 的客户端负载均衡工具。它基于 Netflix Ribbon 实现,可以很好的控制 HTTP 和 TCP 的一些行为。Spring Cloud Ribbon 提供了一系列完善的配置项如连接超时、重试等。


Spring Cloud Ribbon 虽然只是一个工具类框架,它不需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。因为微服务间的调用, API 网关的请求转发等内容,实际上都是通过 Ribbon 来实现的。


所以,对 Spring Cloud Ribbon 的理解和使用,对于我们使用 Spring Cloud 来构建微服务非常重要。在这一章中,我们将具体介绍如何使用 Ribbon 来实现客户端的负载均衡,并且通过源码分析来了解 Ribbon 实现客户端负载均衡的基本原理。


目标:

在本章中,您将学习:

  • 负载均衡的原理
  • Ribbon 的应用

负载均衡方案

目前主流的负载方案分为两种:

  • 服务端负载均衡(集中式负载均衡)

  • 客户端负载均衡


服务端负载均衡(集中式负载均衡)

  • 集中式负载均衡,在消费者和服务提供方中间使用独立的代理方式进行负载,有硬件的负载均衡器,比如 F5,也有软件,比如 Nginx。

bg right fit


集中式负载均衡的工作原理,负载均衡器负责维护需要负载的服务实例信息,如:192.168.1.1:8080 和 192.168.1.2:8080 这两个实例。

客户端不直接请求 192.168.1.1:8080 和 192.168.1.2:8080 这两个实例,而是通过负载均衡器来进行转发,客户端的请求到了负载均衡器这里,负载均衡器会根据配置的算法在 192.168.1.1:8080 和 192.168.1.2:8080 这两个实例中选取一个实例,转发到具体的实例上。

这样的好处是客户端不需要关心对应服务实例的信息,只需要跟负载均衡器进行交互,服务实例扩容或缩容,客户端不需要修改任何代码。


客户端负载均衡

  • 客户端负载均衡,客户端根据自己的请求情况做负载,Ribbon 就属于客户端自己做负载的框架。

客户端负载均衡需要自己维护服务实例的信息,然后通过某些负载均衡算法,从实例中选取一个实例,直接进行访问。

bg right fit


负载均衡在系统架构中是一个非常重要,并且是不得不去实施的内容。因为负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。我们通常所说的负载均衡都指的是服务端负载均衡,其中分为硬件负载均衡和软件负载均衡。


  • 硬件负载均衡主要通过在服务器节点之间安装专门用于负载均衡的设备,比如 F5 等;

F5 是一家美国的网络设备厂商,主要生产网络负载均衡器、应用程序交付控制器、Web 应用程序防火墙、DNS 服务器、内容分发网络、SSL VPN、身份管理系统、网络安全系统等产品。


  • 软件负载均衡则是通过在服务器上安装一些具有负载均衡功能或模块的软件来完成请求分发工作,比如 Nginx 等。

Nginx: 是一个高性能的 HTTP 和反向代理服务器,同时也提供了 IMAP/POP3/SMTP 服务。


硬件负载均衡的设备或是软件负载均衡的软件模块都会维护一个可用的服务端清单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。当客户端发送请求到负载均衡设备的时候,该设备按某种算法(比如线性轮询、按权重负载、按流量负载等)从维护的可用服务端清单中取出一台服务端的地址,然后进行转发。


而客户端负载均衡和服务端负载均衡最大的不同点在于上面所提到的服务清单所存储的位置。


在客户端负载均衡中,所有客户端节点都维护着自己要访问的服务端清单,而这些服务端的清单来自于服务注册中心,比如上一章我们介绍的 Nacos 服务端。同服务端负载均衡的架构类似,在客户端负载均衡中也需要心跳去维护服务端清单的健康性,只是这个步骤需要与服务注册中心配合完成。


Spring Cloud Ribbon 快速实现负载均衡效果

通过 Spring Cloud Ribbon 的封装,我们在微服务架构中使用客户端负载均衡调用非常简单,只需要如下两步:

  • 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。
  • 服务消费者直接通过调用被 @LoadBalanced 注解修饰过的 RestTemplate 来实现面向服务的接口调用。

这样,就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。


注释:
我们可以借助 IntelliJ IDEA 的配置复制功能,只用修改一下端口号即可很方便的启动多个实例.


小问题:
在微服务分布式项目中, 我们通常所说的负载均衡都指的是?

  1. 客户端负载均衡
  2. 前端负载均衡
  3. 服务端负载均衡
  4. 后端负载均衡
查看答案


答案: 3. 服务端负载均衡


Ribbon 主要组件

实现一个通用的负载均衡框架,则需要很多组件支持,Ribbon 中就提供了这些组件,有了这些组件,整个框架的扩展性便会更好,更灵活,我们可以根据业务需求,选择是否要自定义对应的组件来满足特定场景下的需求。



当我们需要通过 Ribbon 选择一个可用的服务实例信息,进行远程调用时,Ribbon 会根据指定的算法从服务列表中选择一个服务实例进行返回。

bg right fit


  • ServerList:

    • 在这个选择服务实例的过程中,服务实例信息是怎么来的呢?这就需要一个服务实例的存储组件来支持,ServerList 就是这个组件。存储分为静态和动态两种方式。静态存储需要事先配置好固定的服务实例信息,动态存储需要从注册中心获取对应的服务实例信息。
  • ServerListFilter

    • 有了服务信息后,在某些场景下我们可能需要过滤一部分信息,这个时候可以用 ServerListFilter 组件来实现过滤操作。

  • ServerListUpdater

    • Ribbon 会将服务实例在本地内存中存储一份,这样就不需要每次都去注册中心获取信息,这种场景的问题在于当服务实例增加或者减少后,本地怎么更新呢?这个时候就需要用到 ServerListUpdater 组件,ServerListUpdater 组件就是用于服务实例更新操作。
  • IPing

    • 缓存到本地的服务实例信息有可能已经无法提供服务了,这个时候就需要有一个检测的组件,来检测服务实例信息是否可用,这个组件就是 IPing。
  • IRule

    • Ribbon 会根据指定的算法来选择一个可用的实例信息,IRule 组件提供了很多种算法策略来选择实例信息。
  • ILoadBalancer

    • 最后就是我们使用的入口了,我们要选择一个可用的服务,怎么选择?问谁要这个服务?这时 ILoadBalancer 就上场了,ILoadBalancer 中定义了软件负载均衡操作的接口,比如动态更新一组服务列表,根据指定算法从现有服务器列表中选择一个可用的服务等操作。

总结

--组件-- --职责--
ServerList 服务实例信息存储
ServerListFilter 服务实例信息过滤
ServerListUpdater 服务实例信息更新
IPing 服务实例信息检测
IRule 服务实例信息选择
ILoadBalancer 服务实例信息获取

这些组件都支持自定义,扩展性很强。


负载均衡的流程,可以用一幅图来总结一下:


bg fit


内置负载均衡规则类 规则描述
RoundRobinRule 简单轮询服务列表来选择服务器。它是 Ribbon 默认的负载均衡规则。
AvailabilityFilteringRule 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果 3 次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续 30 秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了 AvailabilityFilteringRule 规则的客户端也会将其忽略。
WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。

内置负载均衡策略

w:38em


内置负载均衡规则类 规则描述
ZoneAvoidanceRule 以区域可用的服务器为基础进行服务器的选择。使用 Zone 对服务器进行分类,这个 Zone 可以理解为一个机房、一个机架等。而后再对 Zone 内的多个服务做轮询。(默认)
BestAvailableRule 忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule 随机选择一个可用的服务器。
RetryRule 重试机制的选择逻辑
WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
NacosRule Nacos 服务发现的负载均衡策略,根据 Nacos 中设置的权重随机访问。

除了上面的内置负载均衡策略外,Ribbon 还提供了一种自定义负载均衡策略的方式,即继承 AbstractLoadBalancerRule 类,然后实现 choose 方法,该方法的返回值就是选出的服务实例。这种方式的好处是可以根据自己的业务需求来实现自定义的负载均衡策略。


Ribbon 使用方式

Ribbon 的使用方式主要分为下面这三种,

  • 原生 API,Ribbon 是 Netflix 开源的,如果你没有使用 Spring Cloud,也可以在项目中单独使用 Ribbon,在这种场景下就需要使用 Ribbon 的原生 API。

  • Ribbon + RestTemplate,当我们项目整合了 Spring Cloud 时,就可以用 Ribbon 为 RestTemplate 提供负载均衡的服务。

  • Ribbon + Feign,关于 Feign 的使用方式会在后面的章节中进行详细的讲解。


原生 API

Ribbon 原生 API 使用非常方便,首先我们需要配置一个服务列表,数据格式为 IP + PORT,你可以固定写几个服务列表,也可以从别处读取,比如注册中心。


// 服务列表
List serverList = Arrays.asList(
    new Server("localhost", 9020),
    new Server("localhost", 9021),
    new Server("localhost", 9022)
);
// 构建负载实例
BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
loadBalancer.setRule(new RandomRule()); // 随机策略
// 调用10次来测试效果
for (int i = 0; i < 10; i++) {
  String result = LoadBalancerCommand.builder().withLoadBalancer(loadBalancer).build()
    .submit(server -> {
        try {
        String addr = "http://" + server.getHost() + ":" + server.getPort() + "/user/1";
        System.out.println("调用地址:" + addr);
        return Observable.just("");
        } catch (Exception e) {
        return Observable.error(e);
        }
    }).toBlocking().first();
System.out.println("调用结果:" + result);
}

Ribbon + RestTemplate 配置详解

下面来详细介绍 Ribbon 在使用时的各种配置方式。


自动化配置

由于 Ribbon 中定义的每一个接口都有多种不同的策略实现,同时这些接口之间又有一定的依赖关系,这使得第一次使用 Ribbon 的开发者很难上手,不知道如何选择具体的实现策略以及如何组织它们的关系。 Spring Cloud Ribbon 中的自动化配置恰恰能够解决这样的痛点,在引入 SpringCloud Ribbon 的依赖之后,就能够自动化构建下面这些接口的实现。


  • IClientConfig : Ribbon 的客户端配置,默认采用 com.netflix.client.config.DefaultClientConfiglmpl 实现。
  • IRule: Ribbon 的负载均衡策略,默认采用 com.netflix.loadbalancer.ZoneAvoidanceRule 实现,该策略能够在多区域环境下选出最佳区域的实例进行访问。
  • IPing: Ribbon 的实例检查策略,默认采用 com.netflix.loadbalancer.DummyPing 实现,该检查策略是一个特殊的实现,实际上它并不会检查实例是否可用,而是始终返回 true, 默认认为所有服务实例都是可用的。
  • ServerList<Server> :服务实例清单的维护机制,默认采用 com.netflix.loadbalancer.ConfigurationBasedServerList 实现。

  • ServerListFilter<Server> : 服务实例清单过滤机制,默认采用 org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter实现,该策略能够优先过滤出与请求调用方处于同区域的服务实例。
  • ILoadBalancer : 负载均衡器,默认采用 com.netflix.loadbalancer.ZoneAwareLoadBalancer 实现,它具备了区域感知的能力。

上面这些自动化配置内容仅在没有引入 Spring Cloud Alibaba Nacos 等服务治理框架时如此, 在引入 Nacos 依赖时,自动化配置会有一些不同,后续会做详细的介绍。


通过自动化配置的实现,我们可以轻松地实现客户端负载均衡。同时,针对一些个性化需求,我们也可以方便地替换上面的这些默认实现。只需在 Spring Boot 应用中创建对应的实现实例就能覆盖这些默认的配置实现。比如可以在启动类中添加如下的配置内容,由于创建了 NacosRule 实例,所以默认的 ZoneAvoidanceRule 就不会被创建。


@Bean
public IRule rule(){
    return new NacosRule();
}

image-20220719165005737


参数配置

对于 Ribbon 的参数配置通常有两种方式:全局配置以及指定客户端配置。

  • 全局配置的方式很简单,只需使用 ribbon.<key>=<value>格式进行配置即可。 其中,<key>代表了 Ribbon 客户端配置的参数名,<value>则代表了对应参数的值。比如,可以像下面这样全局配置 Ribbon 的连接超时时间。
ribbon:
  connectTimeout: 5000 # 默认2000

注意:全局配置的参数名是不区分大小写的,但是值是区分大小写的。比如,下面这两种配置方式是等价的。

  1. ribbon.ConnectTimeout=5000
  2. ribbon.connectTimeout=5000

客户端所有可配置的 key 可通过此类查看
com.netflix.client.config.CommonClientConfigKey

默认的客户端配置可以通过此类查看
com.netflix.client.config.DefaultClientConfigImpl


全局配置可以作为默认值进行设置,当指定客户端配置了相应 key 的值时,将覆盖全局配置的内容。

  • 指定客户端的配置方式采用<client>.ribbon.<key>=<value>的格式进行配置。其中,<key><value>的含义同全局配置相同,而<client>代表了客户端的名称,也可以将它理解为是一个服务名。假设现在没有例如 Nacos 服务治理框架的帮助来得到动态服务列表,为了实现负载均衡, 我们可以进行如下配置来设置静态的服务列表:

    userservice.ribbon.listOfServers=localhost:8081,localhost:8082,localhost:8083

IRule 配置

上面使用代码的方式可以实现修改负载均衡规则,不过在启动类中修改,属于是全局应用的范围.也就是说,无论调用哪个微服务,我们都将使用该负载均衡的规则.通过另一种方式,我们可以实现更加细粒度的配置.

在 application.yml 文件中,添加新的配置也可以修改规则,只针对某个微服务起效:

userservice: #服务名
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡规则的全类名

饥饿加载

Ribbon 默认采用懒加载,也就是在第一次访问时 LoadBalanceClient 才会创建,请求时间会很长。而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过如下配置即可开启饥饿加载:

#开启userservice服务的饥饿加载
ribbon:
  eager-load:
    enabled: true
    clients:
      - userservice #服务名

与 Nacos 结合

当在 Spring Cloud 的应用中引入 Spring Cloud Alibaba Nacos 依赖时,会触发 Nacos 中实现的对 Ribbon 的自动化配置。

这时 ServerList 的维护机制实现将被 com.alibaba.cloud.nacos.ribbon.NacosServerList 的实例所覆盖,该实现会将服务清单列表交给 Nacos 的服务治理机制来进行维护。

image-20220719183708475


在与 Spring Cloud Alibaba Nacos 结合使用的时候,我们的配置将会变得更加简单。不再需要通过类似 userservice.ribbon.listOfServers 的参数来指定具体的服务实例清单,因为 Nacos 将会为我们动态维护所有服务的实例清单。而对于 Ribbon 的参数配置,依然可以采用之前的两种配置方式来实现,而指定客户端的配置方式可以直接使用 Nacos 中的服务名作为客户端名来完成针对各个微服务的个性化配置。


Spring Cloud Ribbon 默认实现了区域亲和策略。此策略会优先请求同一个 Zone 区域的服务实例。但在一些情况下:比如当不存在同区域的服务、并且同区域的服务负载过高等,仍会自动切换成其他区域的服务。


spring:
  application:
    name: userservice #注册到注册中心的服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 #nacos 服务端地址
      discovery:
        cluster-name: shanghai #区域(集群)名称

spring.cloud.nacos.discovery.cluster-name 属性用于指定当前服务所在的区域名称,当服务启动时,会将该属性的值作为服务的元数据信息,注册到 Nacos 服务注册中心中。当服务消费者调用服务提供者时,会根据服务提供者的元数据信息来选择服务实例。


重试机制

从 Camden SR2 版本开始, Spring Cloud 整合了 Spring Retry 来增强 RestTemplate 的重试能力,对于开发者来说只需通过简单的配置,原来那些通过 RestTemplate 实现的服务访问就会自动根据配置来实现重试策略。重试机制是默认开启的,以之前对 userservice 服务的调用为例,可以在配置文件中增加如下内容:


# 断路器的超时时间需要大于Ribbon的超时时间,不然不会触发重试。
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000
userservice: #服务名
  ribbon:
    ConnectTimeout: 250 #请求连接的超时时间
    ReadTimeout: 1000 #请求处理的超时时间
    OkToRetryOnAllOperations: true #对所有操作请求都进行重试
    MaxAutoRetriesNextServer: 2 #切换实例的重试次数
    MaxAutoRetries: 1 #对当前实例的重试次数。

根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由 MaxAutoRetries 配置),如果不行,就换一个实例进行访问,如果还是不行,再换一次实例访问(更换次数由 MaxAutoRetriesNextServer 配置),如果依然不行,返回失败信息。


注意:

为了触发重试机制,断路器(第 5 章节讲解)的超时时间应该大于 Ribbon 的超时时间。

具体来说,当请求被发送到远程服务时,Ribbon 会开始计时。如果在超时时间内请求成功完成,则重试机制不会被触发。如果请求在超时时间内未能成功完成,则会尝试使用下一个可用实例进行重试。如果重试也未能成功,则断路器会被触发。

因此,为了触发重试机制,断路器的超时时间应该大于 Ribbon 的超时时间。这样,当重试机制被触发时,断路器才能够捕获到请求失败的事件,并执行相应的处理。


问题: The Raft Group [naming_instance_metadata] did not find the Leader node

原因: Nacos 服务端的注册信息不一致
解决: 先停服务,再停 nacos,再删掉 data 目录下的 protocol 文件夹,再重启 nacos.最后再重启需要注册的那些服务.


自定义负载均衡策略

Spring Cloud Ribbon 提供了一些默认的负载均衡策略,比如轮询、随机、响应时间加权等。但是在实际的开发过程中,我们可能会需要一些自定义的负载均衡策略,比如:根据服务实例的元数据信息来选择服务实例、根据服务实例的运行状态来选择服务实例等。


那么如何实现自定义的负载均衡策略呢?Spring Cloud Ribbon 提供了一个抽象类 AbstractLoadBalancerRule,我们只需要继承该类并实现其中的 choose 方法即可。在 choose 方法中,我们可以根据自己的需求来选择服务实例。


public class MetadataRule extends AbstractLoadBalancerRule {
    @Override
    public Server choose(Object key) {
        // 获取负载均衡器
        ILoadBalancer loadBalancer = getLoadBalancer();
        // 获取服务实例列表
        List serverList = loadBalancer.getAllServers();
        // 获取服务实例元数据信息
        for (Server server : serverList) {
            Map metadata = server.getMetaInfo();
            String version = metadata.get("version");
            if (version.equals("1.0")) {
                return server;
            }
        }
        return null;
    }
}

活动 4.1 :

使用 Ribbon 实现客户端负载均衡


附加活动

  1. 通过 Ribbon 实现客户端负载均衡
  2. 指定自定义的负载均衡策略
  3. 指定自定义的 IPing 实现类

com.example.ribbon.rule.MyRule

/**
 * 自定义负载均衡策略 - 总是选择第一个可用的服务实例
 */
public class MyRule extends AbstractLoadBalancerRule {

    @Override
    public Server choose(Object key) {
        List upList = getLoadBalancer().getReachableServers();
        return upList.get(0);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        String clientName = clientConfig.getClientName();
        System.out.println(clientName);
    }

}

com.example.ribbon.rule.MyIPing

/**
 * 自定义我们如何“ping”服务器以检查其是否处于活动状态的接口
 * 这里我们忽略任何判断直接返回true
 */
public class MyIPing implements IPing {

    @Override
    public boolean isAlive(Server server) {
        return true;
    }

}

可以以配置文件方式指定自定义的负载均衡策略和 IPing 实现类.

# 指定userservice的负载策略
userservice.ribbon.NFLoadBalancerRuleClassName=com.example.ribbon.rule.MyRule
userservice.ribbon.NFLoadBalancerPingClassName=com.example.ribbon.rule.MyIPing

也可以以代码方式指定自定义的负载均衡策略和 IPing 实现类, 优先级高于配置文件.

com.example.ribbon.config.RibbonClientConfig

@RibbonClient(name="userservice", configuration=RibbonConfiguration.class)
public class RibbonClientConfig {

}

com.example.ribbon.config.RibbonConfiguration

@Configuration
public class RibbonConfiguration {

    @Bean
    public IRule myRule() {
        return new MyRule();
    }

    @Bean
    public IPing myPing() {
        return new MyIPing();
    }

}

练习问题

  1. 以下哪个不是负载均衡器?
    a. DynamicServerListLoadBalancer
    b. BaseLoadBalancer
    c. AbstractLoadBalancer
    d. ServerListSubsetFilter

    查看答案
    答案: D

  1. 下面哪个不是 ServerListFilter 接口的实现类?
    a. ZonePreferenceServerListFilter
    b. DefaultNIWSServerListFilter
    c. ZoneAwareLoadBalancer
    d. ZoneAffinityServerListFilter

    查看答案
    答案: C

  1. AbstractLoadBalancer 是什么接口的抽象实现?
    a. ILoadBalancer
    b. IloadBalancr
    c. IoadBalancer
    d. IloadBlancer

    查看答案
    答案: A

  1. 对于 Ribbon 的参数配置通常有几种方法?
    a. 1
    b. 2
    c. 3
    d. 4

    查看答案
    答案: B

小结

在本章中,您学习了:

  • 负载均衡是对系统的高可以、网络压力的缓解和处理能力扩容的重要手段之一
  • 在 Ribbon 中通过 RestTemplate 实现客户端负载均衡
  • 负载均衡器
    • AbstractLoadBalancer
    • BaseLoadBalancer
    • DynamicServerListLoadBalancer
    • ZoneAwareLoadBalancer

  • 负载均衡策略
    • AbstractLoadBalancerRule
    • RandomRule
    • RoundRobinRule
    • RetryRule
    • WeightedResponseTimeRule
    • ClientConfigEnabledRoundRobinRule
    • BestAvailableRule
    • PredicateBasedRule
    • AvailabilityFilteringRule
    • ZoneAvoidanceRule

  • Ribbon 在使用时的各种配置方式
    • 自动化配置
    • 参数配置
    • 与 Nacos 结合
    • 加入了重试机制

Views: 16

SpringCloud与微服务-第3章-服务治理(Nacos/Eureka)

第 3 章 - 服务治理


服务治理可以说是微服务架构中最为核心和基础的模块,它主要用来实现各个微服务实例的自动化注册与发现。为什么我们在微服务架构中那么需要服务治理模块呢?


在最初开始构建微服务系统的时候可能服务并不多,我们可以通过做一些静态配置来完成服务的调用。比如,有两个服务 A 和 B,其中服务 A 需要调用服务 B 来完成一个业务操作时,为了实现服务 B 的高可用,不论采用服务端负载均衡还是客户端负载均衡,都需要手工维护服务 B 的具体实例清单。


但是随着业务的发展,系统功能越来越复杂,相应的微服务应用也不断增加,我们的静态配置就会变得越来越难以维护。并且面对不断发展的业务,我们的集群规模、服务的位置、服务的命名等都有可能发生变化,如果还是通过手工维护的方式,那么极易发生错误或是命名冲突等问题。同时,对于这类静态内容的维护也必将消耗大量的人力。


为了解决微服务架构中的服务实例维护问题,产生了大量的服务治理框架和产品。这些框架和产品的实现都围绕着服务注册与服务发现机制来完成对微服务应用实例的自动化管理。

目标:


在本章中,您将学习:

  • 构建服务注册中心
  • 服务注册与服务发现

常见方案

Netflix Eureka

Spring Cloud Eureka,它既包含了服务端组件,也包含了客户端组件,并且服务端与客户端均采用 Java 编写,所以 Eureka 主要适用于通过 Java 实现的分布式系统,或是与 JVM 兼容语言构建的系统。但是,由于 Eureka 服务端的服务治理机制提供了完备的 RESTful API,所以它也支持将非 Java 语言构建的微服务应用纳入 Eureka 的服务治理体系中来。


zookeeper

ZooKeeper 是一个开放源码的分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现,是 Hadoop 和 Hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。


Consult

Consul 是一个服务网格(微服务间的 TCP/IP,负责服务之间的网络调用、限流、熔断和监控)解决方案,它是一个分布式的,高度可用的系统,而且开发使用都很简便。它提供了一个功能齐全的控制平面,主要特点是:服务发现、健康检查、键值存储、安全服务通信、多数据中心。


Spring Cloud Alibaba Nacos

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。

Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。


方案对比

服务治理的方案不一而论,现将主流的技术框架进行横向比对:


bg fit


从上面的对比可以知道,Nacos 作为服务发现中心具备更多的功能支持,且从长远来看 Nacos 在以后的版本会支持 SpringCloud+K8s 的组合,填补两者的鸿沟,在两套体系下可以采用同一套服务发现和配置管理的解决方案,这将大大的简化使用和维护成本.本节课程将以 Nacos 为例,讲解服务治理的相关概念.


服务治理

在服务治理框架中,通常都会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。


搭建 Nacos 服务

Nacos 的安装和使用都非常简单.下面我们就在 Windows 系统上搭建一个 Nacos 服务吧.


1.下载安装包

在 Nacos 的 GitHub 页面,提供有下载链接,可以下载编译好的 Nacos 服务端或者源代码:

GitHub 主页:https://github.com/alibaba/nacos

GitHub 的 Release 下载页:https://github.com/alibaba/nacos/releases


如图所示: Windows 系统下载对应的 zip 压缩包即可.

image-20220715095333038


2.解压

将 nacos-server-2.1.0.zip 解压到任意非中文目录,此处将其解压到 D 盘的 develop 目录下:

image-20220715104229949


目录说明:

  • bin:启动脚本
  • conf:配置文件

3.修改端口

Nacos 的默认端口是 8848,如果你电脑上的其它进程占用了 8848 端口,请先尝试关闭该进程。如果无法关闭占用 8848 端口的进程,也可以进入 nacos 的 conf 目录,修改配置文件中的端口:


image-20220715104440506


修改其中的内容:

image-20220715104601567


4.启动

启动非常简单,进入 bin 目录,结构如下:

image-20220715104737161


然后执行命令即可:

当前目录下,打开 cmd 黑窗口,执行 windows 命令:

startup.cmd -m standalone

(以单实例本机启动)。

也可以打开 startup.cmd, 修改
set MODE="clusterset MODE="standalone",然后执行startup.cmd


启动成功界面如下:

image-20220715105459284


5.访问

在浏览器中访问 Console 给出的 URL: http://192.168.1.4:8848/nacos/index.html ,Nacos 默认的账号密码都是 nacos 。


image-20220715105806185


登录成功:

image-20220715105710888


本章节,我们通过两个简单的微服务模块来了解 Nacos 是如何进行服务注册和发现.

  • 服务提供者
  • 服务消费者

注册服务提供者

现在我们已经成功构建了两个微服务业务模块,接下来我们尝试将这两个业务模块加入 Nacos 的服务治理体系中去.服务之间通过 HTTP 协议进行通信,所以当我们使用 Nacos 进行管理的时候,Nacos 也需要知道相关的信息,比如 IP,端口号等。


1. 添加依赖

由于 Spring Cloud Alibaba 是新加入 Spring Cloud 的组件,所以没有在 spring-cloud-dependencies 里面,我们需要在项目的父工程 pom.xml 文件中加入如下依赖管理:


    com.alibaba.cloud
    spring-cloud-alibaba-dependencies
    2.2.5.RELEASE
    pom
    import

2. 添加 Nacos 的客户端依赖

在上一节,我们已经搭建了 Nacos 服务,而想将我们的应用注册到 Nacos 中,我们还需要在模块中添加 Nacos 的客户端依赖。现在将依赖添加到其中一个模块的 pom.xml 文件中:



    com.alibaba.cloud
    spring-cloud-starter-alibaba-nacos-discovery

3. 添加 Nacos 地址

引入以上依赖之后,我们就可以在 application.yml 文件中配置相关信息,将服务注册到 Nacos 中。

在服务治理框架中,通常都会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。


所以我们需要配置本服务的服务名以及 nacos 的服务端地址信息,如下:

spring:
  application:
    name: orderservice #注册到注册中心的服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 #nacos 服务端地址

4. 启动测试

重启当前应用,观察 nacos 控制台服务管理下的服务列表菜单,注册成功则会将该应用实例显示在列表中。


bg fit


按照以上步骤,我们可以将另一个服务也注册到 Nacos 中,注册成功后,可以在 Nacos 中看到两条服务信息.


bg fit


服务发现与消费

通过上面的内容介绍与实践,己经搭建起了微服务架构中的核心组件——服务注册中心。同时,还对两个业务模块做了改造。通过简单的配置,使该程序注册到 Nacos 注册中心上,成为该服务治理体系下的服务。

现在我们己经有了服务注册中心和服务提供者,服务消费者,下面就来尝试让服务消费者完成两个目标,发现服务以及消费服务。我们通过构建一个简单的示例,看看在 Nacos 的服务治理体系下如何实现服务的发现与消费。


1.添加@LoadBalanced 注解

在没有将服务交由 Nacos 管理之前,我们通过简单的 RestTemplate 对象的方法即可模拟 HTTP 请求,而现在我们将使用服务名代替传统的 IP+端口号的这种 URL,并且实现简单的负载均衡(负载均衡的内容我们将在之后的章节详细介绍).


在声明 RestTemplate 的地方添加注解:

/**
     * 在容器中注册 RestTemplate 方便项目内调用
     * @return
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

2.使用服务名

在发送请求的时候,使用服务名替换掉原 IP+端口。服务消费方 Controller 关键代码如下所示:


@RestController
@RequestMapping("order")
public class OrderController {
   @Autowired
   private OrderService orderService;
   @Autowired
   private RestTemplate restTemplate;
    @GetMapping("{orderId}")
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
        // 根据id查询订单并返回
        Order order = orderService.queryOrderById(orderId);
        //通过RestTemplate查询用户信息 使用服务名代替ip和端口号
        String url = "http://userservice/user/"+order.getUserId();
        User forObject = restTemplate.getForObject(url, User.class);
        //封装返回的数据
        order.setUser(forObject);
        return order;
    }
}

3.测试

当以上步骤完成之后,我们就构建了一个简单但完整的
服务消费方->注册中心->服务提供方
的这样一个简单体系。当我们使用浏览器访问一个服务的时候,该服务内部如何去调用其他服务能力接口的整个过程我们是感知不到的。所以我们像以前一样正常访问即可.


如果测试通过,我们将得到包含两个模块数据的结果集,如下所示案例:

image-20220715163624909


如果希望查询到的 User 实例包含配置文件中的端口信息, 可以这样修改 userservcie 模块 中的 UserController 类:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        User user = userService.queryById(id);
        user.setServerPort(serverPort);
        return user;
    }
}

4.如何启动多个服务提供方

在上面的案例中,我们只启动了一个服务提供方,如果我们想要启动多个服务提供方,并且让服务消费方能够负载均衡的访问到这些服务提供方,那么我们需要做一些额外的配置.


  1. 首先修改运行配置, 开启允许并行运行

bg auto


  1. 准备多个配置文件,并且修改端口号使其各互不冲突, 然后修改运行配置的 active profile 为不同的配置方案, 启动多个服务:

bg cover


  1. 也可以
    直接修改运行配置中的 VM options,添加 -Dserver.port=XXXX,
    其中 XXXX 为不同的端口号, 分别启动多个服务.

bg cover


  1. 找到现有的运行配置服务, 选择复制配置后修改配置文件或者端口设置, 通过不同的运行配置来启动多个服务.

h:14em


  1. 当同时运行 2 个或者多个 springboot 服务时, 可以alt+8打开服务列表, 添加 SpringBoot 的配置类型, 可以方便的统一管理多个服务.


  1. 在 Nacos 运行的运行的情况下可以查看到服务列表, 包括服务的名称, 实例数量, 以及服务的运行状态.
    h:17em

Q: Nacos 服务注册中心的作用是什么?

A: 服务注册中心是一个服务的注册表, 用于存储服务提供方的信息, 以便服务消费方能够找到服务提供方, 从而实现服务的调用.


w:28em


搭建 Eureka 服务

pom.xml



    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-server

application.yml

server:
  port: 10086 # 服务端口
#将Eureka服务注册到Eureka注册中心上,方便以后做集群管理
spring:
  application:
    name: eurekaserver # eureka的服务名称
eureka:
  client:
    service-url: # eureka的地址信息, 如果搭建Eureka集群则需要配置多个地址, 以逗号分隔, 交叉注册
      defaultZone: http://127.0.0.1:10086/eureka

运行 Eureka 服务, 查看可视化界面

h:16em


启用 Eureka 客户端

user 和 order 模块:

@MapperScan("com.niit.user.mapper")
@SpringBootApplication
@EnableEurekaClient  // for uereka
public class MainApplication {
...
}

application.yml

# Eureka 的地址信息
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

Q: Nacos 的服务注册中心和 Eureka 的服务注册中心有什么区别?

A: Nacos 的服务注册中心和 Eureka 的服务注册中心都是用于存储服务提供方的信息, 但是 Nacos 的服务注册中心是一个更加完善的服务注册中心, 它不仅仅是一个服务注册中心, 还是一个配置中心, 服务发现中心, 以及服务管理中心.


活动 3.1

使用 Nacos 实现服务调用


练习问题

  1. 下面哪个选项不是常见的服务治理方案?
    a. Netflix Eureka
    b. Zookeeper
    c. Spring Cloud Alibaba Nacos
    d. Spring Boot Application

    答案

    d


  1. 我们在使用 Spring Cloud Alibaba Nacos 作为注册中心,可以在发送请求的时候,使用服务名替换掉原 IP+端口。
    a. 错误
    b. 正确

    答案

    b


  1. 下面哪条命令是在本机以单实例启动 Nacos 服务?
    a. startup.cmd -m standalone
    b. startup.cmd -f standalone
    c. startup.exe -m standalone
    d. startup.cmd

    答案

    a


  1. 下面哪个是 Nacos 注册中心的客户端依赖?
    a. spring-cloud-starter-alibaba-nacos-config
    b. spring-cloud-starter-alibaba-nacos-discovery
    c. spring-cloud-alibaba-starter-nacos-discovery
    d. spring-cloud-alibaba-nacos-starter-discovery

    答案

    b


小结

在本章中,您学习了:

  • 服务治理的常见方案
  • 搭建 Nacos 服务
    • 注册服务提供者
    • 服务发现与消费

Views: 5

SpringCloud与微服务-第2章-SpringBoot基础回顾

第 2 章 - SpringBoot 快速入门

在展开 Spring Cloud 的微服务架构部署之前,我们先通过本章的内容来了解一下用于构建微服务的基础框架——Spring Boot


在本章中我们将介绍下面这些与后续课程有密切联系的内容。

目标:

  • 如何构建 Spring Boot 项目
  • 如何实现 RESTful API 接口
  • 如何实现多环境的 Spring Boot 应用配置
  • Spring Boot 应用的监控与管理

Spring Boot 框架简介

Spring Boot 可以轻松创建独立的、生产级的基于 Spring 的应用程序,可以“直接运行”。Spring 平台可以轻松集成常用的第三方库。大多数 Spring Boot 应用程序只需要最少的 Spring 配置。

特征

  • 创建独立的 Spring 应用程序
  • 直接嵌入 Tomcat、Jetty 或 Undertow(无需部署 WAR 文件)
  • 提供“starter”依赖项以简化您的构建配置
  • 尽可能自动配置 Spring 和 第三方方库
  • 提供为生产环境准备的功能,例如指标、健康检查和外部配置
  • 绝对没有代码生成,也不需要 XML 配置

项目构建与解析

系统及工具版本要求

  • Java 8 及以上版本(这里选择 jdk11)
  • IntelliJ IDEA 2020 及以上版本

使用 IDEA 快速创建应用

1.打开新建项目选项卡

选中 Spring Initializr 功能,选择项目 SDK(此处以 JDK 11 为例),最后点击下一步。

bg right fit


2.填写版本信息和项目信息

image-20240413220440276


3.选择相关初始化依赖

image-20240413220511660


4.选择项目的本地路径

image-20240413220535228


工程结构解析

  • src/main/java:源代码目录,该目录用来存放应用的源代码,其中 com.example.demo 包名是由我们在创建项目时指定的。
  • src/main/resources:资源文件目录,该目录用来存放应用的配置文件,如 application.properties、application.yml 等。
  • src/test:测试代码目录,该目录用来存放应用的测试代码。

Maven 配置分析

打开当前工程下的 pom.xml 文件,看看生成的项目都引入了哪些依赖来构建 Spring Boot 工程,内容大致如下所示。

parent:
  - spring-boot-starter-parent@2.7.1
dependencies:
  - spring-boot-starter-web
  - spring-boot-starter-jdbc
  - mysql-connector-java(runtime)@5.1.47
  - mybatis-spring-boot-starter
  - spring-boot-devtools
  - spring-boot-configuration-processor
  - lombok(optional)
  - spring-boot-starter-test
  - spring-data-commons(分页支持)

在基础信息部分,groupld 和 artifactld 对应生成项目时页面上输入的内容。另外 Spring Boot 默认将该微服务应用打包为 jar 的形式,而非 war 的形式,因为默认的 Web 模块依赖会包含嵌入式的 Tomcat,这样使得我们的应用 jar 自身就具备了提供 Web 服务的能力,后续我们会演示如何启动它。


父项目 parent 配置指定为 spring-boot-starter-parent 的 2.7.1 版本,该父项目中定义了 Spring Boot 版本的基础依赖以及一些默认配置内容,比如,配置文件 application.properties 的位置等。在项目依赖 dependencies 配置中,包含了下面两项。

  • spring-boot-starter-web:全桟 Web 开发模块,包含嵌入式 Tomcat、Spring MVC。
  • spring-boot-starter-test:通用测试模块,包含 JUnit、Hamcrest、Mockito。

这里所引用的 web 和 test 模块,在 Spring Boot 生态中被称为 Starter POMs。Starter POMs 是一系列轻便的依赖包,是一套一站式的 Spring
相关技术的解决方案。开发者在使用和整合模块时,不必再去搜寻样例代码中的依赖配置来复制使用,只需要引入对应的模块包即可。


Spring Boot 的 官方 Starter POMs 采用 spring-boot-starter-xxx的命名方式,*代表一个特别的应用功能模块,比如这里的 web、test。

另外 xxx.sring-boot-starter 命名的模块是非官方的第三方模块,比如 mybatis-spring-boot-starter


文档:
https://docs.spring.io/spring-boot/docs/2.3.12.RELEASE/reference/html/index.html


实现 RESTful API

在 Spring Boot 中创建一个 RESTful API 的实现代码同 Spring MVC 应用一样,只是不需要像 Spring MVC 那样先做很多配置,而是像下面这样直接开始编写 Controller 内容:

  • 新建 package,可根据实际的构建情况修改成自己的路径。
  • 新建 HelloWorldController 类,内容如下所示。

package com.niit.quickstart.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 控制器类
 */
@RestController
@RequestMapping("/test")
public class HelloWorldController {
    @RequestMapping("/hello")
    public String showHello(){
        return "Hello World!";
    }
}

  • 找到启动类,启动该应用

image-20220711233735883


  • 启动成功,默认端口 8080

    image-20220711233826742


image-20220711233007071


  • 在服务器上部署运行时,通常先使用 mvn package 或 mvn install(双击即可) 将应用打包成 jar 包,再通过 java -jar xxx.jar 来启动应用。

image-20220711233958684


编写单元测试

功能实现之后,我们要养成随手写配套单元测试的习惯,这在微服务架构中尤为重要。

单元测试是在开发过程中用来验证代码正确性非常好的手段,并且这些单元测试将会很好地支持我们未来可能会进行的重构。


在 Spring Boot 中实现单元测试同样非常方便,下面我们编写一个简单的单元测试来模拟 HTTP 请求,测试之前实现的/hello 接口,该接口应返回 Hello World 字符串。

该单元测试使用 RestTemplate 类模拟 发出 HTTP 请求.


1. 声明 RestTemplate 类

/**
 *主配置类 项目启动入口
 */
@SpringBootApplication
public class DemoApplication {

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

    /**
     * 在bean容器中注册RestTemplate 方便在应用中使用
     * @return
     */
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

2.编写单元测试类


/**
 * @apiNote 测试类 用于测试controller 通过restTemplate调用接口 (需要启动项目)
 */
@SpringBootTest
class TestControllerTest {

    @Autowired
    RestTemplate restTemplate;

    @Test
    public void test() {
        String url = "http://localhost:8080/test/hello";
        String result = restTemplate.getForObject(url, String.class);
        System.out.println(result);
    }
}

3.点击测试方法左侧按钮,开始测试

image-20220711235420676


4.测试结果

image-20220711235510407


5.注意事项

使用 RestTemplate 进行访问时,需要确保服务端保持活动状态,否则会测试不通过.

image-20220711235804341


快速入门小结

我们通过 IDEA 工具构建了一个 Spring Boot 的基础项目,并详细介绍了该基础项目的结构及 Maven 的依赖关系,接着在该项目中实现一个输出“Hello World”的 RESTful 接口以及针对该接口的单元测试用例,完成了一个虽然简单,但涵盖了项目构建、服务开发、单元测试的全套开发内容。


我们在后续使用 Spring Cloud 的时候会构建多个 Spring Boot 的项目来实现微服务架构中的基础设施以及微服务应用,通过本节的学习,我们己经具备了使用 Spring Boot 来构建简单微服务项目的基本能力。我们将对后续在 Spring Cloud 组件使用过程中会涉及的配置内容做一些介绍和解释。


配置详解

我们已经轻松地实现了一个简单的 RESTful API 应用,体验了 Spring Boot 的诸多优点。在配置方面,除了 Maven 的配置之外,没有引入任何其他配置。这就是之前我们提到的,Spring Boot 针对常用的开发场景提供了一系列自动化配置来减少原本复杂而又几乎很少改动的模板化配置内容。


但是,我们还是需要了解如何在 Spring Boot 中修改这些自动化的配置内容,以应对一些特殊的场景需求。比如,我们在同一台主机上需要启动多个基于 Spring Boot 的 Web 应用,若不为每个应用指定特别的端口号,那么默认的 8080 端口必将导致冲突。


配置文件

我们在使用 Spring Cloud 的各个组件的时候,其实有大量的工作都会是针对配置文件的。所以我们有必要深入了解一些关于 Spring Boot 中的配置文件的知识,比如配置方式、如何实现多环境配置、配置信息的加载顺序等


Spring Boot 的工程结构 src/main/ resources 目录是 Spring Boot 的配置目录,所以当要为应用创建个性化配置时,应在该目录下进行。Spring Boot 的默认配置文件位置为 src/main/resources/application.properties

image-20220712110030957

上图在配置文件中定义了应用的端口号以及上下文路径.


关于 Spring Boot 应用的配置内容都可以集中在该文件中,根据我们引入的不同 Starter 模块,可以在这里定义容器端口号、数据库连接信息、日志级别等各种配置信息。Spring Boot 的配置文件除了可以使用传统的 properties 文件之外,还支持现在被广泛推荐使用的
YAML 文件。

image-20220712110332417


通过 YAML 的配置方式我们可以看到,配置信息利用阶梯化缩进的方式,其结构更为清晰易读,同时配置内容的字符量也得到显著减少。后续的配置文件,我们将逐步使用 YAML 方式进行编写.


YAML(英语发音为/;jaem3l/,尾音类似 camel 骆驼)是一个可读性高,用来表达资料序列的格式。YAML 的意思其实是:Yet Another Markup Language (仍是一种标记语言),但为了强调这种语言以数据作为中心,而不是以标记语言为重点,而用反向缩略语重新命名。

YAML 的语法特别适合用来表达或编辑数据结构、各种设定文档、文件大纲(例如,许多电子邮件标题格式和 YAML 非常接近)。尽管它比较适合表达阶层式(hierarchical model)的数据结构,不过也有精致的语法可以表示关联性(relational model)的数据结构。

由于 YAML 使用空白符号和分行来分隔资料,其让人最容易上手的特色是巧妙避开各种封闭符号,如引号、各种括号等,这些符号在 xml 结构时会变得复杂而难以辨认。


自定义参数

除了可以在 Spring Boot 的配置文件中设置各个 Starter 模块中预定义的配置属性,也可以在配置文件中定义一些我们需要的自定义属性。比如在 application.yml 中添加:

# 自定义属性
book:
  name: SpringCloudInAction
  author: Jeff

自定义 Book 类,使用@Value 进行属性注入

@Component
@Data
public class Book {
    @Value("${book.name}")
    private String name;
    @Value("${book.author}")
    private String author;
}

@Value 注解加载属性值时可以支持两种表达式来进行配置,如下所示。

  • 一种是上面介绍的 PlaceHolder 方式,格式为${ • • • },大括号内为 PlaceHolder。
  • 另一种是使用 SpEL 表达式(Spring Expression Language),格式为#{•••},大括号内为 SpEL 表达式

编写单元测试

/**
 * Book属性注入 单元测试类
 */
@SpringBootTest
class BookTest {
    @Autowired
    Book book;

    @Test
    public void testBook() {
        System.out.println(book);
    }

}

启动测试,测试结果将在控制台输出:

image-20220712112643626


参数引用

在 application .yml 中的各个参数之间可以直接通过使用 PlaceHolder 的方式来进行引用,就像下面的设置 :

# 自定义属性
book:
  name: SpringCloudInAction
  author: Jeff
  desc: ${book.author} is writing <<${book.name}>>

修改 Book 类,添加 desc 属性.

@Component
@Data
public class Book {
    @Value("${book.name}")
    private String name;
    @Value("${book.author}")
    private String author;
    @Value("${book.desc}")
    private String desc;
}

单元测试步骤同上.测试成功将在控制台打印:

image-20220712114324986


多环境配置

我们在开发应用的时候,通常同一套程序会被应用和安装到几个不同的环境中,比如开发、测试、生产等。其中每个环境的数据库地址、服务器端口等配置都不同,如果在为不同环境打包时都要频繁修改配置文件的话,那必将是个非常烦琐且容易发生错误的事。

Spring Boot 通过配置多份不同环境的配置文件,实现多环境配置更加简单.


在 Spring Boot 中,多环境配置的文件名需要满足 application-{profile}.yml 的格式(我们以.yml 格式为例),其中{profile}对应你的环境标识,如下所示:

  • application-dev.yml:开发环境。
  • application-test.yml:测试环境。
  • application-prod.yml:生产环境。

至于具体哪个配置文件会被加载,需要在 application.yml 文件中通过 spring.profiles.active 属性来设置,其值对应配置文件中的{profile}值。如 spring.profile.active=dev 就会加载 application-dev.yml 配置文件内容。


如下所示:

image-20220712122558581


多环境的配置思路:

  • application.yml 中配置通用内容,并设置 spring.profiles.active=dev,以开发环境为默认配置。
  • application-{profile}.yml 中配置各个环境不同的内容。

加载顺序

在上面的例子中,我们将 SpringBoot 应用需要的配置内容都放在了项目工程中,己经能够通过 spring.profiles.active 来实现多环境的支持。但是,当团队逐渐壮大,分工越来越细致之后,往往不需要让开发人员知道测试或是生产环境的细节,而是希望由每个环境各自的负责人(QA 或是运维)来集中维护这些信息。


那么如果还是以这样的方式存储配置内容,对于不同环境配置的修改就不得不去获取工程内容来修改这些配置内容,当应用非常多的时候就变得非常不方便。同时,配置内容对开发人员都可见,这本身也是一种安全隐患。对此,出现了很多将配置内容外部化的框架和工具,后续将要介绍的 Spring Cloud Config 和Spring Cloud Alibaba Nacos就是一些解决方案,为了后续能更好地理解 这些解决方案 的加载机制,我们需要对 Spring Boot 对数据文件的加载机制有一定的了解。


为了能够更合理地重写各属性的值,Spring Boot 使用了下面这种较为特别的属性加载顺序:

  1. 在命令行中传入的参数。
  2. SPRING_APPLICATION_JSON 中的属性。SPRING_APPLICATION_JSON 是以 JSON 格式配置在系统环境变量中的内容。
  3. java:comp/env 中的 JNDI 属性。
  4. Java 的系统属性,可以通过 System.getProperties()获得的内容。
  5. 操作系统的环境变量。
  6. 通过 random.* 配置的随机属性。

优先级按上面的顺序由高到低,数字越小优先级越高。


  1. 位于当前应用 jar 包之,针对不同{profile}环境的配置文件内容,例如 application-{profile}.properties 或是 YAML 定义的配置文件。
  2. 位于当前应用 jar 包之,针对不同{profile}环境的配置文件内容,例如 application-{profile}.properties 或是 YAML 定义的配置文件。
  3. 位于当前应用 jar 包之外的 application.properties 和 YAML 配置内容。

可以看到,其中第 7 项和第 9 项都是从应用 jar 包之外读取配置文件,所以,实现外部化配置的原理就是从此切入,为其指定外部配置文件的加载位置来取代 jar 包之内的配置内容。通过这样的实现,我们的工程在配置中就变得非常干净,只需在本地放置幵发需要的配置即可,而不用关心其他环境的配置,由其对应环境的负责人去维护即可。


  1. 位于当前应用 jar 包之内的 application.properties 和 YAML 配置内容。
  2. @Configuration 注解修改的类中,通过 @PropertySource 注解定义的属性。
  3. 应用默认属性,使用 SpringApplication.setDefaultProperties 定义的内容。

配置加载优先级通用原理总结:

  • 距离用户越近,优先级越高,距离程序越近,优先级越低。
  • 个性化配置优先级高于通用化配置

活动 2.1

创建 SpringBoot 应用,使用 RestTemplate 进行互相调用。


监控与管理

在微服务架构中各个应用的内部逻辑因分解而得以简化,但是由于部署应用的数量成倍增长,使得系统的维护复杂度大大提升。对于运维人员来说,为了能对这些成倍增长的应用做到高效运维,传统的运维方式显然是不合适的,所以我们需要实现一套自动化的监控运维机制,而这套机制的运行基础就是不间断地收集各个微服务应用的各项指标情况,并根据这些基础指标信息来制定监控和预警规则,更进一步甚至做到一些自动化的运维操作等。


当我们决定用 Spring Boot 来作为微服务框架时,除了它强大的快速开发功能之外,还因为它在 Starter POMs 中提供了一个特殊依赖模块 spring-boot-starter-actuator

引入该模块能够自动为 SpringBoot 构建的应用提供一系列用于监控的端点。当然,它也并不是万能的,有时候也需要对其做一些简单的扩展来帮助我们实现自身系统个性化的监控需求。所以,在本节将详细介绍一些关于 spring-boot-starter-actuator 模块的内容,包括原生提供的端点以及一些常用的扩展和配置方式等。


初识 actuator

下面,我们通过对“快速入门”小节中实现的 Spring Boot 应用增加 spring-boot- starter-actuator 模块功能,来对它有一个直观的认识。在现有的 Spring Boot 应用中引入该模块非常简单,只需要在 pom. xml 的 dependency 节点中,新增 spring-boot-starter-actuator 的依赖即可,具体如下:


        org.springframework.boot
        spring-boot-starter-actuator


增加该依赖之后,重新启动应用。此时输出:

image-20220712193352403

上图显示了端点定义,这个端点并非我们自己在程序中创建的,而是由 spring-boot-starter-actuator 模块根据应用依赖和配置自动创建出来的监控和管理端点。通过这个端点,我们可以实时获取应用的各项监控指标


比如访问/actuator 端点。我们可以获得如下类似信息:

{
  "_links": {
    "self": {
      "href": "http://localhost:8081/dev/actuator",
      "templated": false
    },
    "health": {
      "href": "http://localhost:8081/dev/actuator/health",
      "templated": false
    },
    "health-path": {
      "href": "http://localhost:8081/dev/actuator/health/{*path}",
      "templated": true
    }
  }
}

在没有引入其他依赖之前,该端点的内容较为简单,后续我们在使用 Spring Cloud 的各个组件之后,它的返回会变得非常丰富,这些内容将帮助我们制定更为个性化的监控策略。


原生端点

通过在快速入门示例中添加 spring-boot-starter-actuator 模块,我们已经对它有了一个初步的认识。接下来,我们详细介绍一下 spring-boot-starter-actuator 模块中己经实现的一些原生端点。根据端点的作用,可以将原生端点分为以下三大类。

  • 应用配置类:获取应用程序中加载的应用配置、环境变量、自动化配置报告等与 SpringBoot 应用密切相关的配置类信息。
  • 度量指标类:获取应用程序运行过程中用于监控的度量指标,比如内存信息、线程池信息、HTTP 请求统计等。
  • 操作控制类:提供了对应用的关闭等操作类功能。

出于安全考虑,某些端点默认是没有对外暴露的.为方便测试,我们进行手动开启.

在 application.yml 中添加如下配置:

#主动暴露所有web端点
management:
  endpoints:
    web:
      exposure:
        include: "*"

下面我们来详细了解一下这三类端点都分别可以为我们提供怎样的有用信息和强大功能,以及我们如何去扩展和配置它们。


应用配置类

由于 Spring Boot 为了改善传统 Spring 应用繁杂的配置内容,采用了包扫描和自动化配置的机制来加载原本集中于 XML 文件中的各项内容。虽然这样的做法让我们的代码变得非常简洁,但是整个应用的实例创建和依赖关系等信息都被离散到了各个配置类的注解上,这使我们分析整个应用中资源和实例的各种关系变得非常困难。而这类端点可以帮助我们轻松获取一系列关于 Spring 应用配置内容的详细报告,比如 Bean 创建的报告、环境属性的报告等。


/beans

该端点用来获取应用上下文中创建的所有 Bean。以本项目 dev 环境为例,启动后访问: http://localhost:8081/dev/actuator/beans ,可获取如下信息(部分展示):

image-20220712210434708


在如上示例中,我们可以看到在每个 Bean 中都包含了下面这些信息 。

  • scope:Bean 的作用域。
  • type:Bean 的 Java 类型。
  • resource:class 文件的具体路径。
  • dependencies:依赖的 Bean 名称 。

/configprops:

以本项目 dev 环境为例,启动后访问: http://localhost:8081/dev/actuator/configprops 。该端点用来获取应用中配置的属性信息报告。如下所示,prefix 属性代表了属性的配置前缀,properties 代表了各个属性的名称和值。

image-20220712212525622


所以,我们可以通过该报告来看到各个属性的配置路径,比如我们要关闭该端点,就可以通过使用 endpoints.configprops.enabled=false 来完成设置。如下所示,添加该配置后重启应用即可关闭 /configprops 端点

management:
  endpoint:
    configprops:
      enabled: false

/env:

以本项目 dev 环境为例,启动后访问: http://localhost:8081/dev/actuator/env。该端点与/configprops 不同,它用来获取应用所有可用的环境属性报告,其中还包括了应用还没有使用的配置,所以它可以帮助我们方便地看到当前应用可以加载的配置信息,并配合@ConfigurationProperties 注解将它们引入到我们的应用程序中来进行使用。另外,为了配置属性的安全,对于一些类似密码等敏感信息,该端点都会进行隐私保护,但是我们需要让属性名中包含 password、secret、key 这些关键词,这样该端点在返回它们的时候会使用 * 来替代实际的属性值。


image-20220712213122940


/mappings:

以本项目 dev 环境为例,启动后访问: http://localhost:8081/dev/actuator/mappings。该端点用来返回所有 Spring MVC 的控制器映射关系报告。

image-20220712213559520


度量指标类

上面我们所介绍的应用配置类端点可以说是一个静态报告。而度量指标类端点提供的报告内容则是动态变化的,这些端点提供了应用程序在运行过程中的一些快照信息,比如内存使用情况、HTTP 请求统计、外部资源指标等。

这些端点对于我们构建微服务架构中的监控系统非常有帮助,由于 Spring Boot 应用自身实现了这些端点,所以我们可以很方便地利用它们来收集我们想要的信息,以制定出各种自动化策略。下面,我们就来分别看看这些强大的端点功能。


/metrics:

以本项目 dev 环境为例,启动后访问: http://localhost:8081/dev/actuator/metrics

该端点用来返回当前应用的各类重要度量指标,比如内存信息、线程信息、垃圾回收信息等。


如下所示(部分):

image-20220713112701319


在当前路径后拼接上面路径即可查看对应详细信息. 如拼接"disk.free" ,访问: http://localhost:8081/dev/actuator/metrics/disk.free

可以获知当前磁盘剩余可用容量。

image-20220713113557094


/health:

以本项目 dev 环境为例,启动后访问: http://localhost:8081/dev/actuator/health。该端点用来获取应用的各类健康指标信息。如下所示:

image-20220713115753613


当前项目较为简单,只标识出应用的状态.后续集成 Spring Cloud 组件后,返回信息将变得丰富起来.状态码解释如下:

  • UNKNOWN:未知状态,映射 HTTP 状态码为 503
  • UP:正常,映射 HTTP 状态码为 200
  • DOWN:失败,映射 HTTP 状态码为 503
  • OUT_OF_SERVICE:不能对外提供服务,但是服务正常。映射 HTTP 状态码为 200

操作控制类

操作控制类端点拥有更强大的控制能力,如果要使用它们的话,需要通过属性来配置开启操作。

在原生端点中,只提供了一个用来关闭应用的端点:/shutdown(在后续我们引入了 Eureka 之后,会引入更多控制端点)。可以通过如下配置开启它:

image-20220713122012395


在配置了上述属性之后,只需要访问该应用的/shutdown 端点就能实现关闭该应用的远程操作。由于开放关闭应用的操作本身是一件非常危险的事,所以真正在线上使用的时候,需要对其加入一定的保护机制,比如定制 actuator 的端点路径、整合 Spring Security 进行安全校验等。

该端点不支持 Get 请求,我们可以借助 PostMan 等类似工具发送 POST 请求.

image-20220713121939085


活动 2.2


在 Spring Boot 项目中通过 actuator 端点查看应用的相关信息.

小问题 :

以下哪个内部类在定义时可以没有类名称?

  • A. 正则内部类
  • B. 静态内部类
  • C. 方法 - 局部内部类
  • D. 匿名内部类
答案

D


启用热部署以提升开发效率

IDEA 2021.2 版本 DevTools 热部署为例

首先引入依赖


    org.springframework.boot
    spring-boot-devtools
    true

修改 Settings 里 Compiler 选项下的配置,将下图中红色方框里的选项全部勾上

bg


修改 Advanced Settings 里的配置,将下图的选项勾上


注意 Advanced Settings 选项只有在 File->Settings 里面才有,而在 File->New Projects Setup->Settings for New Projects 这个设置页面里是没有的,所以只能设置本项目,而不能自动设置新建的项目。

如果使用的是 IDEA 2021.2 之前版本的话还是使用快捷键 shift+Ctrl+Alt+/,选择 Registry…,将 compiler.automake.allow.when.app.running 选项勾上。

完成上面的步骤后重启下 IDEA 基本就没问题了,如果还是不行的话建议检查 pom 和 application 文件是否引入了 DevTools 依赖并且正确配置。


练习问题


  1. 下面哪个选项不是 Spring Boot 的基础结构?
  • a. src/main/java
  • b. src/main/resources
  • c. src/test:
  • d. src/main/spring
答案

d


  1. 我们在使用 Restful 风格进行编程的时候,是遵循了配置大于约定的规范。
  • a. 错误
  • b. 正确
答案

a


  1. 下面哪个不是 Spring Boot 应用的配置文件?
  • a. application.yml
  • b. application.yaml
  • c. userservice.xml
  • d.application.properties
答案

c


  1. 在 Spring Boot 应用中,不必引入 spring-boot-starter-actuator 的 Maven 即可实现端点监控?

    a. 正确
    b. 错误

    答案

    b


小结

  • 本章我们通过构建一个最基本的 Spring Boot 工程,让大家对其有了一个最直观的感受。
  • 为后续构建各类 Spring Cloud 组件和微服务应用做了一些基础准备工作。
  • 另外,我们对 SpringBoot 中的配置原理以及监控管理做了深入的介绍,因为这些内容将在后续的介绍中有所涉及,并且它们有助于理解 Spring Cloud 组件的运行原理。
  • 更多关于使用 Spring Boot 开发微服务应用的内容,我们不在本书中详细介绍,读者可参阅官方文档或其他书籍资料来学习

Views: 17

SpringCloud与微服务-第1章-基本概念简介

第 1 章 - 微服务基础知识

在进行 SpringCloud 的具体内容介绍之前,我们先通过本章学习一些关于微服务架构以及 SpringCloud 的基础知识。对 SpringCloud 能够解决的具体问题有一个大致的了解,以帮助我们更好地理解后续章节对各个组件的介绍。


学习目标

  • 了解分布式系统
  • 了解什么是微服务架构
  • 为什么选择 Spring Cloud

随着业务规模和系统复杂度的提升,很多系统会历经从单一架构到垂直架构,再到分布式架构的技术发展过程。


单体系统

在以往传统的企业系统架构中,我们针对一个复杂的业务需求通常使用对象或业务类型来构建一个单体项目。在项目中我们通常将需求分为三个主要部分:数据库、服务端处理、前端展现。在业务发展初期,由于所有的业务逻辑在一个应用中,开发、测试、部署都还比较容易且方便。


w:36em



但是由于单体系统部署在一个进程内,功能之间是紧密耦合的, 往往我们修改了一个很小的功能,部署上线会影响其他功能的运行。

并且,单体应用中的这些功能模块的使用场景、并发量、消耗的资源类型都各有不同,对于资源的利用又互相影响,这样使得我们对各个业务模块的系统容量很难给出较为准确的评估。

所以,单体系统在初期虽然可以非常方便地进行开发和使用,但是随着系统的发展,维护成本会变得越来越大,且难以控制。


面对大流量高并发的用户访问,以及随之产生的海量数据处理等诸多挑战下,如何能为用户提供稳定可靠的服务,成为目前很多互联网大公司面临的技术问题。比如,常见的高并发场景有:

  • 购物节
  • 春运购票
  • 秒杀系统
  • 抖音

购物节的支付系统要想要在高并发的场景下实现五个 9(99.999%)的高可用性,保证支付率,还要保证秒杀场景下单位时间内成交的订单数更多,仅靠单一架构是不能做到的。


如果采用集群的垂直架构,随着业务的发展和系统复杂度的提生,要会出现越来越多的子项目,并且子项目之间功能存在重叠, 维护和部署也变更复杂,很难方便的扩展。

因此越来越多的公司采用分布式架构


分布式架构

将一个系统横向分成若干子系统或服务,实现服务性能的动态扩容。
这不但大幅提高服务处理能力, 且降低单一程序的开发维护以及部署难度。

从整体来看系统复杂度和部署难度是增加了, 但是可以通过 CI/CD 即持续集成和持续部署手段将繁杂的测试部署等环节尽可能自动化.


w:28em


使用分布式服务作为软件体系结构的最早记录可追溯到二十世纪 80 年代初。


SOA 架构

SOA 首先在 90 年代中期得名,当时一家名为 Gartner Group 的公司认识到了这个软件架构的新趋势,并在全球推广。通过这样做,他们设法大大加快了这种架构模式的采用和进一步发展。

SOA(全称:Service Oriented Architecture),中文意思为 “面向服务的架构”,你可以将它理解为一个架构模型或者一种设计方法,而并不是服务解决方案。其中包含多个服务, 服务之间通过相互依赖或者通过通信机制,来完成相互通信的,最终提供一系列的功能。一个服务通常以独立的形式存在与操作系统进程中。各个服务之间通过网络调用 。


SOA 通常使用 ESB(企业总线)来连接各个服务节点。各个服务之间,只需要和 ESB 进行通信,这个时候,各个应用之间的交互就会变得更加的清晰,业务架构/逻辑等,也会变得很清楚。原本杂乱没有规划的系统,梳理成了一个有规划可治理的系统。

bg right


微服务架构

“微服务”一词源于 Martin Fowler 的名为 Microservices 的博文.

简单地说,微服务是系统架构上的一种设计风格,可以认为 SOA 是微服务的超集。微服务架构重点强调的一个是"业务需要彻底的组件化和服务化",原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用称为微服务,微服务之间通过基于 HTTP 的 RESTful API 进行通信协作。每个服务都维护着自身的数据存储、业务开发、自动化测试案例以及独立部署机制。由于有了轻量级的通信协作基础,所以这些微服务可以使用不同的语言来编写。


微服务和 SOA 的区别

  1. 微服务去中心化,去掉 ESB 企业总线。微服务不再强调传统 SOA 架构里面比较重的 ESB 企业服务总线,同时 SOA 的思想进入到单个业务系统内部实现真正的组件化。

  2. Docker 容器技术的出现,为微服务提供了更便利的条件,比如更小的部署单元,每个服务可以通过类似 Node 或者 SpringBoot 等技术跑在自己的进程中。

  3. SOA 注重的是系统集成方面,而微服务关注的是完全分离。


由于微服务是独立部署的,我们可以更准确地为每个服务评估性能容量,通过配合服务间的协作流程也可以更容易地发现系统的瓶颈位置,以及给出较为准确的系统级性能容量评估。


  • 使用微服务架构的厂商
    • 淘宝
    • 京东
    • 抖音
    • ... ...

bg fit right


bg fit


RPC(remote procedure call)

整个应用分散成多个服务使得整个系统变得更为复杂。我们需要在分布式开发中引入额外的技术,以解决服务之间交互和分布式部署导致的问题。

RPC(远程过程调用),即在本地调用远程机器的函数或者对象方法,使实际的体验和调用本地函数或者对象方法无异。

RPC 也是一种技术思想,HTTP 和 WebService 就是 RPC 思想的一种很好的体现方式,但 HTTP 已经满足不了企业内外部日益复杂的信息交互。因此许多优秀的 RPC 框架应运而生,比如著名的 Dubbo ,封装了一些像负载均衡、熔断降级、服务注册发现等面向对象的高级特性。


如何实施微服务

在实施微服务之前,我们必须要知道,微服务虽然有非常多吸引人的优点,但是也因为服务的拆分引发了诸多原本在单体应用中没有的问题。


  • 运维的新挑战:在微服务架构中,运维人员需要维护的进程数量会大大增加。我们需要运维人员有更多的技能来应对这样的挑战,运维过程需要更多的自动化,这就要求运维人员具备一定的开发能力来编排运维过程并让它们能自动运行起来。

  • 接口的一致性:虽然我们拆分了服务,但是业务逻辑上的依赖并不会消除,只是从单体应用中的代码依赖变为了服务间的通信依赖。我们需要更完善的接口和版本管理,或是严格地遵循开闭原则。


  • 分布式的复杂性:由于拆分后的各个微服务都是独立部署并运行在各自的进程内,它们只能通过通信来进行协作,所以分布式环境的问题都将是微服务架构系统设计时需要考虑的重要因素,比如网络延迟、分布式事务、异步消息等。

  • 尽管微服务架构有很多缺点和问题,但是其实现的敏捷开发和自动化部署等优点依然被广大优秀架构师和开发者所青睐.微服务架构并没有一个标准或正式的定义,


Martin Fowler 在 Microservices—文中,提炼出了微服务架构的九大特性,用于指导大家设计架构。

bg fit right


1. 服务组件化

在微服务架构中,需要我们对服务进行组件化分解。服务,是一种进程外的组件,它通过 HTTP 等通信协议进行协作,而不是像传统组件那样以嵌入的方式协同工作。每一个服务都独立开发、部署,可以有效避免一个服务的修改引起整个系统的重新部署。


bg fit


2. 按业务组织团队

在实施微服务架构时,需要采用不同的团队分割方法。由于每一个微服务都是针对特定业务的宽栈或是全栈实现,既要负责数据的持久化存储,又要负责用户的接口定义等各种跨专业领域的职能。因此,面对大型项目的时候,对于微服务团队的拆分更加建议按业务线的方式进行拆分,一方面可以有效减少服务内部修改所产生的内耗;另一方面,团队边界可以变得更为清晰.


w:20em

康威法则: 一个组织的结构应该反映其沟通结构。


bg fit


3. 做“产品”的态度

在实施微服务架构的团队中,每个小团队都应该以做产品的方式,对其产品的整个生命周期负责。持续关注服务的运作情况,并不断分析以帮助用户来改善业务功能。


4. 智能端点与哑管道

在微服务架构中,组件间的通信模式发生了改变,若仅仅将原本在进程内的方法调用改成 RPC 方式的调用,会导致微服务之间产生烦琐的通信,使得系统表现更为糟糕,所以,我们需要更粗粒度的通信协议。

Martin Flower 将微服务架构的服务通信理念称为“Smart endpoints and dumb pipes”, 简单翻译为“聪明的终端,愚蠢的管道”,大意是指强服务个体和弱通信。


在微服务架构中,通常会使用以下两种服务调用方式:

  • 使用 HTTP 的 RESTful API 或轻量级的消息发送协议
  • 通过在轻量级消息总线上传递消息,类似 RabbitMQ 等一些提供可靠异步交换的中间件。

在极度强调性能的情况下,有些团队会使用二进制的消息发送协议,例如 protobuf 即使是这样,这些系统仍然会呈现出“智能端点和哑管道”的特点,这是为了在易读性与高效性之间取得平衡。当然大多数 Web 应用或企业系统并不需要在这两者间做出选择,能够获得易读性已经是一个极大的胜利了。


5. 去中心化治理

在实施微服务架构时,通过采用轻量级的契约定义接口,使得我们对于服务本身的具体技术平台不再那么敏感,这样整个微服务架构系统中的各个组件就能针对其不同的业务特点选择不同的技术平台.


6. 去中心化管理数据

我们在实施微服务架构时,都希望让每一个服务来管理其自有的数据库,这就是数据管理的去中心化。

bg fit left

由于数据存储于不同的数据库实例中后,数据一致性也成为微服务架构中亟待解决的问题之一。(分布式锁,分布式事务)


7. 基础设施自动化

近年来云计算服务与容器化技术的不断成熟,在微服务架构中,务必从一开始就构建起“持续交付”平台来支撑整个实施过程:

  • 自动化测试:每次部署前的强心剂,尽可能地获得对正在运行的软件的信心。
  • 自动化部署:解放烦琐枯燥的重复操作以及对多环境的配置管理。

w:28em

Reference Link


8. 容错设计

微服务架构中,由于服务都运行在独立的进程中,所以存在部分服务出现故障,而其他服务正常运行的情况。由于服务之间存在调用链,因此在微服务架构中,快速检测出故障源并尽可能地自动恢复服务是必须被设计和考虑的。

通常,我们都希望在每个服务中实现监控和日志记录的组件,比如服务状态、断路器状态、吞吐量、网络延迟等关键数据的仪表盘等。


9. 演进式设计

在很多情况下,架构师都会以演进的方式进行系统的构建。

  • 在初期,以单体系统的方式来设计和实施,一方面系统体量初期并不会很大,构建和维护成本都不高,此时还不需要微服务架构。

  • 随着系统的发展或者业务的需要,架构师会将一些经常变动或是有一定时间效应的内容进行微服务处理,并逐渐将原来在单体系统中多变的模块逐步拆分出来,而稳定不太变化的模块就形成一个核心微服务存在于整个架构之中。


Spring Cloud

SpringCloud 的出现,可以说是对微服务架构的巨大支持和强有力的技术后盾,SpringCloud 是一个解决微服务架构实施的综合性解决框架的有序集合,利用 Spring Boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 Spring Boot 的开发风格做到一键启动和部署。

w:20em


SpringCloud 核心部件:

w:28em


近几年人们对于微服务架构的热情非常高,无数的架构师和开发者在实际项目中实践该设计理念并为此付出了诸多努力,同时也分享了他们在微服务架构中针对不同应用场景出现的各种问题的各种解决方案和开源框架,其中也不乏国内互联网企业的杰出贡献。

  • 服务治理:阿里巴巴开源的 Spring Cloud Alibaba Nacos、Netflix 的 Eureka、Apache 的 Consul 等。
  • 分布式配置管理:百度的 Disconf、SpringCloud 的 Config、阿里巴巴开源的 Spring Cloud Alibaba Nacos 等。
  • 批量任务:当当网的 Elastic-Job、Linkedln 的 Azkaban、SpringCloud 的 Task 等。
  • 服务跟踪:京东的 Hydra、SpringCloud 的 Sleuth、Twitter 的 Zipkin 等。

早期 Netflix 和现在的 Alibaba 都是非常重要的 SpringCloud 的贡献者,他们都是微服务架构的先行者,他们的开源框架和解决方案都是我们在实施微服务架构时的参考。

w:28em


Spring Cloud Netflix

h:6em

Spring Cloud Alibaba

h:6em


上面列举了一些在实施微服务架构初期,就需要被我们考虑进去的问题,以及针对这些问题的开源解决方案。可以看到国内、国外的技术公司都在贡献着他们的智慧。我们搜索微服务架构的实施方案时会发现,几乎大部分的分享主要以理论或是一个粗轮廓框架为主,整合了来自不同公司或组织的诸多开源框架,并加入针对自身业务的一些优化,所以找不到一个完全相同的架构方案。


Spring Cloud 包含了多个子项目(针对分布式系统中涉及的多个不同开源产品,还可能会新增),挑选部分项目,如下所述。

  • Spring Cloud Config:配置管理工具,支持使用 Git 存储配置内容,可以使用它实现应用配置的外部化存储,并支持客户端配置信息刷新、加密/解密配置内容等。

  • Spring Cloud Netflix:核心组件,对多个 Netflix OSS 开源套件进行整合。

    • Eureka:服务治理组件,包含服务注册中心、服务注册与发现机制的实现。
    • Hystrix:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。
    • Ribbon:客户端负载均衡的服务调用组件。
    • Feign:基于 Ribbon 和 Hystrix 的声明式服务调用组件。
    • Zuul:网关组件,提供智能路由、访问过滤等功能

  • Spring Cloud Alibaba: 核心组件,对多个 Alibaba 开源套件进行整合.

    • 流量控制和服务降级: Alibaba Sentinel 的流量控制、断路和系统自适应保护
    • 服务注册与发现:实例注册到阿里巴巴的 Nacos,通过负载均衡器 Ribbon,客户端可以通过 Spring 管理的 bean 发现实例。
    • 分布式配置:使用阿里巴巴 Nacos 作为数据存储
    • 事件驱动:构建与 Spring Cloud Stream RocketMQ Binder 连接的高度可扩展的事件驱动微服务
  • Spring Cloud Bus:事件、消息总线,用于传播集群中的状态变化或事件,以触发后续的处理,比如用来动态刷新配置等

  • Spring Cloud Cluster:针对 ZooKeeper、Redis、Hazelcast、Consul 的选举算法和通用状态模式的实现。

  • Spring Cloud Stream:通过 Redis、RabbitMQ 或者 Kafka 实现的消费微服务,可以通过简单的声明式模型来发送和接收消息。


  • Spring Cloud Security:安全工具包,提供在 Zuul 代理中对 OAuth2 客户端请求的中继器。
  • Spring Cloud Sleuth: SpringCloud 应用的分布式跟踪实现,可以完美整合 Zipkin
  • Spring Cloud ZooKeeper:基于 ZooKeeper 的服务发现与配置管理组件。
  • Spring Cloud Starters:SpringCloud 的基础组件,它是基于 SpringBoot 风格项目的基础依赖模块。

image-20220711131013102


Spring Cloud 版本说明

当我们通过搜索引擎查找一些 SpringCloud 的文章或示例时,往往可以在依赖中看到很多不同的版本名字,比如 Angel.SR6、Brixton.SR5 等,为什么 SpringCloud 没有像其他 Spring 的项目使用类似 l.x.x 的版本命名规则呢?这些版本之间又有什么区别呢?在学习之初,非常有必要弄清楚这些版本的意义和内容,这样才能在我们使用 SpringCloud 时,指导我们选择更为合适的版本进行架构与开发。


版本名与版本号

由于 SpringCloud 不像 Spring 社区其他一些项目那样相对独立,它是一个拥有诸多子项目的大型综合项目,可以说是对微服务架构解决方案的综合套件组合,其包含的各个子项目也都独立进行着内容更新与迭代,各自都维护着自己的发布版本号。因此每一个 Spring Cloud 的版本都会包含多个不同版本的子项目,为了管理每个版本的子项目清单,避免 SpringCloud 的版本号与其子项目的版本号相混
淆,没有采用版本号的方式,而是通过命名的方式。


这些版本的名字采用了伦敦地铁站的名字,根据字母表的顺序来对应版本时间顺序,比如最早的 Release 版本为 Angel,第二个 Release 版本为 Brixton 经过上面的解释,不难猜出,之前所提到的 Angel.SR6、Brixton.SR5 中的 SR6、SR5 就是版本号了。

当一个版本的 SpringCloud 项目的发布内容积累到临界点或者一个严重 bug 解决可用后,就会发布一个“servicereleases”版本,简称 SRX 版本,其中 X 是一个递增的数字,所以 Brixton.SR5 就是 Brixton 的第 5 个 Release 版本。


SpringCloud 与 SpringBoot 的版本兼容关系如下:

Release Train Boot Version
2021.0.x aka Jubilee 2.6.x
2020.0.x aka Ilford 2.4.x, 2.5.x (Starting with 2020.0.3)
Hoxton 2.2.x, 2.3.x (Starting with SR5)
Greenwich 2.1.x
Finchley 2.0.x
Edgware 1.5.x
Dalston 1.5.x

注意:在本教程,主要以 Hoxton.SR10 版本进行案例演示.因此对应的 SpringBoot 版本是 2.2.x ~ 2.3.x 版本.


活动手册

我们在构建 SpringBoot 应用的时候,总是需要一整套的符合 MVC 规范的简单模板文件,为了避免重复性操作,我们可以使用一些工具类帮我们完成这部分的工作.比如说 Intellij IDEA 的一款优秀插件: EasyCode。

EasyCode 是基于 IntelliJ IDEA 开发的代码生成插件,支持自定义任意模板(Java,html,js,xml)。只要是与数据库相关的代码都可以通过自定义模板来生成。支持数据库类型与 java 类型映射关系配置。支持同时生成生成多张表的代码。每张表有独立的配置信息。完全的个性化定义,规则由你设置。


活动 1.1 使用 EasyCode 插件生成辅助代码

创建项目- 推荐使用 Spring Initializr 创建项目

  • 使用https://start.aliyun.com/ 作为启动页

  • 建议使用 JDK11 或 JDK17


  • 依赖:

    • spring-boot-starter-parent 2.3.12.RELEASE

    • springboot-starter-web - 相关

    • springboot-starter-jdbc

    • spring-data-commons 2.3.9.RELEASE

    • mybatis-spring-boot-starter 2.1.4

    • mysql-connector-java (5.1.x)

    • spring-boot-starter-actuator

    • lombok optional

    • spring-boot-configuration-processor optional

    • spring-boot-devtool optional


步骤 1. 安装 EasyCode 插件

h:16em


步骤 2. 配置 EasyCode 插件

h:16em


步骤 3. 配置数据库连接

按照活动手册要求创建数据库, 然后Views -> Tool Windows -> Database

h:16em


步骤 4. 配置模板

settings -> Other Settings -> EasyCode -> templates

如果使用 lombok,需要在 entity 类模板中添加 @Data 注解

##使用全局变量实现默认包导入
$!{autoImport.vm}
import java.io.Serializable;
import lombok.Data;
##使用宏定义实现类注释信息
#tableComment("实体类")
@Data
public class $!{tableInfo.name} implements Serializable {
    private static final long serialVersionUID = $!tool.serial();
#foreach($column in $tableInfo.fullColumn)
    #if(${column.comment})/**
     * ${column.comment}
     */#end
    private $!{tool.getClsNameByFullName($column.type)} $!{column.name};
#end

dao 接口类模板示例

queryAllByLimit代码需要修改才能使用

public interface $!{tableName} {

    /**
     * 通过ID查询单条数据
     * @param $!pk.name 主键
     * @return 实例对象
     */
    $!{tableInfo.name} queryById($!pk.shortType $!pk.name);

    /**
     * 查询指定行数据
     * @param $!tool.firstLowerCase($!{tableInfo.name}) 查询条件
     * @param pageable         分页对象
     * @return 对象列表
     */
    List<$!{tableInfo.name}> queryAllByLimit(
      @Param("entity") $!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name}),
      @Param("pageable") Pageable pageable
    );

mapper.xml 模板示例

mapper.xml文件保存路径需要调整与 SpringBoot 默认的一致

##引入mybatis支持
$!{mybatisSupport.vm}

##设置保存名称与保存位置
$!callback.setFileName($tool.append($!{tableInfo.name}, "Dao.xml"))
$!callback.setSavePath($tool.append($modulePath, "/src/main/resources/mappers"))

mapper.xml 模板示例

queryAllByLimit的查询需要修改才能使用

    
    

    
    

步骤 5. 生成代码

application.properties 需要修改mapper.xml的生成路径

# 应用名称
spring.application.name=EasyCodeDemo
# 数据库驱动:
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 数据源名称
spring.datasource.name=MySQLDataSource
# 数据库连接地址
spring.datasource.url=jdbc:mysql://localhost:3306/course?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
# 数据库用户名&密码:
spring.datasource.username=root
spring.datasource.password=niit1234
#下面这些内容是为了让MyBatis映射
#指定Mybatis的Mapper文件 - EasyCode自动生成的包为mapper, 需修改
mybatis.mapper-locations=classpath:mappers/*xml
#指定Mybatis的实体目录
mybatis.type-aliases-package=com.example.easycodedemo.mybatis.entity
# debug
logging.level.com.example.easycodedemo=debug
spring.jpa.show-sql=true

可以选择多表, 选择生成代码


指定生成规则, 点击 OK 进行生成


指定 Mapper 的包扫描路径

package com.example.easycodedemo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.easycodedemo.dao")
public class EasyCodeDemoApplication {

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

}

测试:

  • 对于 get 请求可以直接使用浏览器窗口进行测试(只支持 get)
    • 或者编写页面使用 ajax 进行测试
  • 或者 idea 的插件 (方便)
    • HTTP Client(Builtin)
    • RestfulTool(MarketPlace 免费)
    • Restful Fast Request(收费)
  • 另外还可以使用专用软件进行测试 (功能强大)
    • curl
    • postman
    • apifox
    • apipost

这里使用 RestfulTool 进行测试: body 设置{}表示无条件分页查询

bg right


分页查询报错的问题解决

测试http://localhost:8080/course/接口收到报错如下:

[Request processing failed;
nested exception is java.lang.IllegalStateException:
No primary or default constructor found for class org.springframework.data.domain.PageRequest] with root cause

java.lang.NoSuchMethodException: org.springframework.data.domain.PageRequest.()

新建config包, 并创建一个自定义参数解析器PageRequestHandlerMethodArgumentResolver, 让它也能解析Pageable的子类 PageRequest

@Component
public class PageRequestHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolver {

    /**
     * {@link PageableHandlerMethodArgumentResolver} SpringData 提供的只能解析 {@link Pageable} 类型的
     * 这里让它也能解析它的子类 {@link PageRequest}
     *
     * @param parameter
     * @return
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return Pageable.class.isAssignableFrom(parameter.getParameterType());
    }
}

接着创建WebConfig 配置类

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private PageRequestHandlerMethodArgumentResolver pageRequestHandlerMethodArgumentResolver;

    /**
     * 添加自定义参数解析器
     *
     * @param argumentResolvers
     */
    @Override
    public void addArgumentResolvers(List argumentResolvers) {
        argumentResolvers.add(pageRequestHandlerMethodArgumentResolver);
    }
}

分页和排序支持:

http://localhost:8080/course?size=2&page=0&sort=cid,desc&sort=cname,asc

表示:

  1. 页容量为2, 显示第1页的数据
  2. 二次排序: 先按cid倒序, 再按cname正序

小问题:

Spring Cloud 为以下哪些操作提供了一种简单的开发方式?

  • A. 服务治理
  • B. 智能路由
  • C. 微代理
  • D. 控制总线


谢谢

Views: 8

Index