简介
Hystrix设计目标
- 对来自依赖的延迟和故障进行防护和控制
- 阻止故障的连锁反应
- 快速失败并迅速恢复
- 回退并优雅降级
- 提供近实时的监控与告警
Hystrix遵循的设计原则
- 防止任何单独的依赖耗尽资源(线程)
- 过载立即切断并快速失败,防止排队
- 尽可能提供回退以保护用户免受故障
- 使用隔离技术(例如隔板,泳道和断路器模式)来限制任何一个依赖的影响
- 通过近实时的指标,监控和告警,确保故障被及时发现
- 通过动态修改配置属性,确保故障及时恢复
- 防止整个依赖客户端执行失败,而不仅仅是网络通信
Hystrix如何实现这些设计目标
- 使用命令模式将所有对外部服务(或依赖关系)的调用包装在HystrixCommand或HystrixObservableCommand对象中,并将该对象放在单独的线程中执行。
- 每个依赖都维护着一个线程池(或信号量),线程池被耗尽则拒绝请求(而不是让请求排队)。
- 记录请求成功,失败,超时和线程拒绝。
- 服务错误百分比超过了阈值,熔断器开关自动打开,一段时间内停止对该服务的所有请求。
- 请求失败,被拒绝,超时或熔断时执行降级逻辑。
- 近实时地监控指标和配置的修改。
Hystrix能做什么
- 通过hystrix可以解决雪崩效应问题,它提供了资源隔离、降级机制、融断、缓存等功能。
- 资源隔离:包括线程池隔离和信号量隔离,限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。
- 降级机制:超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。
- 融断:当失败率达到阀值自动触发降级(如因网络故障/超时造成的失败率高),熔断器触发的快速失败会进行快速恢复。
- 缓存:返回结果缓存,后续请求可以直接走缓存。
- 请求合并:可以实现将一段时间内的请求(一般是对同一个接口的请求)合并,然后只对服务提供者发送一次请求。
隔离模式
Hystrix提供了两种隔离模式:线程池隔离模式、信号量隔离模式。
线程池隔离模式:使用一个线程池来存储当前请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求先入线程池队列。这种方式要为每个依赖服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)
信号量隔离模式:使用一个原子计数器(或信号量)记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃该类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)
降级
服务降级的目的保证上游服务的稳定性,当整体资源快不够了,将某些服务先关掉,待渡过难关,再开启回来。
快速模式:如果调用服务失败了,那么立即失败并返回。
故障转移:如果调用服务失败了,那么调用备用服务,因为备用服务也可能失败,所以也可能有再下一级的备用服务,如此形成一个级联。例如:如果服务提供者不响应,则从缓存中取默认数据。
主次模式:举个例子,开发中需要上线一个新功能,但为了防止新功能上线失败可以回退到老的代码,我们会做一个开关比做一个配置开关,可以动态切换到老代码功能。那么Hystrix它是使用通过一个配置来在两个command中进行切换。
熔断器原理
请求合并器
微服务架构中通常需要依赖多个远程的微服务,而远程调用中最常见的问题就是通信消耗与连接数占用。在高并发的情况之下,因通信次数的增加,总的通信时间消耗将会变得越来越长。同时,因为依赖服务的线程池资源有限,将出现排队等待与响应延迟的清况。
为了优化这两个问题,Hystrix 提供了HystrixCollapser来实现请求的合并,以减少通信消耗和线程数的占用。
HystrixCollapser实现了在 HystrixCommand之前放置一个合并处理器,将处于一个很短的时间窗(默认10毫秒)内对同一依赖服务的多个请求进行整合,并以批量方式发起请求的功能(前提是服务提供方提供相应的批量接口)。HystrixCollapser的封装多个请求合并发送的具体细节,开发者只需关注将业务上将单次请求合并成多次请求即可。
需要注意请求合并的额外开销:用于请求合并的延迟时间窗会使得依赖服务的请求延迟增高。比如,某个请求不通过请求合并器访问的平均耗时为5ms,请求合并的延迟时间窗为lOms (默认值), 那么当该请求设置了请求合并器之后,最坏情况下(在延迟时间 窗结束时才发起请求)该请求需要15ms才能完成。
合并请求存在额外开销,所以需要根据依赖服务调用的实际情况决定是否使用此功能,主要考虑下面两个方面:
请求命令本身的延迟:
对于单次请求而言,如果[单次请求平均时间/时间窗口]越小,对于单次请求的性能形象越小。如果依赖服务的请求命令本身是一个高延迟的命令,那么可以使用请求合并器,因为延迟时间窗的时间消耗显得微不足道了。
并发量:
时间窗口内并发量越大,合并求情的性能提升越明显。如果一个时间窗内只有少数几个请求,那么就不适合使用请求合并器。相反,如果一个时间窗内具有很高的并发量,那么使用请求合并器可以有效减少网络连接数量并极大提升系统吞吐量,此时延迟时间窗所增加的消耗就可以忽略不计了。
Hystrix工作流程
简单例子
在文章中做些简单修改:/2020/12/cloud_springcloud_eureka/
注册中心:
服务提供方,分别启动2个服务实例,
1 2 3
| java -jar demo.jar --server.port=9001
java -jar demo.jar --server.port=9002
|
服务消费方:
未加入断路器之前,关闭其中一个服务,如9001的实例,发送GET请求到: http://127.0.0.1:9101/ribbon-consumer ,可以获得以下信息:
1 2 3
| ... I/O error on GET request for "http://localhost:8888/ribbon-consumer/default": Connection refused: connect; ...
|
那么我们现在加入hystrix,在 demo-consumer工程中,pom.xml,加入内容:
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
|
pom.xml所有内容:
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 70 71 72 73 74 75 76 77 78 79 80
| <?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <!-- springboot版本与springcloud须匹配 --> <version>2.1.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>demo-consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo-consumer</name> <description>Demo project for Spring Boot</description>
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <!-- springboot版本与springcloud须匹配 --> <spring.boot.version>2.1.4.RELEASE</spring.boot.version> <spring.cloud.version>Greenwich.SR1</spring.cloud.version> <spring.maven.version>2.4.1</spring.maven.version> </properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
</dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring.cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.maven.version}</version> </plugin> </plugins> </build>
</project>
|
创建一个HelloService.java,内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package com.example.democonsumer.service;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
@Service public class HelloService { @Autowired private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "helloFallback") public String helloService() { String memberUrl = "http://HELLO-SERVICE/hello"; String result = restTemplate.getForObject(memberUrl, String.class); System.out.println("访问结果" + result); return result; }
public String helloFallback() { String str = "error:helloFallback"; System.out.println(str); return str; } }
|
ConsumerController.java,修改内容:
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
| package com.example.democonsumer.controller;
import com.example.democonsumer.service.HelloService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.slf4j.Logger; import org.slf4j.LoggerFactory;
@RestController public class ConsumerController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired private HelloService helloService;
@RequestMapping(value = "/ribbon-consumer", method = RequestMethod.GET) public String helloConsumer() {
return helloService.helloService(); }
}
|
DemoConsumerApplication.java加入@EnableCircuitBreaker,内容:
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
| package com.example.democonsumer;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate;
@EnableCircuitBreaker @EnableDiscoveryClient @SpringBootApplication public class DemoConsumerApplication {
@Bean // 让RestTemplate在请求时拥有客户端负载均衡的能力 @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
public static void main(String[] args) { SpringApplication.run(DemoConsumerApplication.class, args); } }
|
加入断路器后,关闭其中一个服务,如9001的实例,发送GET请求到: http://127.0.0.1:9101/ribbon-consumer ,输出信息: