Hystrix断路器

在微服务架构中,我们将系统拆分成很多服务单元,各单元的应用间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会因等待出现故障的依赖方响应形成任务积压,最终导致自身服务的瘫痪。

上面那样说你可能不太明白,没关系接下来举一个司空见惯的例子来帮助你理解。如果你想开发一个电商网站,里面包括用户管理、商品管理、类目管理、订单管理等模块,按照传统的软件开发方式其实非常简单,只需要创建一个Web服务,很快就能将这个功能上线,但是如果我们会采用微服务架构+服务治理的方式来实现,那么此时就稍微麻烦一些。首先需要将这个项目拆分为用户管理、商品管理、类目管理、订单管理等4个微服务,这四个服务各建一个模块,分别是用户模块、商品模块、类目模块、订单模块,然后这四个模块通过内部服务治理互相调用。但是可能会出现一个问题,就是目前这些模块是通过服务注册与订阅的方式互相依赖,如果某个模块出现故障,那么会导致依赖它的模块也发生故障从而发生故障蔓延,进而导致整个服务的瘫痪。举个例子,如这里的商品模块,如果商品模块出现问题,那么会导致用户无法创建订单,影响了订单服务的运行。

再举一个例子,如将某电商系统拆分为用户、订单、库存、积分、评论等一系列服务单元。用户在创建一个订单的时候,客户端将会调用订单服务的创建订单接口,此时创建订单接口又会向库存服务来请求出货(前提是还有库存),此时若库存服务因自身处理逻辑等原因造成响应缓慢,会直接导致创建订单服务的线程被挂起,以等待库存申请服务的响应,在漫长的等待之后用户会因为请求库存失败而得到创建订单失败的结果。如果在高并发情况之下,因这些挂起的线程在等待库存服务的响应而未能释放,使得后续到来的创建订单请求被阻塞,最终导致订单服务也不可用。

在微服务架构中,存在那么多的服务单元,若一个单元出现故障,就很容易因依赖关系而引发故障的蔓延,最终导致整个系统的瘫痪,这样的架构相对于传统架构更加不稳定。那么为了解决这样的问题,就产生了断路器等一系列的服务保护机制。

在分布式架构中,断路器的作用是当某个服务单元发生故障后,通过断路器的故障监控,向调用方返回一个错误响应,而不是长时间的等待,这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。

针对上述问题,SpringCloud Hystrix实现了断路器、线程隔离等一系列服务保护功能。它也是基于Netflix的开源框架实现的,该框架的作用是通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。

说了那么多,接下来开始进入SpringCloud Hystrix的学习和使用。

一个demo

在介绍SpringCloud Hystrix实现断路器之前,先回顾一下现有的架构,如下所示:

为了实现上述架构,需要启动以下信息:
(1)服务名为eureka-server的工程,这是服务注册中心,注意端口为1111,即启动实例名为peer1的服务实例。
(2)服务名为hello-service的工程,这是服务提供者,注意需要启动两个实例,启动的端口分别为8081和8082,即启动实例名为p1和p2的服务实例(注意两个服务提供者均需往服务注册中心注册)。
(3)服务名为ribbon-consumer的工程,这是服务消费者,注意需要启动一个实例,启动的端口为9000,即启动实例名为ribbon-consumer的服务实例。

启动之后,在浏览器地址栏中访问http://localhost:1111/,如下所示:

可以看到服务启动成功,接着访问http://localhost:9000/ribbon-consumer ,如下所示:

接下来关闭端口号为8081的服务提供者,接着再去访问这个链接,可以看到结果为:

经过前面的学习,我们知道SpringCloud中默认采用的负载均衡策略是轮询,所以当一个服务提供者停止服务后,刷新时的服务请求成功和请求失败是成对出现的:当服务消费者去请求那个停止服务的服务提供者时,就会出现请求失败;而当服务消费者去请求正常的服务提供者时,就能获得到期望的结果。所以我们要求请求失败时,不能给用户展示一个ErrorPage页面,而是自定义页面,此时该如何解决呢?可以使用断路器来解决这个问题。

第一步,在服务消费者中加入断路器。开发者需要在ribbon-consumer工程的pom.xml文件中添加SpringCloud Hystrix依赖,相应的代码为:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>

第二步,开启断路器功能。在项目的入口类上添加@EnableCircuitBreaker注解用于开启断路器功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class RibbonConsumerApplication {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(RibbonConsumerApplication.class, args);
}
}

其实开发者还可以使用SpringCloud应用中的@SpringCloudApplication注解来修饰应用主类,该注解的具体含义如下所示:

1
2
3
4
5
6
7
8
9
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}

可以看到该注解其实是一个组合注解,里面包含了上述我们所需要的三个注解,这也意味着一个SpringCloud标准应用应包含服务发现以及断路器。

第三步,改造服务消费方式。接下来将对服务消费方式进行修改,在项目目录下新建一个service包,并在里面新建一个HelloService类,该类用于注入RestTemplate实例,相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class HelloService {
@Autowired
private RestTemplate restTemplate;

@HystrixCommand(fallbackMethod = "helloFallback")
public String helloConsumer(){
return restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
}

public String helloFallback(){
return "error";
}
}

简单解释一下上述代码的含义:(1)RestTemplate对象用于执行网络请求;(2)helloFallback是当请求失败时需要回调的方法;(3)在helloConsumer方法上添加@HystrixCommand(fallbackMethod = "helloFallback")注解表示当请求失败后,将会调用指定的回调方法,此处为helloFallback。

第四步,修改controller类。修改ConsumerController类,并在其中注入前面实现的HelloService实例,并在helloConsumer方法中进行调用:

1
2
3
4
5
6
7
8
9
10
@RestController
public class ConsumerController {
@Autowired
private HelloService helloService;

@GetMapping(value = "/ribbon-consumer")
public String helloConsumer(){
return helloService.helloConsumer();
}
}

所以其实这里就是将原本写在controller中的逻辑进行拆分,都写到service层中,然后在controller中只是单纯的调用即可。

接下来开始验证断路器实现的服务回调逻辑,重新启动之前关闭的运行于8081端口的hello-service服务,并确保此时服务注册中心、两个hello-service服务提供者实例以及ribbon-consumer服务消费者都已经正常启动了。

可以看到服务启动成功,接着访问http://localhost:9000/ribbon-consumer链接时可以轮询两个HELLO-SERVICE并返回一些文字信息,如下所示:

接下来关闭端口号为8081的服务提供者,接着再去访问这个链接,当轮询到8081服务端时,可以看到输出内容为error,不再是之前的errorPage页面,Hystrix的服务回调生效,结果如下所示:

模拟服务阻塞

除了通过断开具体的服务实例来模拟某个节点无法访问的情况之外,其实还可以模拟服务阻塞(长时间未响应)的情况。

对之前HELLO-SERVICE服务中的/hello接口进行修改,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
public class HelloController {
private static final Logger logger = LoggerFactory.getLogger(HelloController.class);

@Autowired
private DiscoveryClient client;

//模拟服务阻塞
@GetMapping(value = "/hello")
public String index() throws Exception {
List<ServiceInstance> instances = client.getInstances("hello-service");
//让处理线程等待几秒钟
int sleepTime = new Random().nextInt(3000);
logger.info("sleepTime is: "+sleepTime);
Thread.sleep(sleepTime);

for(int i=0;i<instances.size();i++){
logger.info("/hello,host: "+instances.get(i).getHost()+
",service_id: "+instances.get(i).getServiceId());
}
return "hello,world!";
}
}

这里通过Thread.sleep()方法可以让/hello接口的处理线程不是马上返回内容,而是在阻塞几秒之后才返回内容。由于Hystrix默认超时时间为2000毫秒,所以这里采用了0-3000的随机数,可以让处理过程有一定概率发生超时来触发断路器。

为了更精准地观察断路器的触发,开发者可以在服务消费者调用函数中做一些时间记录。打开ribbon-consumer项目,修改HelloService类中的helloConsumer方法,其中的代码为:

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
@Service
public class HelloService {
private static final Logger logger = LoggerFactory.getLogger(HelloService.class);

@Autowired
private RestTemplate restTemplate;

@HystrixCommand(fallbackMethod = "helloFallback")
public String helloConsumer(){
//开始时间
long startTime = System.currentTimeMillis();

//服务消费者的逻辑
String result = restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();

//结束时间
long endTime = System.currentTimeMillis();

//计算花费时间
logger.info("spend time is: "+(endTime-startTime));

return result;
}

public String helloFallback(){
return "error";
}
}

接下来开始验证断路器实现的服务回调逻辑,重新启动之前关闭的运行于8081端口的hello-service服务,并确保此时服务注册中心、两个hello-service服务提供者实例以及ribbon-consumer服务消费者都已经正常启动了。

可以看到服务启动成功,接着连续多次访问http://localhost:9000/ribbon-consumer链接,我们可以观察到当RIBBON-CONSUMER的控制台中输出的spend time大于2000的时候,就会返回error,即服务消费者因调用的服务超时从而触发熔断请求,并调用回调逻辑返回结果: