微服务集群中,服务端和客户端都支持集群部署。而庞大的集群中,部分服务出现问题不可避免,如何减少故障的影响以及确保集群高可用,是一个需要解决的问题。Hystrix就是Spring Cloud提供的一个集群保护框架。
Hystrix 是一个延迟和容错库,旨在隔离远程系统、服务和第三方库的访问点,停止级联故障,并在故障不可避免的复杂分布式系统中实现弹性。Hystrix通过添加延迟阈值和容错逻辑,帮助控制分布式系统组建的交互。
一、实现的功能
1、当所依赖的网络服务发生延迟或者失败时,对访问的客户端程序进行保护。如下图基础服务无法访问,则立即执行回退逻辑。
2、在分布式系统中,停止级联故障。
3、网络恢复后,可以快速恢复客户端的访问能力。
4、调用失败时执行服务回退。
5、可以支持实时监控、报警以及其他操作。
二、运行流程
第一步:在命令开始执行时,会准备一些工作,比如命令会创建相应的线程池。
第二步:判断是否打开了缓存,打开缓存的情况会直接查找缓存并返回结果。
第三步:判断断路器是否打开,打开的话说明链路不可用,直接执行回退,响应用户请求。
第四步:判断线程池、信号量(计数器)等条件,如线程池超负荷则直接执行回退方法,否则就去执行命令的内容。
第五步:执行命令,计算是否要对断路器进行处理。执行完毕后,如果满足一定的条件,则需求开启断路器。如果执行成功,则返回结果,否则执行回退。
整个Hystrix运行流程中,断路器开启状态是最核心的。
三、使用要点
1、命令属性配置
超时配置
命令组名称(GroupKey),必须配置。默认情况下,全局维护的线程池Map作为该值的key,该Map的value为执行命令的线程池。
命令名称(CommanKey),可选配置。
线程池名称(ThreadPoolKey),指定线程池的key后,全局维护的线程池Map将作为该值的Key。
2、回退
触发回退的三种情况:
a、断路器被打开
b、线程池、信号量满载
c、实际执行失败
满足三种条件之一即可进行回退。执行父类HystrixCommand的getFallback方法即可实现回退。
回退模式
回退机制灵活,特别注意的是,如果A和B两个命令时在一个事务内的,在控制类中调用回退方法,A和B不能有回退方法,不然就不能执行控制类的回退方法了。
断路器开启
如果断路器开启了,将不再执行命令,也不更新链路的健康状况,在一段时间内一直在回退。
触发断路器开启的需要同时满足两个条件:
第一个:整个链路达到一定阈值。默认10秒内产生20个请求,则符合该条件。
第二个:达到第一个条件的同时,如果请求失败的百分比大于阈值,则开启断路器。失败百分比默认为50%。
断路器关闭
断路器打开后,在一段时间内(默认为5秒),命令不在执行而是在不断触发回退,这段时间被称为休眠期。
休眠期结束后,此时断路器处于半开状态(非开启也非关闭),hystrix会尝试执行一次命令,如果命令执行成功,那么将关闭断路器并清空链路的健康信息,如果执行失败,则断路器依旧保持打开的状态。
隔离机制
除了断路器要处于关闭状态外,命令真正执行还需要考虑命令的线程池或者信号量是否满载。如果满载则不会执行命令,而是触发回退,从机制上控制了命令的执行,实现了错误的隔离。
隔离策略
THRED:默认策略,有线程池来决定是否执行命令,如果线程池满载,则不会执行命令。默认线程池大小为10。
SEMAPHORE:由信号量来决定命令的执行。当请求并发数高于阈值时,不再执行命令。默认最大并发数为10.
信号量策略开销小,但是不支持超时和异步,一般不建议使用。
合并请求
在线程隔离策略下,可以将一段时间内的同类请求(URL一致,参数不同)收集到一个命令中执行。
这样做的好处:节省线程开销、减少网络连接。
合并请求的三个条件:
a、需要一个执行请求的命令,将全部的参数进行整理,然后调用外部服务。
b、需求一个合并处理器,用于收集请求,以及处理结果
c、外部接口提供支持。(提供批量查询等接口)
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.Future;
import com.netflix.config.ConfigurationManager;
import com.netflix.hystrix.HystrixCollapser;
import com.netflix.hystrix.HystrixCollapser.CollapsedRequest;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
public class CollapseTest {
public static void main(String[] args) throws Exception {
// 收集 1 秒内发生的请求,合并为一个命令执行
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.collapser.default.timerDelayInMilliseconds", 1000);
// 请求上下文
HystrixRequestContext context = HystrixRequestContext
.initializeContext();
// 创建请求合并处理器
MyHystrixCollapser c1 = new MyHystrixCollapser("Angus");
MyHystrixCollapser c2 = new MyHystrixCollapser("Crazyit");
MyHystrixCollapser c3 = new MyHystrixCollapser("Sune");
MyHystrixCollapser c4 = new MyHystrixCollapser("Paris");
// 异步执行
Future<Person> f1 = c1.queue();
Future<Person> f2 = c2.queue();
Future<Person> f3 = c3.queue();
Future<Person> f4 = c4.queue();
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
context.shutdown();
}
/**
* 合并执行的命令类
*/
static class CollapserCommand extends HystrixCommand<Map<String, Person>> {
// 请求集合,第一个类型是单个请求返回的数据类型,第二是请求参数的类型
Collection<CollapsedRequest<Person, String>> requests;
private CollapserCommand(
Collection<CollapsedRequest<Person, String>> requests) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory
.asKey("ExampleGroup")));
this.requests = requests;
}
@Override
protected Map<String, Person> run() throws Exception {
System.out.println("收集参数后执行命令,参数数量:" + requests.size());
// 处理参数
List<String> personNames = new ArrayList<String>();
for(CollapsedRequest<Person, String> request : requests) {
personNames.add(request.getArgument());
}
// 调用服务(此处模拟调用),根据名称获取Person的Map
Map<String, Person> result = callService(personNames);
return result;
}
// 模拟服务返回
private Map<String, Person> callService(List<String> personNames) {
Map<String, Person> result = new HashMap<String, Person>();
for(String personName : personNames) {
Person p = new Person();
p.id = UUID.randomUUID().toString();
p.name = personName;
p.age = new Random().nextInt(30);
result.put(personName, p);
}
return result;
}
}
static class Person {
String id;
String name;
Integer age;
public String toString() {
// TODO Auto-generated method stub
return "id: " + id + ", name: " + name + ", age: " + age;
}
}
/**
* 合并处理器
* 第一个类型为批处理返回的结果类型
* 第二个为单请求返回的结果类型
* 第三个是请求参数类型
*/
static class MyHystrixCollapser extends
HystrixCollapser<Map<String, Person>, Person, String> {
String personName;
public MyHystrixCollapser(String personName) {
this.personName = personName;
}
@Override
public String getRequestArgument() {
return personName;
}
@Override
protected HystrixCommand<Map<String, Person>> createCommand(
Collection<CollapsedRequest<Person, String>> requests) {
return new CollapserCommand(requests);
}
@Override
protected void mapResponseToRequests(Map<String, Person> batchResponse,
Collection<CollapsedRequest<Person, String>> requests) {
// 让结果与请求进行关联
for (CollapsedRequest<Person, String> request : requests) {
// 获取单个响应返回的结果
Person singleResult = batchResponse.get(request.getArgument());
// 关联到请求中
request.setResponse(singleResult);
}
}
}
}
其中,CollapserCommand是命令内部类,MyHystrixCollapser是合并处理器。
请求缓存
针对同一次请求中,多个地方调用同一个接口的情况,可以考虑使用缓存。
缓存被打开后,下次命令不会执行,而是直接从缓存中获取响应并返回。
开启:在命令中重写父类的getCacheKey方法即可。
合并请求、请求缓存需要在一次请求过程中才能实现,因此需要先初始化请求上下文。
四、单独使用Hystrix
1、服务提供方
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyController {
@GetMapping("/normalHello")
@ResponseBody
public String normalHello(HttpServletRequest request) {
return "Hello World";
}
@GetMapping("/errorHello")
@ResponseBody
public String errorHello(HttpServletRequest request) throws Exception {
// 模拟需要处理10秒
Thread.sleep(10000);
return "Error Hello World";
}
}
2、服务调用方
第一步:添加hystrix依赖
<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>
第二步:编写命令类,需要继承HystrixCommand
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;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
/**
* 调用一个正常的服务
*
*/
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 "";
}
protected String getFallback() {
System.out.println("执行 HelloCommand 的回退方法");
return "error";
}
}
第三步:编写调用类
public class HelloMain {
public static void main(String[] args) {
// 请求正常的服务
String url = "http://localhost:8080/normalHello";
HelloCommand command = new HelloCommand(url );
String result = command.execute();
System.out.println("请求正常的服务,结果:" + result);
// 请求异常的服务
String url = "http://localhost:8080/errorHello";
command = new HelloCommand(url );
result = command.execute();
System.out.println("请求异常的服务,结果:" + result);
}
}
五、在Spring Cloud中使用Hystrix
1、整合Hystrix流程(服务调用方)
第一步:添加Hystrix依赖,添加eureka、ribbon以及hystrix
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
第二步:在启动类中,加入启用断路器的注解
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class InvokerApplication {
@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(InvokerApplication.class, args);
}
}
第三步:编写服务类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
@Component
public class PersonService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "getPersonFallback")
public Person getPerson(Integer id) {
// 使用RestTemplate调用Eureka服务
Person p = restTemplate.getForObject(
"http://spring-hystrix-provider/person/{personId}",
Person.class, id);
return p;
}
/**
* 回退方法,返回一个默认的Person
*/
public Person getPersonFallback(Integer id) {
Person p = new Person();
p.setId(0);
p.setName("Crazyit");
p.setAge(-1);
p.setMessage("request error");
return p;
}
/**
* 测试配置,对3个key进行命名
* 设置命令执行超时时间为1000毫秒
* 设置命令执行的线程池大小为1
*/
@HystrixCommand(
fallbackMethod="testConfigFallback", groupKey="MyGroup",
commandKey="MyCommandKey", threadPoolKey="MyCommandPool",
commandProperties={
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
value = "1000")
},
threadPoolProperties={
@HystrixProperty(name = "coreSize",
value = "1")
})
public String testConfig() {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
return "ok";
}
public String testConfigFallback() {
return "error";
}
/**
* 声明了忽略MyException,如果方法抛出MyException,则不会触发回退
*/
@HystrixCommand(ignoreExceptions = {MyException.class},
fallbackMethod="testExceptionFallBack")
public String testException() {
throw new MyException();
}
public String testExceptionFallBack() {
return "error";
}
}
第四步:编写控制器
package org.crazyit.cloud;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import org.crazyit.cloud.cache.CacheService;
import org.crazyit.cloud.collapse.CollapseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Configuration
public class InvokerController {
@Autowired
private PersonService personService;
@Autowired
private CacheService cacheService;
@Autowired
private CollapseService collapseService;
@RequestMapping(value = "/router/{personId}", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public Person router(@PathVariable Integer personId) {
Person p = personService.getPerson(personId);
return p;
}
@RequestMapping(value = "/testConfig", method = RequestMethod.GET)
public String testConfig() {
String result = personService.testConfig();
return result;
}
@RequestMapping(value = "/testException", method = RequestMethod.GET)
public String testException() {
String result = personService.testException();
return result;
}
@RequestMapping(value = "/cache1/{personId}", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public Person testCacheResult(@PathVariable Integer personId) {
// 调用多次服务
for(int i = 0; i < 3; i++) {
Person p = cacheService.getPerson(personId);
System.out.println("控制器调用服务 " + i);
}
return new Person();
}
@RequestMapping(value = "/cache2", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public String testCacheRemove() {
for(int i = 0; i < 3; i++) {
cacheService.cacheMethod("a");
System.out.println("控制器调用服务 " + i);
}
// 清空缓存
cacheService.updateMethod("a");
System.out.println("========== 清空了缓存");
// 再执行多次
for(int i = 0; i < 3; i++) {
cacheService.cacheMethod("a");
System.out.println("控制器调用服务 " + i);
}
return "";
}
@RequestMapping(value = "/collapse", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public String testCollapse() throws Exception {
// 连续执行3次请求
Future<Person> f1 = collapseService.getSinglePerson(1);
Future<Person> f2 = collapseService.getSinglePerson(2);
Future<Person> f3 = collapseService.getSinglePerson(3);
Person p1 = f1.get();
Person p2 = f2.get();
Person p3 = f3.get();
System.out.println(p1.getId() + "---" + p1.getName());
System.out.println(p2.getId() + "---" + p2.getName());
System.out.println(p3.getId() + "---" + p3.getName());
return "";
}
}
2、常用注解
缓存注解:在使用@CacheResult和@HystrixCommand共同注释服务方法,如下:
@CacheResult
@HystrixCommand
public Person getPerson(Integer id) {
System.out.println("执行 getPerson 方法");
Person p = new Person();
p.setId(id);
p.setName("angus");
return p;
}
合并请求注解:使用@HystrixCollapser修饰方法,如下图:在一秒内(collapserProperties属性指定)收集到的请求,交由指定了方法getPersons(batchMethod属性指定)进行批处理。
@Component
public class CollapseService {
// 配置收集1秒内的请求
@HystrixCollapser(batchMethod = "getPersons", collapserProperties =
{
@HystrixProperty(name = "timerDelayInMilliseconds", value = "1000")
}
)
public Future<Person> getSinglePerson(Integer id) {
System.out.println("执行单个获取的方法");
return null;
}
@HystrixCommand
public List<Person> getPersons(List<Integer> ids) {
System.out.println("收集请求,参数数量:" + ids.size());
List<Person> ps = new ArrayList<Person>();
for (Integer id : ids) {
Person p = new Person();
p.setId(id);
p.setName("crazyit");
ps.add(p);
}
return ps;
}
}