1.概述
在很多系统架构中都需要考虑横向扩展、单点故障等问题,对于一个庞大的应用集群,部分服务或机器出现问题不可避免。在出现问题时,如何减少故障的影响、保障集群的高可用,成为一个重要的课题。在微服务集群中,不管是服务器,还是客户端,都支持集群部署,本节将介绍Spring Cloud中所用的集群保护框架:Hystrix.
1.1实际问题
假设有如下应用程序
用户范围销售模块,服务通过Web接口或者其他方式访问会员模块,会员模块访问数据库。如果数据库因为某些原因变得不可用,会员模块就会得到“数据库无法访问”的信息,并且会将此信息告知销售模块。在实际问题中,用户会不断地向销售模块发请求,而销售模块这继续请求会员模块,会员模块会不断地请求连接有问题的数据库直到超时,但是还是会有大量的用户请求(包括重试的)会发过来,导致整个应用不堪重负。可能情况会比这个更糟糕,用户的请求不停的发送给销售模块,而由于数据库的原因,会员模块迟迟没有响应,有可能导致整个机房的网络阻塞,受害的不仅仅是这个应用程序,机房中的所有服务都有可能因为网络的原因而瘫痪。
1.2传统的解决方式
对于前面遇到的实际问题,可以选择在连接数据库的同时加上超时的配置,让会员模块快速响应。但这仅仅是解决了其中的一种情况,在实际情况中,会员模块有可能出现问题,例如部分线程阻塞、进程假死等,在这些情况下,对外的服务销售模块面对大量的用户与有故障的会员模块,仍然无法独善其身,前面所说的问题依旧会出现。
在当今的互联网时代,面对大量的用户请求,传统或者单一的解决方式在复杂的急群中显得力不从心,我们需要跟优雅更完善的方案来解决这些问题。
2.集群容错框架Hystrix
在分布式环境中,总会有一些被依赖的服务会失效,例如像网络短暂无法访问、服务器宕机等情况。Hystrix是Netflix下的一个java库,Spring Cloud将Hystrix整合到Netflix项目中,Hystrix通过添加延迟阈值以及容错的逻辑,来帮助我们控制分布式系统间组件的交互。Hystrix通过隔离服务间的访问点、停止他们之间的级联故障、提供可回退操作来实现容错。
例如我们之前讲到的问题,如果数据库层面出现问题,销售模块在访问会员模块时必然会出现超时的情况,此时可以将会员模块隔离开来,销售模块短时间内不再调用会员模块,并且会快速响应用户的请求,从而保证销售模块自身乃至整个集群的稳定性,这是Hystrix可以解决的问题。加入了容错机制,当会员模块或者数据库不可用时,销售模块将对其进行“熔断”,在一定时间内,销售模块不会再调用会员模块,以维持自身的稳定,结构图就变成下面的图了
Hystrix主要实现以下的功能:
> 当所依赖的网络服务发生延迟或者失败时,对访问的客户端程序进行保护,就像上面的例子对销售模块进行保护一样;
> 在分布式系统中停止级联故障;
> 网络服务恢复正常后,可以快速恢复客户端的访问能力;
> 调用失败时执行服务回退;
> 可支持实时监控、报警和其他操作。
3.第一个Hystrix程序
本例将编写一个简单的Hello World程序,展示Hystrix的基本功能。
3.1 创建服务提供者
使用SpringBoot的spring-boot-starter-web项目,创建一个普通的web项目,发布两个测试服务用于测试,项目目录及代码清单如下
pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.triheart</groupId> <artifactId>hystrixserver</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>1.5.4.RELEASE</version> </dependency> </dependencies> </project>
ServerApp.java
package com.triheart.hystrixserver;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
/**
* @author 阿遠
* Date: 2018/8/29
* Time: 21:16
*/
@SpringBootApplication
public class ServerApp {
public static void main(String[] args) {
new SpringApplicationBuilder(ServerApp.class).run(args);
}
}
MyController.java
package com.triheart.hystrixserver;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* @author 阿遠
* Date: 2018/8/29
* Time: 21:18
*/
@RestController
public class MyController {
@GetMapping("/normalHello")
public String normalHello(HttpServletRequest request) {
return "normal Hello!";
}
@GetMapping("/errorHello")
public String errorHello(HttpServletRequest request) throws Exception{
// 模拟处理让线程睡眠10秒
Thread.sleep(10000);
return "error Hello";
}
}
在MyController控制器中,我们提供了一个正常的服务,提供了一个需要等待10秒才有返回的服务。
3.2 创建客户端并使用Hystrix
使用Hystri来请求Web服务,与原来的方式不太一样,新建项目hystrixclient,项目的目录结构如下
添加相关的依赖,pom.xml的代码清单如下
pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.triheart</groupId> <artifactId>hystrixclient</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-core</artifactId> <version>1.5.12</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <version>1.7.25</version> <artifactId>slf4j-log4j12</artifactId> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.2</version> </dependency> </dependencies> </project>
客户端除了要使用Hystrix外,还会使用HttpClient模块来访问Web服务,因此要加入httpclient的依赖。新建命令类
HelloCommand.java
package com.triheart.hystrixclient; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; /** * @author 阿遠 * Date: 2018/8/29 * Time: 21:43 */ public class HelloCommand extends HystrixCommand<String> { private String url; CloseableHttpClient httpclient; public HelloCommand(String url) { // 调用父类的构造器,设置命令组的key,默认用来作为线程池的key super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); // 创建HttpClient客户端 this.httpclient = HttpClients.createDefault(); this.url = url; } protected String getFallback() { System.out.println("执行 HelloCommand 的回退方法"); return "error"; } protected String run() throws Exception { try { // 调用 GET 方法请求服务 HttpGet httpget = new HttpGet(url); // 得到服务响应 HttpResponse response = httpclient.execute(httpget); // 解析并返回命令执行结果 return EntityUtils.toString(response.getEntity()); } catch (Exception e) { e.printStackTrace(); } return ""; } }
新建运行类,执行HelloCommand命令
HelloMain.java
package com.triheart.hystrixclient; /** * @author 阿遠 * Date: 2018/8/29 * Time: 21:49 */ public class HelloMain { public static void main(String[] args) { // 请求正常的服务 String normalUrl = "http://localhost:8080/normalHello"; HelloCommand command = new HelloCommand(normalUrl); String result = command.execute(); System.out.println("请求正常的服务,结果:" + result); } }
正常情况下,直接调用HttpClient的API来请求Web服务,而此处命令类和运行类则通过执行命令来执行调用的工作。在命令类HelloCommond中,实现了父类的run方法,使用HttpClient调用服务的过程,都放到了该方法中。运行HelloMain类,可以看到,结果与平常调用Web服务无异。
假设我们所调用的Hello服务发生故障,导致无法正常访问,那么对于客户端来说,该如何自保呢?下面将演示调用异常服务的情况
在HelloCommand类中,加入回退方法。在运行类中,调用发生故障的服务
HelloErrorMain.java
package com.triheart.hystrixclient; /** * @author 阿遠 * Date: 2018/8/29 * Time: 21:49 */ public class HelloErrorMain { public static void main(String[] args) { // 请求异常的服务 String normalUrl = "http://localhost:8080/errorHello"; HelloCommand command = new HelloCommand(normalUrl); String result = command.execute(); System.out.println("请求异常的服务,结果:" + result); } }
运行对应的类,可以看到控制台输入如下
根据结果可知,回退方法被执行了。本例中调用的errorHello服务,会阻塞10秒才有返回。默认情况下,如果调用Web服务无法再一秒内完成,那么将触发回退。
回退更像是一个备胎,当请求的服务无法正常返回时,就调用该备胎来实现。这样就可以很好的保护客户端,服务端所提供的服务受网络等条件的制约,如果有服务真的需要10秒才能返回结果,而客户端有没有容错的机制,后果就是客户端将一直等待返回,直到网络超时活着服务有响应,而外界会一直不停地发送请求给客户端,最终导致的结果就是客户端因为请求过多而瘫痪。
4.Hystrix的运作流程
在前面的例子中,我们使用Hystrix时仅仅是创建命令并予以执行。这一步骤看是简单,实际上,Hystrix有一套较为复杂的执行逻辑,下面简单说明一下运作流程。
第一步:在命令执行开始时会做一些准备工作,例如为命令创建相应的线程池等。
第二步:判断是否打开了缓存,打开了缓存就直接查找缓存并返回结果。
第三步:判断断路器是否打开,如果打开就表明该链路不可用,直接执行回退方法。
第四步:判断线程池、信号量(计数器)等条件,例如线程池超负荷,则执行回退方法,否则就去执行命令的内容。
第五步:执行命令,计算是否要对断路器进行处理,执行完成后,如果满足一定条件,这需要开启断路器。如果执行成功,则返回结果,反之则执行回退。
整个流程的关键最主要的地方在于断路器是否打开。我们的客户端在使用Hystrix时表面上只是创建了一个命令来执行,实际上Hystrix已经为客户端添加了几层保护。