微服务架构中,系统互相间调用的次数更加密集和频繁,而在分布式的体系结构下,这也意味出现异常情况的概率增加。一个核心服务往往需要依次调用数个周边服务才能完成一次完整的业务请求,可以想象的是任意一个服务出现异常都可能造成核心服务的不稳定,资源堵塞,最终拖垮整个服务。
为了应对这种情况,由 Martin Fowler 总结了名为 circuit breaker pattern 即「熔断模式」的解决方案。简而言之就是在调用某个服务时,异常次数或是超时次数超过设定的阈值时,就会直接返回一个异常结果或是一个特定的值,避免关键资源的堵塞与浪费。
Hystrix 就是由 Netflix 研发的为远程调用提供服务熔断与降级的框架,同时它也被整合为 Spring Cloud 的一部分。今天就让我们快速开发一个 Hystrix 的小项目,来了解如何使用 Hystrix。
搭建项目
照例我们使用 https://start.spring.io/ 来初始化项目,添加如下图所示的依赖项:
添加 Hystrix 依赖.png使用 IDE 打开项目后,我们可以开始编写 Hystrix 示例程序。先准备一个模拟的远程服务类。
public class RemoteService {
private int sleepPeriod;
public RemoteService(final int sleepPeriod) {
this.sleepPeriod = sleepPeriod;
}
public String doSomething(final String name) {
if (this.sleepPeriod > 0) {
try {
Thread.sleep(sleepPeriod);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "Hello " + name;
}
}
然后是 Hystrix 的包装类:
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
public class RemoteServiceCommand extends HystrixCommand<String> {
private String name;
private RemoteService remoteService;
public RemoteServiceCommand(final RemoteService remoteService, final String name) {
super(HystrixCommandGroupKey.Factory.asKey("Blackfish"));
this.remoteService = remoteService;
this.name = name;
}
@Override
protected String run() throws Exception {
return remoteService.doSomething(name);
}
}
最后是对应的测试用例:
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
public class HystrixCommandTest {
@Test
public void should_invoke_command_successful() throws Exception {
RemoteServiceCommand remoteServiceCommand = new RemoteServiceCommand(new RemoteService(-1), "Joshua");
String hello = remoteServiceCommand.execute();
assertThat(hello, is("Hello Joshua"));
}
}
以上的演示了通过 Hystrix 正常调用一个服务,很简单吧。但是在实际项目中,调用一个远程服务时,比较常见的一种情况是 超时,那如何使用 Hystrix 来优雅的处理超时呢?往下看。
超时的处理
我们先修改 RemoteServiceCommand
增加一个构造函数,方便传入定制的配置信息,代码如下:
public RemoteServiceCommand(final RemoteService remoteService, final String name, Setter setter) {
super(setter);
this.remoteService = remoteService;
this.name = name;
}
然后新增一个测试用例,传入我们自定义的超时配置的参数:
@Test(expected = HystrixRuntimeException.class)
public void should_throw_when_timeout() throws Exception {
HystrixCommand.Setter config = HystrixCommand.Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey("Blackfish"));
HystrixCommandProperties.Setter properties = HystrixCommandProperties.Setter();
properties.withExecutionTimeoutInMilliseconds(1000);
config.andCommandPropertiesDefaults(properties);
RemoteServiceCommand remoteServiceCommand = new RemoteServiceCommand(new RemoteService(10000),
"Joshua",
config);
remoteServiceCommand.execute();
}
上面的代码比较浅显易懂,与之前相比不同的是增加了一个超时的配置,如果 RemoteService.doSomething
不能在指定的时间内返回,则会抛出 HystrixRuntimeException
的异常。你可以修改 RemoteService
线程的休眠时间,改为小于 Hystrix 的超时时间,则方法能够正常返回(当然这个测试用例会挂掉,因为增加了期待异常的断言)。
启用断路器
如同我们文章开头介绍的,断路器的作用是当调用的远程服务的异常达到阈值时,调用方可以提前返回,不再调用出现问题的服务。以下的代码演示了如何使用 Hystrix 提供的断路器。
@Test
public void should_call_remote_service_with_circuit() throws Exception {
HystrixCommand.Setter config = HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("Blackfish"));
HystrixCommandProperties.Setter properties = HystrixCommandProperties.Setter();
properties.withExecutionTimeoutInMilliseconds(1000);
properties.withCircuitBreakerSleepWindowInMilliseconds(4000);
properties.withCircuitBreakerRequestVolumeThreshold(1);
config.andCommandPropertiesDefaults(properties);
invokeCommand(new RemoteService(10000), config);
invokeCommand(new RemoteService(10000), config);
invokeCommand(new RemoteService(10000), config);
invokeCommand(new RemoteService(10000), config);
assertThat(RemoteServiceCommand.getCount(), is(2));
Thread.sleep(5000);
invokeCommand(new RemoteService(-1), config);
invokeCommand(new RemoteService(-1), config);
assertThat(RemoteServiceCommand.getCount(), is(4));
}
private void invokeCommand(final RemoteService remoteService, HystrixCommand.Setter config) {
RemoteServiceCommand command = new RemoteServiceCommand(remoteService, "Joshua", config);
try {
command.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
同时在 RemoteServiceCommand
中新增一个静态变量来记录 run
方法的调用次数。
public class RemoteServiceCommand extends HystrixCommand<String> {
private static int count;
private String name;
private RemoteService remoteService;
public RemoteServiceCommand(final RemoteService remoteService, final String name) {
super(HystrixCommandGroupKey.Factory.asKey("Blackfish"));
this.remoteService = remoteService;
this.name = name;
}
public RemoteServiceCommand(final RemoteService remoteService, final String name, Setter setter) {
super(setter);
this.remoteService = remoteService;
this.name = name;
}
@Override
protected String run() throws Exception {
count++;
return remoteService.doSomething(name);
}
public static int getCount() {
return RemoteServiceCommand.count;
}
}
在测试用例中新增了两个配置,withCircuitBreakerSleepWindowInMilliseconds
和 withCircuitBreakerRequestVolumeThreshold
,第一个参数决定了当断路器打开后,经过多少时间会关闭。而第二个参数决定了多少次调用后才会开启断路器的阈值检测。
还有两个比较重要的指标是 withCircuitBreakerErrorThresholdPercentage
即超过多少百分比会启用断路器,默认为 50,即错误率超过 50% 就会启用断路器。withMetricsRollingStatisticalWindowInMilliseconds
是配置时间窗口的时长,默认为 10 秒。
测试用例中的代码在调用第一,第二次服务后,因为超时的原因会失败,调用计数也增加了。但是两次之后触发了断路器,之后的两次调用就不会再触发实际的服务调用,计数器也不会增加。线程休眠 5 秒后,再次调用服务,因为 5 秒大于 withCircuitBreakerSleepWindowInMilliseconds
的 配置 (4 秒),因此此时断路器已经关闭,会调用实际的服务,而计数器也会增加。
与 Spring Cloud 集成
前面代码演示了如何使用 Hystrix 一些特性,而它与 Spring Cloud 的集成也非常方便,参考如下的代码:
package com.blackfish.hystrixdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
@SpringBootApplication
@EnableCircuitBreaker
public class HystrixDemoApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDemoApplication.class, args);
}
}
在 Application 上增加 @EnableCircuitBreaker
就能启用 Hystrix 了。
package com.blackfish.hystrixdemo;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class RemoteFacadeService {
private RemoteService remoteService;
@Autowired
public RemoteFacadeService(final RemoteService remoteService) {
this.remoteService = remoteService;
}
@HystrixCommand
public String invokeRemoteService(final String name) {
return remoteService.doSomething(name);
}
}
在需要调用远程接口的方法上增加一个 @HystrixCommand
就会对该方法启用断路器。是不是很简单?
结语
Hystrix 在实际应用中还有相当多的配置选项,需要对它的原理有更加深入的了解,后续会更加深入的介绍 Hystrix 的线程模型和最佳实践,有兴趣的可以关注我们的公众号第一时间获得更新信息!
网友评论