SpringCloud学习笔记(五、SpringCloud Netflix Hystrix)
目录:
- Hystrix简介
- 线程隔离:线程池、信号量
- 服务降级、服务熔断、请求缓存、请求合并
- Hystrix完整流程、Hystrix属性值
- 注解方式实现Hystrix
- Hystrix Dashboard
Hystrix简介:
1、Hystrix是什么
Hystrix是Netflix的一款开源的分布式容错和延迟库,目的是用于隔离分布式服务的故障。它提供了优雅的服务降级、熔断机制,使得服务能够快速的失败,而不是一直等待响应,并且它还能从失败中快速恢复。
说白了Hystrix就是封装了服务之间异常调用的一个框架(之前看到过微服务界的一个大佬说的一句话,很有道理:一个好的服务架构一定是具有自诊断功能的)。
2、Hystrix解决的问题
- 限制分布式服务的资源使用,当某一个调用的服务出现问题时不会影响其他服务的调用,通过线程隔离和信号量来实现。
- 提供了优雅的降级机制,超时降级、资源不足时降级;降级后可通过降级接口返回托底数据。
- 提供了熔断机制,当失败率达到了阀值时会自动降级,并且可以快速恢复。
- 提供了请求缓存和请求合并的实现。
线程隔离:线程池、信号量:
我们知道Hystrix对于限制分布式服务的资源使用是通过线程隔离和信号量来实现了,那我们就来说说这两个。
1、线程隔离之线程池
)什么是线程隔离
线程隔离其实就是对线程资源的隔离,它可以将系统资源分开,在发生故障的时候缩小影响范围。
如登录时需要sso和加载广告,当用户登录时加载广告的接口挂了,那么便会影响用户的登录,但其实主流程只是sso,广告挂了也不能影响主流程啊;而线程隔离便可以解决这一问题。
Hystrix的线程隔离便是,把Tomcat请求的任务转交给Hystrix内部的线程去执行,这样在Hystrix开启线程后就可以释放Tomcat线程资源,从而Tomcat便可以响应更多的请求,遭遇过Hystrix将任务执行完后再把结果交给Tomcat。
)Hystrix线程隔离demo
1 public class HystrixCommand4ThreadPool extends HystrixCommand<String> { 2 3 private final String name; 4 5 public HystrixCommand4ThreadPool(String name) { 6 super(Setter 7 // 线程组名称 8 .withGroupKey(HystrixCommandGroupKey.Factory.asKey("ThreadPoolGroup")) 9 // 命令名称 10 .andCommandKey(HystrixCommandKey.Factory.asKey("ThreadPoolCommandKey")) 11 // 线程池名称 12 .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey")) 13 .andCommandPropertiesDefaults( 14 // 请求超时时间 15 HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(3000) 16 ) 17 .andThreadPoolPropertiesDefaults( 18 // 定义Hystrix线程池中线程数量 19 HystrixThreadPoolProperties.Setter().withCoreSize(3) 20 ) 21 ); 22 this.name = name; 23 } 24 25 /** 26 * 降级策略 27 */ 28 @Override 29 protected String getFallback() { 30 System.out.println(Thread.currentThread() + "Hi This is Fallback for name:" + this.name); 31 return this.name; 32 } 33 34 /** 35 * 执行逻辑 36 */ 37 @Override 38 protected String run() throws Exception { 39 TimeUnit.MILLISECONDS.sleep(800); 40 System.out.println(Thread.currentThread() + " This is run in HystrixCommand , name :" + this.name); 41 return name; 42 } 43 44 static class UnitTest { 45 46 /** 47 * 异步调用 48 */ 49 @Test 50 void testQueue() { 51 System.out.println("Thread.currentThread(): " + Thread.currentThread()); 52 for (int i = 0; i < 5; i++) { 53 try { 54 Future<String> queue = new HystrixCommand4ThreadPool("thread" + i).queue(); 55 System.out.println("thread" + i + "在干点其它的事..."); 56 // queue.get -> 异步阻塞的获取执行结果 57 System.out.println("异步调用-获取结果:" + queue.get(1, TimeUnit.SECONDS)); 58 } 59 catch (Exception e) { 60 e.printStackTrace(); 61 } 62 } 63 } 64 65 /** 66 * 同步调用 67 */ 68 @Test 69 void testExecute() { 70 System.out.println("Thread.currentThread(): " + Thread.currentThread()); 71 for (int i = 0; i < 5; i++) { 72 try { 73 String execute = new HystrixCommand4ThreadPool("thread" + i).execute(); 74 System.out.println("同步调用-获取结果:" + execute); 75 } 76 catch (Exception e) { 77 e.printStackTrace(); 78 } 79 } 80 } 81 82 /** 83 * 响应式调用 84 */ 85 @Test 86 void testObserve() { 87 System.out.println("Thread.currentThread(): " + Thread.currentThread()); 88 for (int i = 0; i < 5; i++) { 89 try { 90 Observable<String> observe = new HystrixCommand4ThreadPool("thread" + i).observe(); 91 System.out.println("哈哈哈,在等待响应嘛..."); 92 observe.subscribe(result -> System.out.println("得到结果:" + result)); 93 } 94 catch (Exception e) { 95 e.printStackTrace(); 96 } 97 } 98 99 try { 100 // 因testObserve是@Test,为守护线程,所以需要休眠等待执行结果 101 TimeUnit.SECONDS.sleep(5L); 102 } 103 catch (InterruptedException e) { 104 e.printStackTrace(); 105 } 106 } 107 108 } 109 110 }
我们定义了一个类继承com.netflix.hystrix.HystrixCommand,因此此类便可以完成Hystrix一些基本功能,如线程池和信号量隔离等待。
在HystrixCommand4ThreadPool构造中,我们来初始化Hystrix,定义了Hystrix的线程组名称、命令名称、线程池名称以及超时时间和核心线程数。
获取结果的方式也分为三种:
- 异步调用(异步阻塞式)。
- 同步调用。
- 响应式调用。
知识点:
- getFallback():降级逻辑是运行在main线程中的,在无法获得核心线程时会从其它地方返回兜底数据(如redis等)。
- 线程隔离的目的是管理线程资源,使线程资源不会滥用,可以保障服务节点的可用性。
2、线程隔离之信号量
信号量隔离其实和线程池隔离差不多,只是信号量隔离是内部的限流控制,当超过配置的信号量大小便会拒绝请求,从而达到限流保护的作用。
1 public class HystrixCommand4Semaphore extends HystrixCommand<String> { 2 3 private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 4 5 private final String name; 6 7 public HystrixCommand4Semaphore(String name) { 8 super(Setter 9 .withGroupKey(HystrixCommandGroupKey.Factory.asKey("SemaphoreGroup")) 10 .andCommandKey(HystrixCommandKey.Factory.asKey("SemaphoreKey")) 11 .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("SemaphoreThreadPoolKey")) 12 .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() 13 // 信号量隔离 14 .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE) 15 .withExecutionTimeoutInMilliseconds(3000) 16 // 配置信号量大小 17 .withExecutionIsolationSemaphoreMaxConcurrentRequests(3) 18 // 配置降级并发量 19 // .withFallbackIsolationSemaphoreMaxConcurrentRequests(1) 20 ) 21 ); 22 this.name = name; 23 } 24 25 @Override 26 protected String run() throws Exception { 27 System.out.println(sdf.format(new Date()) + "," + Thread.currentThread() + " This is run in HystrixCommand , name :" + this.name); 28 return this.name; 29 } 30 31 @Override 32 protected String getFallback() { 33 System.out.println(sdf.format(new Date()) + "," + Thread.currentThread() + " Hi This is Fallback for name:" + this.name); 34 return this.name; 35 } 36 37 38 static class UnitTest { 39 @Test 40 void testHystrixCommand4Semaphore() { 41 for (int i = 0; i < 5; i++) { 42 final int j = i; 43 try { 44 new Thread(() -> new HystrixCommand4Semaphore("Semaphore-Thread " + j).execute()).start(); 45 } 46 catch (Exception e) { 47 e.printStackTrace(); 48 } 49 } 50 51 try { 52 TimeUnit.SECONDS.sleep(5); 53 } 54 catch (InterruptedException e) { 55 e.printStackTrace(); 56 } 57 } 58 } 59 60 }
知识点:
- Semaphore隔离方式的运行时在main线程中,所以只支持同步调用方式。
- Semaphore可以设置降级并发的线程数,.withFallbackIsolationSemaphoreMaxConcurrentRequests(int value)
3、线程池与信号量的比较
线程池 | 信号量 | |
线程 | 与调度线程非同一线程 | 与调度线程是同一线程 |
开销 | 排队、调度、上下文切换等开销 | 无线程切换,开销低 |
异步 | 支持 | 不支持 |
并发支持 |
支持(由线程池大小决定) | 支持(由信号量大小决定) |
服务降级:
当请求出现异常、超时、服务不可用等情况时,Hystrix可以自定义降级策略,防止返回null或抛出异常。
注意:
1、无限循环属于超时,会导致降级。
2、Hystrix降级就是用HystrixBadRequestException来处理的,所以抛出这个异常不会走降级。
demo:
1 public class HystrixCommand4Fallback extends HystrixCommand<String> { 2 3 private final String name; 4 5 public HystrixCommand4Fallback(String name) { 6 super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("FallbackGroup")) 7 // 超时时间1秒 8 .andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(1000))); 9 this.name = name; 10 } 11 12 @Override 13 protected String getFallback() { 14 System.err.println(Thread.currentThread() + " Hi This is Fallback for name:" + this.name); 15 return this.name; 16 } 17 18 @Override 19 protected String run() throws Exception { 20 // 1.无限循环,默认1秒钟超时。 21 while (true) { 22 } 23 } 24 25 // @Override 26 // protected String run() throws Exception { 27 // // 2.运行时异常 28 // int i = 1 / 0; 29 // return name; 30 // } 31 32 // @Override 33 // protected String run() throws Exception { 34 // // 3.throw 异常 35 // throw new Exception("xyz"); 36 // } 37 38 // @Override 39 // protected String run() throws Exception { 40 // // 4.HystrixBadRequestException异常不会触发降级 41 // throw new HystrixBadRequestException("xtz"); 42 // } 43 44 public static class UnitTest { 45 @Test 46 public void testHystrixCommand4Fallback() throws ExecutionException, InterruptedException { 47 System.out.println("--"); 48 Future<String> threadFallback = new HystrixCommand4Fallback("Thread Fallback").queue(); 49 threadFallback.get(); 50 } 51 } 52 }
服务熔断:
熔断也叫做过载保护,它其实就是一个自我统计,统计啥呢?
在一段时间内请求成功和失败的次数,当失败次数达到阀值后,下次请求直接走fallback。一段时间后会尝试走正常流程,若成功的话后续流程便会走正常流程。
注意:
1、若在指定时间内没有达到请求数量,即使所有的请求失败了,也不会打开断路器。
2、必须满足时间、请求数、失败比例三个条件才会触发断路器。
1 public class HystrixCommand4CircuitBreaker extends HystrixCommand<String> { 2 private final String name; 3 4 protected HystrixCommand4CircuitBreaker(String name) { 5 super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("circuitBreakerGroupKey")) 6 .andCommandKey(HystrixCommandKey.Factory.asKey("circuitBreakerKey")) 7 .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("circuitThreadPoolKey")) 8 .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(200)) 9 .andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withCircuitBreakerEnabled(true) 10 // 10s内至少请求10次,如果10s内没有接收到10次请求,即使所有请求都失败了,断路器也不会打开 11 .withMetricsRollingStatisticalWindowInMilliseconds(10000) 12 .withCircuitBreakerRequestVolumeThreshold(10) 13 // 当出错率超过50%后开启断路器. 14 .withCircuitBreakerErrorThresholdPercentage(50) 15 // 断路器打开后的休眠时间(当等待时间到达后才会开始统计) 16 .withCircuitBreakerSleepWindowInMilliseconds(5000))); 17 this.name = name; 18 } 19 20 @Override 21 protected String getFallback() { 22 System.out.println(Thread.currentThread() + "Hi This is Fallback for name:" + this.name); 23 // // 当熔断后, fallback流程由main线程执行, 设置sleep, 体现熔断恢复现象. 24 // try { 25 // TimeUnit.MILLISECONDS.sleep(900); 26 // } catch (InterruptedException e) { 27 // e.printStackTrace(); 28 // } 29 return this.name; 30 } 31 32 @Override 33 protected String run() throws Exception { 34 System.out.println("-----" + name); 35 int num = Integer.valueOf(name); 36 37 // 模拟执行成功 38 if (num % 2 == 1) { 39 System.out.println("Hi This is HystrixCommand for name:" + this.name); 40 return name; 41 } else { 42 // 模拟异常 43 while (true) { 44 } 45 } 46 } 47 48 public static class UnitTest { 49 @Test 50 public void testHystrixCommand4CircuitBreaker() { 51 final long start = System.currentTimeMillis(); 52 for (int i = 0; i < 50; i++) { 53 try { 54 // queue() 异步调用 , execute() 同步调用 55 new HystrixCommand4CircuitBreaker(i + "").execute(); 56 } catch (Exception e) { 57 System.out.println("run 捕获异常 "); 58 e.printStackTrace(); 59 } 60 } 61 } 62 } 63 }
运行后发现10秒内请求数量超时10个,且有一半以上失败时后续的请求才会走fallback。
然后我们再看第16行,我们设置了断路器执行时间,当断路器执行5秒后则会休眠,继续重试正常流程;我们将23 - 28行注释打开便会发现断路器执行5秒后便会重试正常流程。
请求缓存:
Hystrix可以将请求的数据缓存起来,当后续有相同请求参数时会直接拿缓存的。这样可以避免了直接调用服务,从而减轻了服务器的压力。
Hystrix的请求缓存不常用,现在一般采用其它的缓存框架来实现,如redis,memoryCache。
1 public class HystrixCommand4Cache extends HystrixCommand<Boolean> { 2 3 private final int key; 4 private final String value; 5 6 protected HystrixCommand4Cache(int key, String value) { 7 super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("CacheGroup")).andCommandPropertiesDefaults( 8 HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(1000))); 9 this.key = key; 10 this.value = value; 11 } 12 13 @Override 14 protected Boolean run() { 15 System.out.println("This is Run... "); 16 return true; 17 } 18 19 @Override 20 protected String getCacheKey() { 21 // 构建cache的key;如果调用getCacheKey 得到的结果是相同的, 说明是相同的请求 可以走缓存 22 return key + value; 23 } 24 25 @Override 26 protected Boolean getFallback() { 27 System.err.println("fallback"); 28 return false; 29 } 30 31 public static class UnitTest { 32 @Test 33 public void testHystrixCommand4Cache() { 34 //同一个请求上下文中 35 HystrixRequestContext.initializeContext(); 36 HystrixCommand4Cache command2a = new HystrixCommand4Cache(2, "HystrixCommand4RequestCacheTest"); 37 HystrixCommand4Cache command2b = new HystrixCommand4Cache(2, "HystrixCommand4RequestCacheTest"); 38 HystrixCommand4Cache command2c = new HystrixCommand4Cache(2, "NotCache"); 39 System.out.println("command2a:" + command2a.execute()); 40 // 第一次请求,不可能命中缓存 41 System.err.println("第1次请求是否命中缓存:" + command2a.isResponseFromCache()); 42 System.out.println("command2b:" + command2b.execute()); 43 // 命中缓存 44 System.err.println("第2次请求是否命中缓存:" + command2b.isResponseFromCache()); 45 System.out.println("command2c:" + command2c.execute()); 46 //未命中缓存 47 System.err.println("第3次请求是否命中缓存:" + command2c.isResponseFromCache()); 48 49 // 开启一个新的请求,会重新获取一个新的上下文(清空缓存) 50 HystrixRequestContext.initializeContext(); 51 HystrixCommand4Cache command3a = new HystrixCommand4Cache(2, "HystrixCommand4RequestCacheTest"); 52 HystrixCommand4Cache command3b = new HystrixCommand4Cache(2, "HystrixCommand4RequestCacheTest"); 53 System.out.println("command3a:" + command3a.execute()); 54 // 新的请求上下文中不会命中上一个请求中的缓存 55 System.err.println("第4次请求是否命中缓存:" + command3a.isResponseFromCache()); 56 // 从新的请求上下文中command3a.execute()执行中得到的cache 57 System.out.println("command3b:" + command3b.execute()); 58 System.err.println("第5次请求是否命中缓存:" + command3b.isResponseFromCache()); 59 } 60 } 61 }
注意:
- 判断是否走请求缓存的依据是,参数是否相同。
- HystrixRequestContext.initializeContext()开启请求上下文,且只有在同一个上下文中请求才会获取缓存。
请求合并:
同意的Hystrix可以将相同类型的请求合并,而不是分别调用服务提供方,这样可以减少服务端的压力。
1、首先拿到一段时间类类似的请求。
- localhost:8080/order/1
- localhost:8080/order/2
- localhost:8080/order/3
2、从getRequestArgument()获得key,然后合并。
3、绑定不同请求与结果的关系。
1 public class HystrixCommand4Collapser extends HystrixCollapser<List<String>, String, Integer> { 2 3 private final Integer key; 4 5 public HystrixCommand4Collapser(Integer key) { 6 this.key = key; 7 } 8 9 @Override 10 public Integer getRequestArgument() { 11 return key; 12 } 13 14 /** 15 * 创建一个批量请求命令 16 */ 17 @Override 18 protected HystrixCommand<List<String>> createCommand(Collection<CollapsedRequest<String, Integer>> requests) { 19 System.out.println("createCommand request size " + requests.size()); 20 // 调用HystrixCommand实现类,构建Collapser命令 21 return new BatchCommand(requests); 22 } 23 24 /** 25 * 将批量请求的结果和对应的请求一一对应 26 */ 27 @Override 28 protected void mapResponseToRequests(List<String> batchResponse, Collection<CollapsedRequest<String, Integer>> requests) { 29 int count = 0; 30 System.out.println("mapResponseToRequests request size " + requests.size() + ", and batchResponse size " + batchResponse.size()); 31 for (CollapsedRequest<String, Integer> request : requests) { 32 request.setResponse(batchResponse.get(count++)); 33 } 34 } 35 36 private static final class BatchCommand extends HystrixCommand<List<String>> { 37 38 private final Collection<CollapsedRequest<String, Integer>> requests; 39 40 public BatchCommand(Collection<CollapsedRequest<String, Integer>> requests) { 41 super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("BatchCommandGroupKey")) 42 .andCommandKey(HystrixCommandKey.Factory.asKey("BatchCommandKey"))); 43 this.requests = requests; 44 } 45 46 @Override 47 protected List<String> run() throws Exception { 48 ArrayList<String> responseList = new ArrayList<>(); 49 // 处理每个请求,返回结果 50 for (CollapsedRequest<String, Integer> request : requests) { 51 // 模拟一个response 52 String response = "ValueForKey: " + request.getArgument() + " thread:" + Thread.currentThread().getName(); 53 responseList.add(response); 54 } 55 System.out.println("responseListSize:" + responseList.size()); 56 return responseList; 57 } 58 } 59 60 static class UnitTest { 61 @Test 62 void testHystrix4Collapser() throws Exception { 63 HystrixRequestContext context = HystrixRequestContext.initializeContext(); 64 try { 65 Future<String> f1 = new HystrixCommand4Collapser(1).queue(); 66 Future<String> f2 = new HystrixCommand4Collapser(2).queue(); 67 Future<String> f3 = new HystrixCommand4Collapser(3).queue(); 68 TimeUnit.MILLISECONDS.sleep(10); 69 Future<String> f4 = new HystrixCommand4Collapser(4).queue(); 70 71 System.out.println(System.currentTimeMillis() + " : " + f1.get()); 72 System.out.println(System.currentTimeMillis() + " : " + f2.get()); 73 System.out.println(System.currentTimeMillis() + " : " + f3.get()); 74 System.out.println(System.currentTimeMillis() + " : " + f4.get()); 75 // 下面3条都不在一个批量请求中 76 System.out.println(new HystrixCommand4Collapser(5).execute()); 77 System.out.println(new HystrixCommand4Collapser(6).queue().get()); 78 System.out.println(new HystrixCommand4Collapser(7).queue().get()); 79 } 80 finally { 81 context.shutdown(); 82 } 83 } 84 } 85 86 }
知识点:
- 调用queue()发起请求,会将毫秒级之内的请求进行合并。
- 首先,调用createCommand()创建批量命令。
- 然后,调用HystrixCommand实现类具体构建命令,并执行该线程。
- 最后,通过mapResponseToRequests构建批量的响应结果。
Hystrix流程:
1、每次调用创建一个新的HystrixCommand,其执行逻辑都在run()方法中。
2、通过执行execute()或queue()方法做同步或异步的调用(底层都是通过toObservable()来完成具体调用)。
3、判断是否有请求缓存,有则用缓存。
4、判断熔断器是否打开,打开则见8,进行降级。
5、判断线程池或信号量是否已满,若已满则见8,进行降级。
6、调用HystrixObservableCommand.construct()或HystrixCommand.run()执行具体逻辑。
- 若逻辑执行有误,见8。
- 若逻辑调用超时,见8。
7、计算熔断器状态及所有的运行状态(成功、失败、拒绝、超时),并上报给熔断器,此操作用于熔断器的判断状态。
8、调用getFallback()方法执行降级逻辑;触发getFallback()方法的条件如下:
- run()方法抛出非HystrixBadRequestException异常。
- run()方法超时。
- 熔断器开启。
- 线程池或信号量已满。
9、返回执行结果。
Hystrix属性:
1、CommandProperties
2、ThreadPoolProperties
注解方式实现Hystrix:
1 @GetMapping("/testThread") 2 @HystrixCommand( 3 groupKey = "ThreadPoolGroupKey", 4 commandKey = "ThreadPoolCommandKey", 5 threadPoolKey = "ThreadPoolKey", 6 fallbackMethod = "fallbackMethod", 7 commandProperties = { 8 @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000"), 9 @HystrixProperty(name = "execution.timeout.enabled", value = "true"), 10 @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD") 11 }, 12 threadPoolProperties = { 13 @HystrixProperty(name = "coreSize", value = "15") 14 } 15 ) 16 public String testThread() { 17 return "Thread Pool"; 18 }
Hystrix Dashboard:
Hystrix Dashboard是一款图形化的Hystrix服务信息工具。
它的使用方式很简单:
1、创建HystrixDashboard项目
2、增加依赖
1 <dependency> 2 <groupId>org.springframework.cloud</groupId> 3 <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> 4 </dependency>
3、启动类上增加@EnableHystrixDashboard注解
以上是单机Hystrix的监控方法,如果是Hystrix集群的话还需要依赖turbine:
1、首先将所有结点及HystrixDashboard注册到eureka
2、Hystrix添加依赖
1 <dependency> 2 <groupId>org.springframework.cloud</groupId> 3 <artifactId>spring-cloud-starter-netflix-turbine</artifactId> 4 </dependency>
3、启动类加上@EnableTurbine
4、配置properties
eureka.client.serviceUrl.defaultZone=http://localhost:9091/eureka ## 配置Turbine管理端服务名称 turbine.app-config=helloServer,helloServer ## 默认集群名称 turbine.cluster-name-expression=new String("default")