微服务故障导致调用方的对外服务也出现延迟, 若此时调用方的请求不断增加, 最后就会因等待出现故障的依赖方响应形成任务积压, 最终导致自身服务的瘫痪
在分布式架构中,当某个服务单元发生故障。通过断路器的故障监控(类似熔断保险丝), 向调用方返回 一个错误响应, 而不是长时间的等待。 这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
Spring Cloud Hystrix实现了断路器、 线程隔离等一系列服务保护功能
快速入门
启动一个eureka-server工程,端口是1111,单机模式
启动两个hello-server工程,端口号分别是8081和8082
启动一个Ribbon-consumer工程,部分代码如下:
添加POM:
1
2
3
4<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-hystrix-amqp</artifactId>
</dependency>Service类:
1
2
3
4public String helloService() {
return restTemplate.getForEntity("http://HELLO-SERVICE/hello",
String.class).getBody();
}主类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//@EnableCircuitBreaker
public class ConsumerApplication {
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}controller:
1
2
3
4"/ribbon-consumer", method = RequestMethod.GET) (value =
public String helloConsumer() {
return helloService.helloService();
}调用/ribbon-consumer,可以看到轮询访问两个服务,当关闭一个服务后,出现报错:
1
2
3
4
5
6
7
8{
"timestamp": 1584462550780,
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.web.client.ResourceAccessException",
"message": "I/O error on GET request for \"http://HELLO-SERVICE/hello\": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused)",
"path": "/ribbon-consumer"
}打开ConsumerApplication主类EnableCircuitBreaker注解,同时Service类改成:
1
2
3
4
5
6
7
8
9public String helloFallback() {
return "error";
}
"helloFallback") (fallbackMethod =
public String helloService() {
return restTemplate.getForEntity("http://HELLO-SERVICE/hello",
String.class).getBody();
}断掉一个服务后,返回的内容变成了helloFallback返回的内容,也即是error
注意:可以使用SpringCloudApplication来直接注解主类,替换上面的注释,原因在于它包含了其他的注解
1
2
3
4
5
6
7
8
9(ElementType.TYPE)
(RetentionPolicy.RUNTIME)
public SpringCloudApplication {
}我们修改服务测得实现,加上睡眠,然后访问消费方接口,同样会得到error错误
1
2
3
4
5
6
7
8
9
10
11
12"/hello", method = RequestMethod.GET) (value =
public String hello() throws Exception {
ServiceInstance instance = client.getLocalServiceInstance();
// 测试超时触发断路器
int sleepTime = new Random().nextInt(3000);
logger.info("sleepTime:" + sleepTime);
Thread.sleep(sleepTime);
logger.info("/hello, host:" + instance.getHost() + ", service_id:" + instance.getServiceId());
return "Hello World";
}原因在于我们配置了超时时间,默认是2S:
1
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
原理分析
工作流程
创建HystrixCommand或HystrixObservableCommand对象
首先, 构建一个HystrixCommand或是HystrixObservableCommand对象,用来表示操作请求, 同时传递所有需要的参数。从命名可以看出来是采用命令模式,命令模式基本形式如下:
1 | /** |
Receiver: 接收者, 它知道如何处理具体的业务逻辑。
Command: 抽象命令, 它定义了一个命令对象 应具备的一系列命令操作, 比如execute()、 undo() 、 redo()等。 当命令操作被调用的时候就会触发接收者去做具体命令对应的业务逻辑。
ConcreteComrnand: 具体的命令实现, 在这里它绑定了命令操作与接收者之间的关系, execute()命令的实现委托给了 Receiver的 action()函数。
Invoker: 调用者, 它持有 一个命令对象, 并且可以在需要的时候通过命令对象完成具体的业务逻辑
调用者Invoker与操作者Receiver通过 Command命令接口实现了解耦(传统的实现比如传递命令的枚举,然后判断命令类型去做不同的操作,这种不方便扩展)
Invoker和Receiver的关系非常类似于 “请求-响应 ” 模式, 所以它比较适用于实现记录日志、 撤销操作、 队列请求等
命令执行
Hystrix在命令执行时会根据创建的Command对象以及具体的情况来选择一个执行。其中HystrixCommand实现了下面两个执行方式:
execute(): 同步执行,从依赖的服务 返回一个单一的结果对象, 或是在发生错误的时候抛出异常。
queue(): 异步执行, 直接返回一个Future对象, 其中包含了服务执行结束时要返回的单一结果对象
R value = command.execute() ;
Future\fValue = command.queue();
HystrixObservableCommand实现了另外两种执行方式:
observe(): 返回Observable对象,它代表了操作的多个结果,它是一个HotObservable
toObservable(): 同样会返回Observable对象, 也代表了操作的多个结果,但它返回的是 一个Cold Observable
Observable\
ohValue = command.observe(};
Observable\ocValue = command. toObservable(};
Observable,可以把它理解为 “ 事件源 ” 或是 “ 被观察者” , 与其对应的Subscriber对象以理解为 “ 订阅者”或是 “ 观察者”
Observable用来向订阅者Subscriber对象发布事件, Subscriber对象则在接收到事件后对其进行处理, 而在这里所指的事件通常就是对依赖 服务的调用
一个Observable可以发出多个事件, 直到结束或是发生异常
Observable 对象每发出一个事件,就会调用对应观察者Subscriber对象的onNext()方法
每一个Observable的执行,最后一定会通过调用 Subscriber. onCompleted()或者Subscriber.onError()来结束该事件的操作流
下面是代码案例
在该示例中, 创建了一个简单的事件源observable, 一个对事件传递内容输出的订阅者 subscriber, 通过 observable.subscribe(subscriber) 来触发事件的发布
在这里我们对于事件源 observable 提到了两个不同的概念:Hot Observable和Cold Observable,分别对应分别对应了上面 command. observe()和command.toObservable()
所以对于 Hot Observable 的每一个 “ 订阅者 ” 都有可能是从 “ 事件源 ” 的中途开始的, 并可能只是看到了整个操作的局部过程。 而 Cold Observable 在没有 “ 订阅者 ” 的时候并不会发布事件, 而是进行等待, 直到有 “ 订阅者 ” 之后才发布事件, 所以对于 Cold Observable 的订阅者, 它可以保证从一开始看到整个操作的全部过程
实际上execute()、 queue()也都使用了Rx.Java来实现:
execute()是通过queue()返回的异步对象 Future
的get()方法来实现同步执行的。 该方法会等待任务执行结束, 然后获得R类型的结果进行返回 queue()则是通过toObservable()来获得一个Cold Observable, 并且通过toBlocking()将该 Observable转换成BlockingObservable, 它可以把数据以阻塞的方式发射出来。 而toFuture 方法则是把 BlockingObservable转
换为一个Future, 该方法只是创建 一个Fu七ure 返回并不会阻塞, 这使得消费者可以自己决定如何处理异步操作。 而execute()就是直接使用了queue()返回的 Future中的阻塞方法 get()来实现同步操作的。 同时通过这种方式转换的Future要求Observable 只发射一个数据,所以这两个实现都只能返回单一结果
结果是否缓存
若当前命令的请求缓存功能是被启用的, 并且该命令缓存命中, 那么缓存的结果会立即以Observable 对象的形式 返回
断路器是否打开
在命令结果没有缓存命中的时候, Hystrix在执行命令前需要检查断路器是否为打开状态
如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑
如果断路器是关闭的, 那么Hystrix跳到第5步,检查是否有可用资源来执行命令
线程池、请求队列、信号量是否占满
如果与命令相关的线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满, 那么Hystrix也不会执行命令, 而是转接到fallback处理逻辑
选择请求方式
Hystrix会根据我们编写的方法来决定采取什么样的方式去请求依赖服务:
HystrixCommand.run(): 返回一个单一的结果,或者抛出异常
HystrixObservableCommand.construct(): 返回一个Observable对象来发射多个结果,或通过onError发送错误通知
如果run()或construct()方法的执行时间超过了命令设置的超时阙值,Hystrix会转接到fallback处理逻辑
计算断路器的健康度
Hystrix会将 “ 成功 ”、 “ 失败 ”、 “ 拒绝 ”、 “ 超时 ” 等信息报告给断路器, 而断路器会维护一组计数器来统计这些数据
断路器会使用这些统计数据来决定是否要将断路器打开, 来对某个依赖服务的请求进行 “ 熔断/短路 ”,直到恢复期结束。 若在恢复期结束后, 根据统计数据判断如果还是未达到健康指标,就再次 “ 熔断/短路 ”
fallback处理
当命令执行失败的时候, Hystrix会进入fallback尝试回退处理, 我们通常也称该操作为 “ 服务降级”。而能够引起 服务降级处理的清况有下面几种:
当前命令处于 “ 熔断I短路 ” 状态, 断路器是打开的时候
当前命令的线程池、 请求队列或 者信号量被占满的时候
HystrixObservableCommand.construct()或HystrixCommand.run()抛出异常的时候
在服务降级逻辑中, 我们需要实现一个通用的响应结果, 并且该结果的处理逻辑应当是从缓存或是根据一些静态逻辑来获取,而不是依赖网络请求获取。如果一定要在降级逻辑中包含网络请求,那么该请求也必须被包装在HystrixCommand或是HystrixObservableCommand中, 从而形成级联的降级策略, 而最终的降级逻辑一定不是一个依赖网络请求的处理, 而是一个能够稳定地返回结果的处理逻辑
在 HystrixCommand和HystrixObservableCommand中实现降级逻辑时还略有不同
当使用HystrixCommand的时候, 通过实现HystrixCommand.getFallback()来 实现服务降级逻辑
当使用 HystrixObservableCommand 的时候, 通过HystrixObservableCommand.resumeWIthFallback() 实现服务降级逻辑 , 该方法会返回一个Observable对象来发射一个或多个降级结果
如果我们没有为命令实现降级逻辑或 者降级处理逻辑中抛出了异常, Hystrix 依然会返回一个Observable对象, 但是它不会发射任何结果数据, 而是通过onError 方法通知命令立即中断请求,并通过onError()方法将引起命令失败的异常发送给调用者。实现一个有可能失败的降级逻辑是一种非常糟糕的做法, 我们应该在实现降级策略时尽可能避免失败的情况
如果降级执行发现失败的时候,Hystrix会 根据不同的执行方法做出不同的处理:
• execute: 抛出异常。
• queue(): 正常返回Future对象,但是当调用get()来获取结果的时候会抛出异常。
• observe(): 正常返回Observable对象, 当订阅它的时候, 将立即通过调用订阅者的onError方法来通知中止请求。
• toObservable(): 正常返回Observable对象, 当订阅它的时候, 将通过调用订阅者的onError方法来通知中止请求
返回成功响应
当Hystrix命令执行成功之后, 它会将处理结果直接返回或是以Observable 的形式返回
toObservable(): 返回最原始的 Observable, 必须通过订阅它才会真正触发命令的执行流程
observe(): 在toObservable()产生原始Observable 之后立即订阅它, 让命令能够马上开始异步执行 , 并返 回一 个Observable 对象, 当调用它的subscribe 时, 将重新产生结果和通知给订阅者
queue():将 toObservable()产生的原始Observable通过toBlocking()方法转换成BlockingObservable 对象, 并调用它的 toFuture()方法返回异步的Future对象
execute(): 在queue()产生异步结果Future对象之后, 通过调用get()方法阻塞并等待结果的返回
短路器原理
断路器在 HystrixCommand 和 HystrixObservableCommand 执行过程中起到了举足轻重的作用,它是 Hystrix 的核心部件
HystrixCircuitBreakerImpl方法实现
isOpen方法:
1 | public boolean isOpen() { |
allowRequest方法
1 | public boolean allowRequest() { |
markSuccess
该函数用来在 “ 半开路 ” 状态时使用。若 Hystrix 命令调用成功,通过调用它将打开的断路器关闭, 并重置度量指标对象
1 | public void markSuccess() { |
依赖隔离
“ 舱壁模式 ” 对于熟悉 Docker 的读者一定不陌生, Docker 通过 “ 舱壁模式 ” 实现进程的隔离, 使得容器与容器之间不会互相影响。 而 Hystrix 则使用该模式实现线程池的隔离,它会为每 一个依赖服务创建 一个独立的线程池, 这样就算某个依赖服务出现延迟过高的情况, 也只是对该依赖服务的调用产生影响, 而不会拖慢其他的依赖服务
在 Hystrix 中除了可使用线程池之外, 还可以使用信号量来控制单个依赖服务的并发度, 信号量的开销远比线程池的开销小, 但是它不能设置超时和实现异步访问。所以, 只有在依赖服务是足够可靠的情况下才使用信号量
如果将隔离策略参数execution.isolation.strategy设置为SEMAPHORE,Hystrix会使用信号量替代线程池来控制依赖服务的并发
当 Hystrix 尝试降级逻辑时, 它会在调用线程中使用信号量
信号量的默认值为10, 我们也可以通过动态刷新配置的方式来控制并发线程的数量。对于信号量大小的估算方法与线程池并发度的估算类似
使用详解
创建请求命令
Hystrix 命令就是我们之前所说的 HystrixCommand, 它用来封装具体的依赖服务调用逻辑。我们可以通过继承的方式来实现:
通过上面实现的 UserCommand, 我们既可以实现请求的同步执行也可以实现异步执行:
同步执行: User u = new UserCommand(restTemplate, lL) . execute ();
异步执行: Future futureUser = new UserCommand(restTemplate,lL) .queue();
另外, 也可以通过@HystrixCommand 注解来更为优雅地实现 Hystrix 命令的定义,比如:
1 | "helloFallback") (fallbackMethod = |
若要实现异步执行则还需另外定义,比如:
1 |
|
除了传统的同步执行与异步执行之外, 我们还可以将HystrixCommand通过Observable 来实现响应式执行方式。通过调用 observe()和toObservable ()方法可以返回 Observable对象:
1 | //前者返回的是一个Hot Observable, 该命令会在observe ()调用的时候立即执行 |
虽然 HystrixCornrnand 具备了 observe ()和toObservable() 的功能,但是它的实现有一定的局限性,它返回的 Observable 只能发射一次数据,所以 Hystrix 还提供了另外 一个特殊命令封装 HystrixObservableCommand, 通过它实现的命令可以获取能发射多次的 Observable:
1 | public class UserObservableCommand extends HystrixObservableCommand<User> { |
注解实现和上面的类似:
在使用 @HystrixCommand注解实现响应式命 令时, 可以通过observableExecutionMode 参数来控制是使用 observe ()还是 toObservable()的执行方式。该参数有下面两种设置方式:
@HystrixCommand(observableExecutionMode = ObservableExecutionMode. EAGER) : EAGER 是该参数的模式值, 表示使用 observe ()执行方式
@HystrixCommand(observableExecutionMode = ObservableExecutionMode. LAZY): 表示使用 toObservable()执行方式
定义服务降级
fallback 是 Hystrix 命令执行失败时使用的后备方法, 用来实现服务的降级处理逻辑。 Hystrix
会在 run() 执行过程中出现错误、 超时、 线程池拒绝、 断路器熔断等情况时, 执行getFallback ()方法内的逻辑。
在 HystrixObservableCommand实现的Hystrix命令中, 我们可以通过重载resumeW辽hFallback方法来实现服务降级逻辑。 该方法会返回 一个 Observable 对象, 当命令执行失败的时候, Hystrix 会将 Observable中的结果通知给所有的订阅者
在上面的例子中,defaultUser 方法将在 getUserBy工d 执行时发生错误的情况下被执行。若 defaultUser 方法实现的并不是一个稳定逻辑,它依然可能会发生异常, 那么我们也可以为它添加 @HystrixCommand 注解以生成 Hystrix 命令
在实际使用,我们也可以不去实现降级逻辑:
执行写操作的命令:当Hystrix命令是用来执行写操作而不是返回一些信息的时候, 通常情况下这类操作的返回类型是 void 或是为空的 Observable,实现降级的意义不大
执行批处理或离线计算的命令:当Hystrix命令是用来执行批处理程序生成一份报告或是进行任何类型的离线计算时, 那么通常这些操作只需要将错误传播给调用者,然后让调用者稍后重试而不是发送给调用者一个静默的降级处理响应
异常处理
异常传播
在使用注册配置实现 Hystrix 命令时,在 HystrixComrnand 实现的 run() 方法中抛出异常时, 除了HystrixBadRequestException之外,其他异常均会被 Hystrix 认为命令执行失败并触发服务降级的处理逻辑它还支持忽略指定异常类型功能:
当 getUserByld 方法抛出了类型为BadRequestException的异常时, Hystrix 会将它包装在 HystrixBadRequestException 中抛出, 这样就不会触发后续的 fallback 逻辑
异常获取
在以传统继承方式实现的 Hystrix 命令中, 我们可以用 getFallback ()方法通过getExectuionException()放来获异常。注解配置也可以获取异常
命令名称、分组以及线程池划分
以继承方式实现的 Hystrix 命令使用类名作为默认的命令名称,我们也可以在构造函数中通过 Setter 静态类来设置
通过设置命令组, Hystrix会根据组来组织和统计命令的告警、 仪表盘等信息。 那么为什么一定要设置命令组呢?因为除了根据组能实现统计之外, Hystrix 命令默认的线程划分也是根据命令分组来实现的。默认情况下, Hystrix 会让相同组名的命令使用同一个线程池,所以我们需要在创建 Hystrix 命令时为其指定命令组名来实现默认的线程池划分
Hystrix 还提供了HystrixThreadPoolKey 来对线程池进行设置
注解的使用方法:
请求缓存
在高并发的场景之下, Hystrix 中提供了请求缓存的功能
开启缓存请求功能
Hystrix 请 求 缓 存 的使用非常简单, 我 们只需要在实 现 HystrixCommand 或HystrixObservableCommand 时, 通过重载 getCacheKey ()方法来开启请求缓存,比如
通过在 getCacheKey 方法中返回的请求缓存 key 值,Hystrix 会根据 getCacheKey 方法返回的值来区分是否是重复的请求,如果它们的 cacheKey 相同, 那么该依赖服务只会在第 一个请求到达时被真实地调用一次, 另外 一个请求则是直接从请求缓存中返回结果, 所以通过开启请求缓存可以让我们实现的 Hystrix 命令具备下面几项好处:
减少重复的请求数, 降低依赖服务的并发度
在同 一用户请求的上下文中, 相同依赖服务的返回数据始终保持一致
请求缓存在 run() 和 construct ()执行之前生效, 所以可以有效减少不必要的线程开销
清理失效缓存功能
如果请求命令中还有更新数据的写操作, 那么缓存中的数据就需要我们在进行写操作时进行及时处理, 以防止读操作的请求命令获取到了失效的数据
我们可以通过 HystrixRequestCache.clear() 方法来进行缓存的清理
工作原理
由于 getCacheKey 方法在AbstractCommand 抽象命令类中实现,所以我们可以先从这个抽象命令类的实现中看起。getCacheKey 方法默认返回的是 null, 并且从 isRequestCachingEnabled 方法的实现逻辑中我们还可以知
道, 如果不重写 getCacheKey 方法, 让它返回一个非null值, 那么缓存功能是不会开启的
再看下toObservable的实现:
会尝试获取请求缓存
在执行命令缓存操作之前, 我们可以看到已经获得了一个延迟执行的命令结果对象hystrixObservable,如果开启了请求缓存并且getCacheKey返回了具体的Key值, 就将hystrixObservable对象包装成请求缓存结果HystrixCachedObservable的实例对象 toCache, 然后将其放入当前命令的缓存对象中
1 | final boolean requestCacheEnabled = isRequestCachingEnabled(); |
使用注解实现请求缓存
设置请求缓存
定义缓存key
可以使用cacheMethod:
或者注解(如果已经使用了 cacheKeyMethod 指定缓存 Key 的生成函数, 那么@CacheKey 注解不会生效):
也可以使用属性中的字段
缓存清理
@CacheRemove 注解的 commandKey 属性是必须要指定的, 它用来指明需要使用请求缓存的请求命令, 因为只有通过该属性的配置, Hystrix 才能找到正确的请求命令缓存位置
请求合并
Hystrix 提供了 HystrixCollapser 来实现请求的合并, 以减少通信消耗和线程数的占用
HystrixCollapser 实现了在 HystrixCommand 之前放置一个合并处理器, 将处于一个很短的时间窗(默认 10 毫秒)内对同一依赖服务的多个请求进行整合并以批量方式发起请求的功能(服务提供方也需要提供相应的批量实 现接口)。 通 过HystrixCollapser 的封装, 开发者不需要关注线程合并的细节过程, 只需关注批量化服务和处理
下面是一个请求合并的案例:
假设当前微服务 USER-SERVICE提供了两个获取 User 的接口
消费端代码如下:
我们实现将短时间内多个获取单一 User 对象的请求命令进行合并:
第一步,为请求合并的实现准备一个批量请求命令的实现,具体如下:
第二步, 通过继承HystrixCollapser实现请求合并器:
在上面的构造函数中, 我们为请求合并器设置 了时间 延迟属性, 合并器会在该时间窗内收集获取单个User的请 求并 在时间窗结束时进行合并组装成单个批量请求。getRequestArgument 方法返回给定的单个请求参数userId, 而createCommand和mapResponseToRequests是请求合并器的两个核心
使用注解:
通过@HystrixCollapser注解为其创建了合并请求器, 通过batchMethod 属性指定了批量请求的实现方法为findAll方法
使用请求合并的额外开销:
由千请求合并器的延迟时间窗会带来额外开销, 所以我们是否使用请求合并器需要 根据依赖服务调用的实际情况来选择, 主要考虑下面两个方面:
请求命令本身的延迟。 如果依赖服务的请求命令本身是一个高延迟的命令, 那么可以使用请求合并器, 因为延迟时间窗的时间消耗显得微不足道了
延迟时间窗内的并发量:如果一个时间窗内只有1-2个请求, 那么这样的依赖服务不适合使用请求合并器
属性详解
根据实现 HystrixCommand 的不同方式将配置方法分为如下两类:
通过继承实现,可使用Setter对象来对请求命令的属性进行设置, 比如下面的例子:
当 通过注解的方法实现时 , 只需使用 @HystrixCommand 中的 commandProperties 属性来设置:
Hystrix 为我们提供的配置内容和配置方式远不止上面这些, 它提供了非常丰富和灵活的配置方法,下面我们将详解介绍 HystrixPropertiesStrategy实现的各项配置属性。首先了解四个优先级的配置:
Command属性
execution配置控制的是HystrixCommand.run()的执行
fallback配置
其他略:
Hystrix仪表盘
度量指标都是HystrixComrnand和Hys七豆xObservableComrnand实例在执行过程中记录的重要信息, 它们除了在Hystrix断路器实现中使用之外,对于系统运维也有非常大的帮助 。这些指标信息会以 “滚动时间窗 ” 与 “桶 ” 结合的方式进行汇总,并在内存中驻留一段时间,以供内部或外部进行查询使用,Hystrix仪表盘就是这些指标内容的消费者之一。Spring Cloud还完美地整合了它的仪表盘组件Hystrix Dashboard, 它主要用来实时监控Hystrix的各项指标信息。入门案例:
引入依赖:
主类:
1 |
|
访问:http://localhost:port/hystrix即可以看到仪表盘,Hystrix Dashboard共支持三种不同的监控方式:
前两者都是对集群的监控,需要整合Turbine才能实现