在很多系统架构中都需要考虑横向扩展、单点故障等问题,对于一个庞大的应用集群, 部分服务或者机器出现问题不可避免。在出现故障时,如何减少故障的影响、保障集群的 高可用,成为一个重要的课题。
在微服务集群中,不管是服务器,还是客户端,都支持集 群部署,本章将讲述Spring Cloud中所使用的集群保护框架:Hystrix。
6.1.1实际问题
假设有一个应用程序,调用关系如图6-1所示
图6-1中用户访问服务A模块,服务通过Web接口或者其他方式访问基础服务模块, 基础服务模块访问数据库。
如果数据库因为某些原因变得不可用,基础服务将会得到“数据库无法访问”的信息, 并且会将此信息告知服务A模块。
在出现问题时,用户不断地请求服务A模块,而服务A 模块则继续请求基础服务模块,基础服务模块仍然不停地连接有问题的数据库直到超时, 大量的用户请求(包括重试的请求)会发送过来,整个应用不堪重负。
实际情况可能更加糟糕,用户的请求不停地发送给服务A模块,而由于数据库的原因, 基础服务模块退迟无法响应,有可能造成整个机房的网络阻塞,受害的不仅仅是该应用程序,机房中的所有服务都有可能因为网络原因而瘫痪。
6.1.2传统的解决方式
对于前面遇到的实际问题,可以选择在连接数据库时加上超时的配置,让基础服务模 块快速响应。但这仅仅算是解决了其中一种情况,在实际情况中,基础服务模块有可能出 现问题,
例如部分线程阻塞、进程假死等,在这些情况下,对外的服务A模块面对大量的 用户与有故障的基础服务模块,仍然无法独善其身,前面所说的问题依然会出现。
笔者曾就职于某电影院售票系统供应商,在某一年的春节档期,几大互联网巨头发起 了观影优惠活动,大量的用户请求涌入我们中心端的系统。由于其中某些服务节点(Tomcat) 处理缓慢,很多重试、新接入的请求不断访问我们的服务。
在这个时候,传说中的“人肉 运维”出现了,值班的运维同事,通过手工重启Tomcat来试图缓解这种情况,然而挣扎了 几个小时后,以失败告终。
最终,整个集群网络阻塞,不得不停止对外服务,公司损失惨重。
在这件事过后,公司方面加强了对服务节点的监控,加入了故障报告、紧急故障处理 等机制,期望能减少或者避免这些问题所带来的影响。
在当今的互联网时代,面对大量的用户请求,传统或者单一的解决方式在复杂的集群 面前显得有点力不从心,我们需要更优雅而且更完善的方案来解决这些问题。
6.1.3集群容错框架Hystrix
在分布式环境中,总会有一些被依赖的服务会失效,例如像网络短暂无法访问、服务 器宕机等情况。
Hystrix是Netflix下的一个Java库,Spring Cloud将Hystrix整合到Netflix 项目中,Hystrix通过添加延迟阈值以及容错的逻辑,来帮助我们控制分布式系统间组件的 交互。
Hystrix通过隔离服务间的访问点、停止它们之间的级联故障、提供可回退操作来实 现容错。
例如我们前面所讲到的问题,如果数据库层面出现问题,服务A模块在访问基础模块 时必定会出现超时的情况,此时可以将基础模块隔离开来,服务A在短时间内不再调用基 础模块,并且快速响应用户的请求,
从而保证服务A自身乃至整个集群的稳定性,这是 Hystrix可以解决的问题。加入容错机制,当出现前面所说的问题时,原来的应用程序将变 为图6-2所示的结构。
如图6-2所示,当前基础服务模块或者数据库不可用时,服务A将对其进行“熔断”, 在一定的时间内,服务A都不会再调用基础服务,以维持本身的稳定。
图6-2加入容错机制
除了服务间的依赖会导致整个集群不可用外,在其他情况下,我们同样需要集群容错。 假设集群中存在30个服务,每个服务在99.99%的时间内是正常运行的,计算下来,整个集群在99.7%的时间内是正常运行的。
如果该集群接受10亿次请求,那么将会有300万次 请求会失败。在现实中,情况可能更加严重,每个服务有99.99%的正常服务时间,已经是 一个很乐观的数字。
网络连接失败、超时,服务器硬件故障,部署引起的问题,应用程序 的bug,所有这些情况都可能会出现,不能因为单点故障而降低整个集群的可用性,容错 机制变得尤为重要。
6.1.4 Hystrix 的功能
Hystrix主要实现以下功能:
- 当所依赖的网络服务发生延迟或者失败时,对访问的客户端程序进行保护,就像前 面例子中对服务A模块进行保护一样。
- 在分布式系统中,停止级联故障。
- 网络服务恢复正常后,可以快速恢复客户端的访问能力。
- 调用失败时执行服务回退。
- 可支持实时监控、报警和其他操作。
接下来,我们将讲述Hystrix的相关功能。
6.2 第一个Hystrix程序
先编写一个简单的Hello World程序,展示Hystrix的基本功能。注意:6.2节与6.3节, Hystrix均没有与Spring Cloud整合使用
6.2.1 准备工作
使用Spring Boot的spring-boot-starter-web项目,建立一个普通的Web项目,发布两个测试服务用于测试,控制器的代码请见代码:
@RestController
public class MyController {
@GetMapping("/normalHello")
public String normalHello(HttpServletRequest request) {
return "Hello World";
}
@GetMapping("/errorHello")
public String errorHello(HttpServletRequest request) throws Exception {
//模拟需要处理10秒
Thread.sleep (10000);
return "Error Hello World";
}
}
一个正常的服务,另外一个服务则需要等待10秒才有返回。本例的Web项目对应的 代码目录为 codes\06\6.2\first-hystrix-server,启动类是 Server Application o
6.2.2客户端使用Hystrix
结合Hystrix来请求Web服务,可能与原来的方式不太一样。新建项目first-hystrix-client, 在pom.xml中加入以下依赖:
<dependency>
<groupld>com.netflix.hystrix</groupld><br>
<artifactld>hystrix-core</artifactld>
<version>l.5.12</version>
</dependency>
<dependency>
<groupld>org.slf4j</groupld>
<version>l.7.25</version>
<artifactld>slf4j-log4j12</artifactld>
</dependency>
<dependency>
<groupld>commons-logging</groupld>
<artifactld>commons-logging</artifactld>
<version>l.2</version>
</dependency>
<dependency>
<groupld>org.apache.httpcomponents</groupld>
<artifactld>httpclient</artifactld>
<version>4.5.2</version>
</dependency>
本书Spring Cloud所使用的Hystrix的版本为1.5.12,我们也使用与其一致的版本。客 户端项目除了要使用Hystrix夕卜,还会使用HttpClient模块访问Web服务,因此要加入相应 的依赖。
新建一个命令类,实现请见代码:
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 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 "";<br>
}
}
新建运行类,执行HelloCommand,如代码清单6-3所示:
public class HelloMain {
public static void main(String[] args) {
//请求正常的服务
String normalUrl = "http://localhost:8080/normalHello";
HelloCommand command = new HelioCommand(normalUrl);
String result = command.execute();
System.out.printin("请求正常的服务,结果:" + result);
}
}
正常情况下,直接调用HttpClient的API来请求Web服务,而前面的命令类与运行类 则通过命令来执行调用的工作。在命令类HelloCommand中,实现了父类的run方法,使 用HttpClient调用服务的过程,都放到了该方法中。
运行HelloMain类,可以看到,结果与 平常调用Web服务无异。接下来,测试使用Hystrix的情况下调用有问题的服务。
6.2.3调用错误服务
假设我们所调用的Hello服务发生故障,导致无法正常访问,那么对于客户端来说, 如何自保呢?本例将调用延时的服务,为客户端设置回退方法。
修改HelloCommand类, 加入回退方法,请见代码清单6-4:
protected String getFallback () {
System.out.printIn("执行 HelloCommand 的回退方法");
return "error";
}
在运行类中,调用发生故障的服务,请见代码:
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.printin ("请求异常的服务,结果:"+ result);
}
}
运行HelloErrorMain类,输出如下:
执行HelloCommand的回退方法
请求异常的服务,结果:error
回退更像是一个备胎,当请求的服务无法正常返回时,就调用该“备胎”的实现。这 样做可以很好地保护客户端,服务端所提供的服务受网络等条件的制约,如果有服务真的 需要10秒才能返回结果, 根据结果可知,回退方法被执行。本例中调用的errorHello服务,会阻塞10秒才有返 回。默认情况下,如果调用的Web服务无法在1秒内完成,那么将会触发回退。
而客户端又没有容错机制,后果就是,客户端将一直等待返回, 直到网络超时或者服务有响应,而外界会一直不停地发送请求给客户端,最终导致的结果 就是,客户端因请求过多而瘫痪。
6.2.4 Hystrix的运作流程
在前面的例子中,使用Hystrix时仅仅创建命令并予以执行。看似简单,实际上,Hystrix 有一套较为复杂的执行逻辑,为了能让大家大致了解该执行过程,笔者将整个流程进行了 简化。
Hystrix的运作流程请见图6-3。
简单说明一下运作流程。
- 第一步:在命令开始执行时,会做一些准备工作,例如为命令创建相应的线程池(后面章节讲述)等。
- 第二步:判断是否打开了缓存,打开了缓存就直接查找缓存并返回结果。
- 第三步:判断断路器是否打开,如果打开了,就表示链路不可用,直接执行回退方 法。结合本章开头的例子,可理解为基础服务模块不可用,服务A模块直接执行回 退,响应用户请求。
- 第四步:判断线程池、信号量(计数器)等条件,例如像线程池超负荷,则执行回 退方法,否则,就去执行命令的内容(例如前面例子中的调用服务)。
- 第五步:执行命令,计算是否要对断路器进行处理,执行完成后,如果满足一定条 件,则需要开启断路器。如果执行成功,则返回结果,反之则执行回退。
整个流程最主要的地方在于断路器是否被打开,后面会讲解断路器的相关内容。我们 的客户端在使用Hystrix时,表面上只是创建了一个命令来执行,实际上Hystrix已经为客 户端添加了几层保护。
图6-3所示的流程图对Hystrix的运作流程做了最简单的描述,对于部分细节,在此不 进行讲述,读者大致了解运作流程即可
6.3 Hystrix的使用
本节将详细讲述Hystrix的使用方法
6.3.1 命令执行
在前面的例子中,使用了 execute方法执行命令,一个命令对象可以使用以下方法来执 行命令。
- toObservable:返回一个最原始的可观察的实例(Observable) , Observable 是 RxJava 的类,使用该对象可以观察命令的执行过程,并且将执行信息传递给订阅者。
- observe:调用toObservable方法,获得一个原始的Observable实例后,使用 ReplaySubject作为原始Observable的订阅者。
- queue:通过toObservable方法获取原始的Observable实例,再调用Observable的 toBlocking 方法得到一个 BlockingObservable 实例,最后调用 BlockingObservable 的 toFuture方法返回Future实例,调用Future的get方法得到执行结果。
- execute:调用queue的get方法返回命令的执行结果,该方法同步执行。
以上4个方法,除execute方法外,其他方法均为异步执行。observe与toObservable 方法的区别在于,toObservable被调用后,命令不会立即执行,只有当返回的Observable 实例被订阅后,才会真正执行命令。
而在observe方法的实现中,会调用toObservable得到 Observable实例,再对其进行订阅,因此调用observe方法后会立即执行命令(异步)。代 码清单6-6使用了 4个执行命令的方法。
public class RunTest {
public static void main(String[] args) throws Exception {
//使用execute方法
RunCommand cl = new RunCommand ("使用 execute 方法执行命令");
cl.execute();
//使用queue方法
RunCommand c2 = new RunCommand ("使用 queue 方法执行命令");
c2.queue();
//使用observe方法
RunCommand c3 = new RunCorranand ("使用 observe 方法执行命令");
c3.observe();
// 使用 toObservable 方法
RunCommand c4 = new RunCommand ("使用 toObservable 方法执行命令");
//调用toObservable方法后,命令不会马上执行
Observable<String> ob = c4.toObservable ();
//进行订阅,此时会执行命令
ob.subscribe(new Observer<String>() {
public void onCompleted() {
System.out.printIn ("命令执行完成");
}
public void onError(Throwable e) {
}
public void onNext(String t) {
System, out .printin ("命令执行结果:"+ t);
}
});
Thread.sleep(100);
}
//测试命令
static class RunCommand extends HystrixCommand<String> {
String msg;
public RunCommand(String msg) (
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.msg = msg;
}
protected String run() throws Exception {
System.out.printIn(msg);
return "success";
}
}
}
对于4个执行命令的方法,读者需要知道toObservable与observe方法的区别,这两个 方法将会在Spring Cloud中使用。
6.3.