Spring Cloud Hystrix断路器快速入门
在微服务架构中,我们将系统拆分成了很多服务单元,各单元的应用间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同
的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题
会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会因等待出现故障的依赖方响应形成任务积压,最
终导致自身服务的瘫痪。
目标:
在本章中,您将学习:
- 如何创建请求命令
- 如何进行异常处理
- 如何实现 turbine 集群监控
- 原理分析
- 断路器原理
- 依赖隔离
- 使用详解
- 属性详解
- 监控面板
服务雪崩
微服务架构下,会存在服务之间相互依赖调用的情况,当某个服务不可用时,很容易因为服务之间的依赖关系使故障扩大,甚至造成整个系统不可用的情况,这种现象称为服务雪崩效应。
-
如上图所示,为服务雪崩效应发生的过程,首先是服务正常状态,当客户端对服务 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 的启动依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!--引入Hystrix--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <!--Hystrix仪表盘--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency> <!--actuator端点--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> |
2.改造启动类
在消费侧启动类上添加@EnableCircuitBreaker
注解,启动断路器功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@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 请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@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 来观察输出结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@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 处打开命令窗口:
1 |
startup.cmd -m standalone |
2.启动消费端
修改配置:
1 2 3 4 5 |
ribbon.staticServerList: userservice: - http://localhost:9020 - http://localhost:9021 - http://localhost:9022 |
并分别启动三个服务实例,端口号分别为 9020,9021,9022
实际测试的时候, 此配置无效
3.启动服务端
我们已经掌握了负载均衡的相关知识,可启动多个服务端,本案例将启动三个服务实例进行演示。
所有项目启动成功后,端口号如下图所示:
测试及结果
1.正常访问
本案例访问路径为: http://localhost:8080/consumer/hystrix/100 ,多次发送 Get 请求,可以看到如下三种可能的结果:
1 2 3 |
当前应用的端口是: 9020; 传递过来的参数是: 100; 当前应用的端口是: 9021; 传递过来的参数是: 100; 当前应用的端口是: 9022; 传递过来的参数是: 100; |
2.服务器宕机
如果ribbon.staticServerList
配置是有效的, 那么假如我们停掉 9022 端口的实例,那么刷新之后,将可能会出现以下这样的三种情境:
1 2 3 |
当前应用的端口是: 9020; 传递过来的参数是: 100; 当前应用的端口是: 9021; 传递过来的参数是: 100; 服务器繁忙,请稍后再试o(╥﹏╥)o |
实际情况是, ribbon.staticServerList
配置是无效的, 估计这个配置已经被废弃了, 因为使用静态的服务列表是无法感知动态同步示例的变化的, 所以实际上也不推荐使用这个配置.
为了模拟降级的发生, 可以干脆停止所有的实例, 这样一定会出现服务降级了:
1 |
服务器繁忙,请稍后再试o(╥﹏╥)o |
3.服务端超时
除了通过断开具体的服务实例来模拟某个节点无法访问的情况之外,我们还可以模拟一下服务阻塞(长时间未响应)的情况。我们对 userservice 的/HystrixProviderController
控制器并做一些修改,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@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
毫秒,所以这里采用了 0
至 2000
的随机数以让处理过程有一定概率发生超时来触发断路器:
1 2 3 4 |
阻塞时间: 865毫秒 当前应用的端口是: 9020; 传递过来的参数是: 100; 共花费执行时间: 865 毫秒 阻塞时间: 1655毫秒 服务器繁忙,请稍后再试o(╥﹏╥)o |
前面演示的是因为超时而触发服务降级的情况,那么如果是因为服务端抛出异常而触发服务降级呢?这里我们将userservice的/hystrix/{param}
接口逻辑改为抛出异常:
1 2 3 4 5 6 |
@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 是如何工作的。
1. 构造一个HystrixCommand
或HystrixObservableCommand
对象
第一步是构造一个HystrixCommand
或HystrixObservableCommand
对象来表示您对依赖项发出的请求。向构造函数传递发出请求时需要的任何参数。代码中我们可以简单的通过使用@HystrixCommand
注解实现.
Hystrix 采用了命令模式来实现对服务调用操作的封装。而这两个 Command 对象分别针对不同的应用场景。
HystrixCommand
:用在依赖的服务返回单个操作结果的时候。- 用于包装将执行潜在风险功能(通常意味着通过网络进行服务调用)的代码,具有容错和延迟、统计信息和性能指标捕获、断路器和隔板功能。此命令本质上是一个阻塞命令,但如果与
observe()
一起使用,则会提供一个监听器模式的非阻塞实现。
- 用于包装将执行潜在风险功能(通常意味着通过网络进行服务调用)的代码,具有容错和延迟、统计信息和性能指标捕获、断路器和隔板功能。此命令本质上是一个阻塞命令,但如果与
HystrixObservableCommand
:用在依赖的服务返回多个操作结果的时候。- 作用和
HystrixCommand
相同, 不同的是此命令应用于纯非阻塞调用模式。此命令的调用方将订阅run()
方法返回的 Observable。
- 作用和
如何给一个接口构造一个
HystrixCommand
或HystrixObservableCommand
对象呢?我们可以通过继承HystrixCommand
类或HystrixObservableCommand
来实现,也可以通过@HystrixCommand
注解来实现。
如何给一个接口构造一个HystrixObservableCommand
对象呢?我们可以通过继承HystrixObservableCommand
类来实现,也可以通过@HystrixCommand
注解来实现。
设计模式之命令模式介绍
Hystrix 利用命令模式来实现对服务调用操作的封装。命令模式是一种数据驱动的设计模式,它属于行为型模式。在命令模式中,我们可以将请求封装成一个对象,以便使用不同的请求、队列或日志来参数化其他对象。命令模式也支持可撤销的操作。通过使用命令模式可以实现“行为请求者”与“行为实现者”的解耦,以便使两者可以适应变化。
下面的示例是对命令模式的简单实现:
-
行为接收者类:
Receiver
, 负责具体的业务逻辑处理,它是真正执行命令的对象。123456public class Receiver {public void action(){//执行真正的业务逻辑System.out.println("Receiver 执行命令...");}}
-
命令接口: ICommand, 它定义了一个命令对象应具备的一系列命令操作,比如
execute ()
、undo()
、redo()
等。当命令操作被调用的时候就会触发接收者去做具体命令对应的业务逻辑。123456public interface ICommand {/*** 执行方法*/void execute();}
-
命令实现类:
ConcreteCommand
, 它是抽象命令类的具体实现类,它是一个具体的命令对象通过调用接收者的相关操作来实现业务逻辑。12345678910111213public class ConcreteCommand implements ICommand {private Receiver receiver; // 组合接收者对象public ConcreteCommand(Receiver receiver){this.receiver = receiver;}@Overridepublic void execute() {this.receiver.action();}}
-
命令调用者类:
Invoker
, 它是命令模式的核心,它负责调用命令对象执行请求,相关的方法叫做行动方法。它可以持有一个或多个命令对象,它在需要的时候可以去调用命令对象的执行方法来执行请求,它通过这种方式来实现间接调用请求接收者的相关操作来实现业务操作。12345678910public class Invoker {private ICommand command; // 组合命令对象public void setCommand(ICommand command) {this.command = command;}public void action(){this.command.execute();}}
-
客户端:
Client
, 它是命令模式的使用者,它通过调用者来执行命令。12345678910public 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();}}
从上面的示例中,我们可以看到,调用者 Invoker 与操作者 Receiver 通过 Command 命令接口实现了解耦。对于调用者来说,我们可以为其注入多个命令操作,比如新建文件、 复制文件、删除文件这样三个操作,调用者只需在需要的时候直接调用即可,而不需要知道这些操作命令的具体对象是来自哪些操作者。
Invoker 和 Receiver 的关系非常类似于“请求 – 响应”模式,所以它比较适用于实现记录日志、撤销操作、队列请求等。
在下面这些情况下应考虑使用命令模式。
- 使用命令模式作为“回调(CallBack)”在面向对象系统中的替代。“CallBack”讲的便是先将一个函数登记上,并不马上执行, 然后在以后需要的时候再调用此函数。
- 需要在不同的时间指定请求、将请求排队。一个命令对象和原先的请求发出者可以有不同的生命周期。换言之,原先的请求发出者可能己经不在了,而命令对象本身仍然是活动的(通过命令接口解耦命令调用方和命令操作方)。这时命令的接收者可以是在本地,也可以在网络的另外一个地址。命令对象可以在序列化之后传送到另外一台机器上去。
- 系统需要支持命令的撤销。命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用
undo()
方法,把命令所产生的效果撤销掉。 - 命令对象还可以提供
redo()
方法,以供客户端在需要时再重新实施命令效果。 - 如果要将系统中所有的数据更新到日志里,以便在系统崩溃时,可以根据日志读回所有的数据更新命令,重新调用
executed
方法一条一条执行这些命令,从而恢复系统在崩溃前所做的数据更新。
2. Hystrix 命令模式
有四种方法可以执行 HystrixCommand
或 HystrixObservableCommand
,通过使用 Hystrix 命令对象的以下四种方法之一:
- 执行
HystrixCommand
execute()
— 同步执行,从依赖的服务返回一个单一的结果对象,或是在发生错误的时候抛出异常。queue()
— 异步执行,直接返回一个 Future 对象,其中包含了服务执行结束时要返回的单一结果对象。
-
执行
HystrixObservableCommand
,通过使用observe()
— 返回来自依赖项的响应的 Observable 对象 ,它代表了操作的多个结果,它是一个hot Observable
。toObservable()
— 同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个cold Observable
。
1234K value = command.execute();Future<K> fValue = command.queue();Observable<K> ohValue = command.observe(); //hot observableObservable<K> ocValue = command.toObservable(); //cold observable
execute()
– 同步执行
1 2 3 4 5 |
@HystrixCommand(fallbackMethod = "fallbackMethod") public String strConsumer() { ResponseEntity<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class); return result.getBody(); } |
fallbackMethod —— 回调方法,在服务调用异常、断路器打开、线程池/请求队列/信号量占满时会走回调逻辑。必须和服务方法定义在同一个类中,对修饰符没有特定的要求,定义为 private、 protected、 public 均可。
queue()
– 异步执行
1 2 3 4 5 6 7 8 9 10 11 12 |
@HystrixCommand(fallbackMethod = "fallbackMethod", ignoreExceptions = {IllegalAccessException.class}) public Future<String> asyncStrConsumer() { Future<String> asyncResult = new AsyncResult<String>() { @Override public String invoke() { ResponseEntity<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class); return result.getBody(); } }; return asyncResult; } |
ignoreExceptions
表示抛出该异常时不走降级回调逻辑,忽略此异常。
observe ()
执行方式
1 2 3 4 5 |
@HystrixCommand(observableExecutionMode = ObservableExecutionMode.EAGER) protected Observable<String> construct() { ResponseEntity<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class); return Observable.just(result.getBody()); } |
ObservableExecutionMode.EAGER
—— 代表使用observe()
执行方式,返回的是一个hot Observable
,即便没有订阅者,它也会立即执行。
toObservable()
执行方式
1 2 3 4 5 |
@HystrixCommand(observableExecutionMode = ObservableExecutionMode.LAZY) protected Observable<String> construct() { ResponseEntity<String> 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// 1. 创建一个被观察者 Observable Observable<String> observable = Observable.create(new Observable.OnSubscribe<String>() { // 2. 在被观察者 Observable 被订阅时,回调该方法 @Override public void call(Subscriber<? super String> subscriber) { // 3. 发送事件:onNext() subscriber.onNext("Hello RxJava"); subscriber.onNext("I am Jeff"); // 4. 发送事件:onCompleted() subscriber.onCompleted(); } }); // 5. 创建一个观察者 Observer Observer<String> observer = new Observer<String>() { // 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)
来触发事件的发布。
调用过程如下:
- 主线程调用
Observable.create()
方法,创建一个被观察者 Observable - 被观察者调用
Observable.subscribe()
方法,订阅观察者 Subscriber - 被观察者调用观察者的
Subscriber.onNext()
方法,让观察者接收到事件 - 这时观察者调用通过
next()
方法输出事件内容 - 被观察者调用
Subscriber.onCompleted()
方法,用来通知观察者事件已经结束 - 这时观察者调用
complete()
方法,用来结束事件流.
这在设计模式上一般称为观察者模式,也叫发布-订阅模式: 定义了对象之间的一对多的依赖关系,让多个观察者对象同时监听某一个主题对象(被观察者),这个主题对象在状态发生变化时,会通知所有观察者对象执行更新方法。
特点: 观察者和被观察者之间是松散耦合的,观察者和被观察者之间建立了一套触发机制。
在这里我们对于 Hystrix 的事件源 observable 提到了两个不同的概念:Hot Observable 和 Cold 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 方法来通知中止请求。
- 返回成功的响应
当 Hystrix 命令执行成功之后,它会将处理结果直接返回或是以 Observable 的形式 返回。而具体以哪种方式返回取决于之前第 2 步中我们所提到的对命令的 4 种不同执行方 式,下图中总结了这 4 种调用方式之间的依赖关系。我们可以将此图与在第 2 步中对前两 者源码的分析联系起来,并且从源头 toObservable ()来开始分析。
execute()
– 以与queue()
相同的方式获取一个 Future,然后在这个 Future 上调用get()
来获取由 Observable 发出的单个值queue()
– 将 Observable 转换为一个 BlockingObservable,以便它可以被转换为一个 Future,然后返回这个 Futureobserve()
– 在toObservable()
产生原始 Observable 之后立即订阅它,让命令能够马上开始异步执行,并返回一个 Observable 对象,当调用它的 subscribe 时,将重新产生结果和通知给订阅者。toObservable()
– 返回最原始的 Observable,必须通过订阅它才会真正触发命令的执行流程。
断路器原理
断路器在 HystrixCommand 和 HystrixObservableCommand 执行过程中起到了举足轻重的作用,它是 Hystrix 的核心部件。
断路器有三种状态:
- 关闭状态(Closed):断路器关闭,所有请求会被发送到服务端。
- 打开状态(Open):当触发熔断阈值后,断路器打开,所有请求都不会被发送到服务端,直接返回错误。
- 半开状态(Half-Open):断路器半开,部分请求会被发送到服务端,如果请求成功,断路器关闭,如果请求失败,断路器打开。
- 当断路器打开后,会启动一个定时器,定时器到期后,断路器进入半开状态。默认情况下,断路器会在 5 秒后进入半开状态。
- 当断路器处于半开状态时,如果请求成功,断路器关闭,如果请求失败,断路器打开。
默认触发熔断并造成断路器打开的条件是:
- 快照时间窗(默认 10 秒)
- 请求总数阈值(默认 20)
- 错误百分比阈值(默认 50%)
涉及断路器的三个参数:快照时间窗、请求总数阀值、错误百分比阀值。
-
快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的 10 秒。它是一个滚动时间窗. 每过一段时间,比如 10 秒,就会统计最近 10 秒内的请求总数,如果请求数量超过了阀值,就会根据错误百分比来判断是否需要打开断路器。
-
请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为 20,意味着在 10 秒内,如果该 hystrix 命令的调用次数不足 20 次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
-
错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了 30 次调用,如果在这 30 次调用中,有 15 次发生了超时异常,也就是超过 50%的错误百分比,在默认设定 50%阀值情况下,这时候就会将断路器打开。
配置断路器参数:
1 2 3 4 5 6 |
@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 官方文档中关于断路器的详细执行逻辑,可以帮助我们理解断路器的工作流程。
- 假设大量的请求数量超过了
HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
的阈值(默认 20);或者在滚动时间窗口内,失败的请求数量占总请求数量的百分比超过了HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
的阈值(默认 50%),那么断路器将会被打开,此时所有的请求都会被拒绝(失败), 熔断器将会从闭合状态变成打开状态;
- 在熔断器处于打开状态的期间,所有对这个依赖进行的调用都会短路,即不进行真正的依赖调用,快速失败;
在等待(冷却)的时间超过HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
的值(默认 5 秒)后,断路器将会进入半开状态,此时断路器将会允许一个请求去调用依赖,如果这个请求调用成功,断路器将会闭合, 后续依赖调用可正常执行。,如果这个请求调用失败,断路器将会继续打开,继续等待下一次的半开状态(冷却)。
- 这里
10 1-second "buckets"
的意思是,每 1 秒,会统计最近 10 秒内的请求总数,如果请求数量超过了阀值,就会根据错误百分比来判断是否需要打开断路器。
这个桶数可以通过 HystrixCommandProperties.metricsRollingStatisticalWindowBuckets()
来配置,默认为 10 个桶,每个桶 1 秒,所以默认的快照时间窗为 10 秒。
现在我们来看看断路器 HystrixCircuitBreaker
的定义:
1 2 3 4 5 6 7 8 9 10 11 12 |
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
的关系集合:1private static ConcurrentHashMap<String, HystrixCircuitBreaker> 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
:断路器打开或是上一次测试的时间 戳。
HystrixCircuitBreakerlmpl
对 HystrixCircuitBreaker
接口的各个方法实现如下所示。
isOpen ()
:判断断路器的打开/关闭状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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():判断请求是否被允许,这个实现非常简单。
1 2 3 4 5 6 7 8 9 10 |
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()
是用来做什么的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 则使用该模式实现线程池的隔离, 它会为每一个依赖服务创建一个独立的线程池,这样就算某个依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的依赖服务。
- 总之,通过对依赖服务实现线程池隔离,可让我们的应用更加健壮,不会因为个别依赖服务出现问题而引起非相关服务的异常。同时,也使得我们的应用变得更加灵活,可以在不停止服务的情况下,配合动态配置刷新实现性能配置上的调整。
虽然线程池隔离的方案带来如此多的好处,但是很多使用者可能会担心为每一个依赖服务都分配一个线程池是否会过多地增加系统的负载和开销。对于这一点,使用者不用过于担心,因为这些顾虑也是大部分工程师们会考虑到的,Netflix 在设计 Hystrix 的时候,认为线程池上的开销相对于隔离所带来的好处是无法比拟的。同时,Netflix 也针对线程池的 开销做了相关的测试,以用结果打消 Hystrix 实现对性能影响的顾虑。
下图是 Netflix Hystrix 官方提供的一个 Hystrix 命令的性能监控图,该命令以每秒 60 个请求的速度(QPS)对一个单服务实例进行访问,该服务实例每秒运行的线程数峰值为 350 个。
每种情况都有两个曲线,一个是未使用线程池隔离,一个是使用了线程池隔离。根据图表统计,不难得出如下结论:
比较情况 | 未使用线程池隔离 | 使用了线程池隔离 | 耗时差距 |
---|---|---|---|
中位数 | 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 命令的定义:
1 2 3 4 5 6 |
@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
均可, 如:
1 2 3 4 5 6 7 8 |
/** * 服务降级方法 * @param param 源方法传入的参数 * @return 和源方法的返回值类型保持一致 */ public String showAppInfoFallback(String param) { return "服务器繁忙,请稍后再试o(╥﹏╥)o" + param; } |
- 在上面的例子中,假如 showAppInfoFallback 方法实现的并不是一个稳定逻辑(有可能会再次进行网络请求),它依然可能会发生异常,那么我们也可以为它添加@HystrixCommand 注解以生成 Hystrix 命令,同时使用 fallbackMethod 来指定服务降级逻辑,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@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); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * 服务降级方法 * @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 参数,比如:
1 2 3 4 5 6 7 |
@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 对象的定义,这样在方法内部就可以获取触发服务降级的具体异常内容了,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@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
注解的 CommandKey
、groupKey
以及 threadPoolKey
属性即可设置命令名称、分组以及线程池划分,比如我们可以像下面这样进行设置:
1 2 3 4 5 6 7 8 9 10 11 12 |
@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 的一些针对具体命令和线程池的相关配置,比如我们可以像下面这样进行设置:
1 2 3 4 5 6 7 8 9 10 11 12 |
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方法的第一行加上如下代码:
1 2 |
//初始化Hystrix请求上下文 @Cleanup("shutdown") HystrixRequestContext.initializeContext(); |
@Cleanup
是 lombok注解, 可以自动生成释放资源的代码,默认是调用资源的close()
方法,也可以自己指定释放的方法
2、使用Filter方式:
在启动类加入@ServletComponentScan
注解, 指定
创建HystrixRequestContextServletFilter.java
,实现Filter
接口,在doFilter
方法中添加方法1中的那一行代码,并在一次请求结束后关掉这个上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@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() { } } |
在应用启动类上开启包扫描:
1 |
@ServletComponentScan(basePackages = {"com.niit.order.filter"}) |
有了过滤器后, 每个Controller方法中的
HystrixRequestContext.initializeContext();
代码就可以去掉了.
如果没有为 HystrixRequestContext 进行初始化,那么系统将报如下错误:
这样, Hystrix 的缓存设置是针对单次请求的: 因为每次请求来之前都执行了 HystrixRequestContext.initializeContext();
进行初始化,每请求一次 controller 就会走一次 filter,上下文又会初始化一次,前面缓存的就失效了,又得重新来。所以如果测试缓存,得在 Controller 请求中多次调用那个加了缓存的 HystrixCommand 命令。
Hystrix 的书上写的是:在同一用户请求的上下文中,相同依赖服务的返回数据始终保持一致。在当次请求内对同一个依赖进行重复调用,只会真实调用一次。在当次请求内数据可以保证一致性。
测试请求缓存
通过注解为请求命令开启缓存功能非常简单,如下例所示,我们只需添加@CacheResult 注解即可。当该依赖服务被调用并返回 User 对象时,Hystrix 会将该结果置入请求缓存中,而 它的缓存 Key 值默认会使用所有的参数,也就是这里 String 类型的 id 值。若要为请求命令指定具体的缓存 Key 生成规则,我们可以使用@CacheResult 的 cacheKeyMethod 属性来指定具体的生成函数;也可以通过使 @CacheKey 注解 在方法参数中指定用于组装缓存 Key 的元素。这里我们使用 @CacheKey 注解(由于只有一个参数, 这个注解其实也可以直接省略):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
@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 "测试"; } |
测试
1 2 3 4 5 6 7 8 9 10 11 12 |
未走缓存 ↓ 123 未走缓存 ↓ 123 未走缓存 ↓ 321 清除缓存 ↓ 123 未走缓存 ↓ 123 未走缓存 ↓ 321 |
发现所有调用都没有走缓存.
实践中发现, 必须通过实例来调用标记缓存的方法缓存才会生效, 也就是说必须通过new 或者自动装配的方式先获取标记缓存的方法所在类的实例, 然后通过实例调用缓存的方法, 缓存才可以生效.
修改如下:
把缓存方法放到service层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@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; } } |
测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@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 "测试"; } |
测试接口:
1 |
GET http://localhost:9010/consumer/hello/test |
输出:
1 2 3 4 5 6 7 8 9 10 |
未走缓存 ↓ 123 123 未走缓存 ↓ 321 清除缓存 ↓ 123 未走缓存 ↓ 123 321 |
使用cacheKeyMethod 定义缓存 Key
当使用注解来定义请求缓存时,若要为请求命令指定具体的缓存 Key 生成规则,我们可以使用@CacheResult 的 cacheKeyMethod 属性来指定具体的生成函数;也可以通过使 @CacheKey 注解 在方法参数中指定用于组装缓存 Key 的元素。
需要注意,@CacheKey 注解的优先级比 cacheKeyMethod 的优先级低,如果己经使用 cacheKeyMethod 指定缓存 Key 的生成函数,那么@CacheKey 注解不会生效。
使用 cacheKeyMethod 方法的示例如下,它通过在Hystrix命令的同一个类中定义一个专门生成 Key 的方法,并用@CacheResult 注解的 cacheKeyMethod 方法来指定它即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@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 2 3 4 5 6 7 8 9 10 11 |
/** * 使用注解清除缓存 方式1 * * @CacheRemove 必须指定commandKey才能进行清除指定缓存 */ @CacheRemove(commandKey = "commandKey1", cacheKeyMethod = "getCacheKey") @HystrixCommand public void flushCacheByAnnotation1(Long id) { log.info("请求缓存已清空!"); //这个@CacheRemove注解直接用在更新方法上效果更好 } |
需要注意的是,@CacheRemove
注解的 commandKey 属性是必须要指定的,它用来指明需要使用请求缓存的请求命令,因为只有通过该属性的配置,Hystrix 才能找到正确的请求命令缓存位置。
测试控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
@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); } } |
测试:
1 |
GET http://localhost:9010/hystrix/cache/cacheAnnotation1 |
日志输出:
请求合并
微服务架构中的依赖通常通过远程调用实现,而远程调用中最常见的问题就是通信消耗与连接数占用。在高并发的情况之下,因通信次数的增加,总的通信时间消耗将会变得不那么理想。同时,因为依赖服务的线程池资源有限,将出现排队等待与响应延迟的情况。所以在资源有限并且短时间内会产生高并发请求的时候,为避免连接不够用而引起的延迟可以考虑使用请求合并器的方式来处理和优化优化这两个问题,Hystrix 提供了 HystrixCollapser 来实现请求的合并,以减少通 信消耗和线程数的占用。
HystrixCollapser 实现了在 HystrixCommand 之前放置一个合并处理器,将处于一个很短的时间窗(默认 10
毫秒)内对同一依赖服务的多个请求进行整合并以批量方式 发起请求的功能(服务提供方也需要提供相应的批量实现接口)。通过 HystrixCollapser 的封装,开发者不需要关注线程合并的细节过程,只需关注批量化服务和处理。
在快速入门的例子中,我们使用@HystrixCommand
注解优雅地实现了 HystrixCommand 的定义,那么对于请求合并器是否也可以通过注解来定义呢?答案是肯定的!
使用注解进行请求合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@GetMapping("/findUser/{id}") @HystrixCollapser( batchMethod = "findAll", collapserProperties = { @HystrixProperty(name = "timerDelayInMilliseconds", value = "100") }) public User findUser(@PathVariable("id") String id) { return null; } @HystrixCommand() public List<User> findAll(List<String> 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}
依赖服务之前设置了一个批量请求合并器。
测试:
1 2 3 4 5 6 |
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输出日志:
1 2 3 4 5 6 |
请求合并的ID: [1] 请求合并的ID: [2] 请求合并的ID: [3] 请求合并的ID: [4] 请求合并的ID: [5] 请求合并的ID: [6] |
可见并没有发生请求合并.
为什么请求合并没有生效
需要注意:
@HystrixCollapser
中有scope
属性,scope
的取值为REQUEST
,GLOBAL
。REQUEST
范围只对一个request请求内的多次服务请求进行合并GLOBAL
是多单个应用中的所有线程的请求中的多次服务请求进行合并。
- 同步请求,不会进行请求合并。只有异步请求才会发生请求合并
修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@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<User> findAll(List<String> 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); } |
测试, 可以使用线程池来模拟异步请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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输出日志:
1 |
请求合并的ID: [2, 6, 5, 3, 1, 4] |
main方法输出日志:
1 2 3 4 5 6 7 |
{"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 个请求,那么这样的依赖服务不适合使用请求合并器。这种情况不但不能提升系统性能,反而会成为系统瓶颈,因为每个请求都需要多消耗一个时间窗才响应。相反,如果一个时间窗内具有很高 的并发量,并且服务提供方也实现了批量处理接口,那么使用请求合并器可以有效减少网络连接数量并极大提升系统吞吐量,此时延迟时间窗所增加的消耗就可以忽略不计了。
属性详解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
@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<String> result = restTemplate.getForEntity("http://cloud-eureka-client/hello", String.class); return result.getBody(); } |
在之前介绍 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
属性来设置:
1 2 3 |
@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
属性来设置,比如:
1 2 3 |
@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 步:
-
创建一个标准的 Spring Boot 工程,命名为 hystrix-dashboard。
-
编辑 pom.xml,具体依赖内容如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!--引入Hystrix--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <!--Hystrix仪表盘--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency> <!--actuator端点--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> |
-
为应用主类加上@EnableHystrixDashboard,启用 Hystrix Dashboard 功能。
-
根据实际情况修改 application.properties 配置文件,比如选择一个未被占用的端口等,此步不是必需的。
12server:port: 2022
到这里我们己经完成了基本配置,接下来可以启动该应用,并访问 http://localhost:2022/hystrix
。可以看到如下页面:
这是 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
:12345678910<!--引入Hystrix--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency><!--actuator端点--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
-
确保在服务实例的主类中己经使用
@EnableCircuitBreaker
注解,开启了断路器功能。 -
在配置文件中开启端点功能
123456#暴露全部的监控信息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 按钮,可以看到如下页面。
回到监控页面,我们来详细说说其中各元素的具体含义。
- 可以在监控信息的左上部找到两个重要的图形信息:一个实心圆和一条曲线。
- 实心圆:其有两种含义。通过颜色的变化代表了实例的健康程度,如下图所示,它的健康度从绿色、黄色、橙色、红色递减。该实心圆除了颜色的变化之外, 它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,我们可以在大量的实例中快速发现故障实例和高压力实例。
- 曲线:用来记录 2 分钟内流量的相对变化,可以通过它来观察流量的上升和下降趋势。
- 其他一些数量指标如下图所示。
- 实心圆: 代表了最近 10 秒内请求的总数。其颜色代表了请求的健康程度,如下图所示,它的健康度从绿色、黄色、橙色、红色递减。
- 七种颜色: 代表了最近 10 秒内请求的状态,分别是:绿色代表成功,黄色代表超时,橙色代表拒绝,红色代表失败,灰色代表断路器打开,蓝色代表短路,紫色代表线程池拒绝。
- 曲线: 代表了最近 10 秒内请求的相对变化,可以通过它来观察请求的上升和下降趋势。
- Circle Breaker: 代表了最近 10 秒内断路器的打开和关闭情况,当断路器打开时,该实心圆会变成红色,当断路器关闭时,该实心圆会变成绿色。
通过本节内容我们己经能够使用 Hystrix Dashboard 来对单个实例做信息监控了,但是在分布式系统中,往往有非常多的实例需要去维护和监控。到目前为止,我们能做的就是通过开启多个窗口来监控多个实例,很显然这样的做法并不合理。在下一节中,我们将介绍利用 Turbine 和 HystrixDashboard 配合实现对集群的监控。
活动 5.2 : 基于图形化的 DashBoard(仪表板)监控
Turbine 集群监控
在上一节介绍 Hystrix Dashboard 的首页时,我们提到过除了可以开启单个实例的监控 页面之外,还有一个监控端点/turbine . stream 是对集群使用的。从端点的命名中,可猜测到这里我们将引入 Turbine,通过它来汇集监控信息,并将聚合后的信息提供给 Hystrix Dashboard 来集中展示和监控。
构建监控聚合服务
下面我们将在上一节内容的基础上做一些扩展,通过引入 Turbine 来聚合多个服务的监控信息,并在 Hystrix Dashboard 上面展示。
具体实现步骤如下:
- 创建一个标准的 Spring Boot 工程,命名为 hystrix-turbine。
-
编辑 pom.xml,引入相关依赖。
123456789101112131415161718192021222324252627<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-turbine</artifactId><!--它默认依赖的是EurekaClient,需要排除,随后引入Nacos相关依赖即可--><exclusions><exclusion><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId></dependency><!--引入Nacos Client即可--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency></dependencies>
-
创建应用主类 TurbineApplication,并使用
@EnableTurbine
注解幵启 Turbine。12345678@SpringBootApplication@EnableHystrixDashboard@EnableTurbinepublic class TurbineApplication {public static void main(String[] args) {SpringApplication.run(TurbineApplication.class,args);}}
-
在 application.yml 配置文件中加入 Nacos 和 Turbine 的相关配置
12345678910111213141516171819202122232425262728293031server:port: 2023spring:application:name: turbinecloud:nacos:server-addr: localhost:8848 #nacos 服务端地址turbine:app-config: orderservice,userservice #监控服务列表aggregator:cluster-config: default #聚合集群名称cluster-name-expression: new String('default') #聚合集群名称combine-host-port: truehystrix: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
的监控,我们可以看到如下页面:
监控面板
在断路器原理的介绍中,我们多次提到关于请求命令的度量指标的判断。这些度量指标都是HystrixCommand 和 HystrixObservableCommand 实例在执行过程中记录的重要信息,它们除了在Hystrix 断路器实现中使用之外,对于系统运维也有非常大的帮助。 这些指标信息会以“滚动时间窗”与“桶”结合的方式进行汇总,并在内存中驻留一段时间,以供内部或外部进行查询使用,Hystrix 仪表盘就是这些指标内容的消费者之一。
通过之前的内容,我们己经体验到了 Spring Cloud 对 Hystrix 的优雅整合。除此之外,SpringCloud 还完美地整合了它的仪表盘组件 Hystrix Dashboard,它主要用来实时监控 Hystrix 的各项指标信息。通过 Hystrix Dashboard 反馈的实时信息,可以帮助我们快速发现系统中存在的问题,从而及时地采取应对措施。
本节中我们将在 Hystrix 入门例子的基础上,构建一个 Hystrix Dashboard项目 来对 Order-Service 服务实现监控。
通过actuator端点产生统计信息
微服务如果想要被监控, 需要暴露相应端点, 这里为了方便暴露了所有端点.
1 2 3 4 5 6 |
#暴露全部的监控信息 management: endpoints: web: exposure: include: "*" |
这里使用了
management.endpoints.web.exposure.include
属性来开启所有的端点,也可以通过management.endpoints.web.exposure.exclude
属性来关闭某些端点。具体的端点信息可以参考 Spring Boot 的官方文档。"*" 表示开启所有端点,也可以使用具体的端点名称,比如 "hystrix.stream"。
另外因为 springboot2.0起监控的默认路径不是 "/hystrix.stream", 需要在自己的项目里面手动配置映射路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Configuration public class OrderServiceConfiguration { @Bean public ServletRegistrationBean getServlet(){ HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean<HystrixMetricsStreamServlet> registrationBean = new ServletRegistrationBean<>(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/actuator/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } } |
以orderservice为例进行测试:
1 |
http://localhost:9010/actuator/hystrix.stream |
userservice也是一样.
创建turbine项目聚合统计信息
如果需要聚合多个微服务信息显示在面板上, 需要是创建一个turbine项目, 并进行统计信息的聚合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>my-turbine</artifactId> <version>0.0.1-SNAPSHOT</version> <name>my-turbine</name> <description>my-turbine</description> <parent> <groupId>com.niit</groupId> <artifactId>chapter05</artifactId> <version>1.0</version> </parent> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-turbine</artifactId> <!--它默认依赖的是EurekaClient,需要排除,随后引入Nacos相关依赖即可--> <exclusions> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </exclusion> </exclusions> </dependency> <!--引入Nacos Client即可--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies> </project> |
由于turbine需要借助nacos获取服务信息, 需要使用spring-cloud-starter-alibaba-nacos-discovery
依赖.
启动类:
1 2 3 4 5 6 7 8 9 10 11 |
@SpringBootApplication @EnableHystrix @EnableTurbine // 开启turbine, 用于聚合监控数据从而实现集群监控 public class MyTurbineApplication { public static void main(String[] args) { SpringApplication.run(MyTurbineApplication.class, args); } } |
配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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
:
创建dashboard项目实现监控
新创建一个项目 my-dashboard , 用于展示单体项目或者聚合项目的统计信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>my-dashboard</artifactId> <version>0.0.1-SNAPSHOT</version> <name>my-dashboard</name> <description>my-dashboard</description> <parent> <groupId>com.niit</groupId> <artifactId>chapter05</artifactId> <version>1.0</version> </parent> <dependencies> <!--引入Hystrix--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <!--Hystrix仪表盘--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency> </dependencies> </project> |
启动类:
1 2 3 4 5 6 7 8 9 |
@SpringBootApplication @EnableHystrixDashboard public class MyDashboardApplication { public static void main(String[] args) { SpringApplication.run(MyDashboardApplication.class, args); } } |
配置:
1 2 3 4 5 6 7 |
server: port: 2022 hystrix: dashboard: proxy-stream-allow-list: '*' #允许任何 IP 地址访问仪表板 |
启动项目后, 访问:http://localhost:2022/hystrix
这是 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
监控聚合项目:
在上面输入框输入http://localhost:2023/turbine.stream
, 点击Monitor Stream
按钮开始监控:
解读监控内容:
一开始是没有监控信息的, 直到请求发生:
各个指标的含义:
熔断如果触发, 则可以看到红色圆圈:
-
实心圆:其有两种含义。通过颜色的变化代表了实例的健康程度,绿色表示正常,红色表示熔断。该实心圆除了颜色的变化之外, 它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,我们可以在大量的实例中快速发现故障实例和高压力实例。
-
Circuit:表示熔断器的状态, Closed(绿字)表示熔断器闭合接收请求, Open(红字)表示熔断器打开走熔断降级流程. 正常的情况实心圆是淡绿色, 熔断器显示闭合; 当熔断发生时, 实心圆红色, 熔断器打开;当熔断器处于半开状态时, 实心圆回复淡绿色, 熔断器显示打开.
-
曲线:代表时间窗口内流量变化,可以通过它来观察流量的上升和下降趋势。
-
六个数字代表不同状态的请求数, 灰色百分号为失败率.
不同颜色代表的含义:
其他文字代表的含义:
-
Cluster 集群每秒请求数
-
Host 主机每秒请求数
-
Hosts 主机数量
-
Median 响应时间的中位数, Mean 平均延迟, 90th 表示90百分位的延迟情况.
可以点击sort右边的排序字段进行排序:
Views: 10