Bulkheads(舱壁)是在船体上的巨大的墙壁,它创建了水密舱。在漏水的情况下,舱壁只把水放在一个隔间里,防止船下沉。同样的工程原理可以应用于分布式系统。当架构中的一个组件失败时,应该隔离它。即使单个组件被打破,系统也应该工作。
另一种能在软件中发挥作用的工程模式是断路器/熔断器。断路器的责任是中断电力的流动,保护各种设备不受过载的影响甚至着火。当危险消失时,断路器可以被重置(手动或自动)。但是,这难道不会让你的电源切断你的电灯、暖气或者(在最坏的情况下)你的路由器吗?不一定。那些其他的电网可能会受到其他断路器的保护,因此它们仍然会工作。最重要的是,你的房子没有着火。
Hystrix在系统集成领域实现了这两种模式。每个命令都有一个超时(默认为1秒)和有限的并发性(默认情况下,从一个给定的组最多有10个并发命令)。这些相当激进的限制确保了命令不会消耗太多的资源,比如线程和内存。另外,通过应用超时,我们不会引入过多的延迟。我们可以将这种行为与船中的舱壁进行比较,因为如果我们的一个依赖项开始失败(记住,过高的延迟与失败是无法区分的),这个问题不会影响我们整个系统。超时和有限并发性极大地减少了在外部系统中阻塞的线程数。
另一方面,断路器更聪明。如果我们发现一个依赖项,过去都是在在100毫秒内作出响应,而现在几乎总是在1秒后才作出响应。如果调用这种不正确的依赖项是更广泛的请求处理的一部分,那么几乎每一项事务现在都慢一秒。如果没有Hystrix,延迟可能会更大,但是可以用纯RxJava实现超时。Hystrix的作用远不止于此。如果它发现某个特定的命令在某个时间窗口内(默认是10秒)持续失败(默认情况下是指50%以上的请求失败的话),那么它就会打开一个断路器。
接下来发生的事情非常有趣。Hystrix不再调用您的失败命令。相反,它立即抛出一个异常,快速失败。
是时候实战Hystrix啦。首先,我们需要使用Mockito模拟MeetupApi,这样它就可以总是会出现一些无法接受的延迟:
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.anyDouble;
import static org.mockito.Mockito.mock;
MeetupApi api = mock(MeetupApi.class);
given(api.listCities(anyDouble(), anyDouble())).willReturn(
Observable
.<Cities>error(new RuntimeException("Broken"))
.doOnSubscribe(() -> log.debug("Invoking"))
.delay(2, SECONDS)
);
默认的超时是1秒,所以实际上您将不会真正看到“"Broken" ”异常,因为超时将首先启动。现在,我们要同时调用MeetupApi多次,看看Hystrix的表现如何:
Observable
.interval(50, MILLISECONDS)
.doOnNext(x -> log.debug("Requesting"))
.flatMap(x ->
new CitiesCmd(api, 52.229841, 21.011736)
.toObservable()
.onErrorResumeNext(ex -> Observable.empty()),
5)
使用interval()操作符,我们每50毫秒发出一个事件。在每个事件中,我们调用CitiesCmd并吞下errors。请记住,在实际项目中,您很可能希望至少使用doOnError()回调来记录它们。每隔50毫秒的,Hystrix调用我们的命令,并注意到它在1秒之后超时。 这个命令实际上可能更慢,但是Hystrix过早地打断了它。当您订阅和运行这个程序时,您会注意到CitiesCmd被调用了多次,有时会突然结束。尽管“Requesting”消息仍然每50毫秒出现一次,但是命令已经不被调用了。
Hystrix通过一些试探法研究发现,CitiesCmd有些坏了,因此不再调用它。相反,当您尝试调用此命令时,所产生的Observable结果会立即出现异常并失败。这是一个断路器的快速启动和失败。您的命令不再被调用,因为Hystrix意识到它不断失败,并且没有必要进一步调用它。当失败率超过50%时,断路器就会打开,随后每一次调用命令的尝试都失败了。通过失败,Hystrix假设这里存在一个异常或超时。
断路器的优点是双重的。从应用程序调用命令的角度来看,无论如何,它都会失败,但我们可以会更快地得到响应,从而获得更好的用户体验。但是,从服务器的角度来看,它甚至更有趣——或者请求的目标是什么。如果您的命令一直失败或超时,这可能表明您的依赖项(即另一个服务、数据库)存在困难。这可能是重新启动、高峰流量,或者是非常长的GC暂停。通过使用断路器切断这条指令,你给系统一点喘息的事件。也许当负载峰值结束或内部工作队列清空时,系统将重新恢复正常。这将防止您的系统执行分布式拒绝服务(distributed denial of service,DDoS)对自己攻击。
那么,Hystrix将如何识别下游的依赖项又恢复正常并关闭断路器/熔断器呢?幸运的是,这个过程是自动的。在前面的例子中,在某个时间点,我们不再看到“"Invoking" ”日志消息,这意味着断路器打开了,命令不再执行。这并不完全正确。偶尔(默认情况下每5秒一次),Hystrix会放行一个请求并调用命令,并检查这次是否可以。与此同时,所有其他客户仍在迅速失败。如果这个请求成功,Hystrix假设命令现在是健康的,并关闭了断路器;否则,断路器仍然开启。
这种特性被称为自愈性,是计算机系统中的一个重要概念。Hystrix在两个方面有帮助。通过暂时关闭损坏的命令,它允许下游依赖关系恢复。恢复后,系统将返回正常的操作。没有这样的机制,即使是小故障也会导致级联故障和手动重启,以恢复各种组件的稳定性。