分布式系统中经常会出现某个基础服务不可用造成整个系统不可用的情况, 这种现象被称为服务雪崩效应. 为了应对服务雪崩, 一种常见的做法是手动服务降级. 而Hystrix的出现,给我们提供了另一种选择.
服务雪崩效应是一种因 服务提供者 的不可用导致 服务调用者 的不可用,并将不可用 逐渐放大 的过程
我把服务雪崩的参与者简化为 服务提供者 和 服务调用者, 并将服务雪崩产生的过程分为以下三个阶段来分析形成的原因:
服务雪崩的每个阶段都可能由不同的原因造成, 比如造成 服务不可用 的原因有:
硬件故障可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问. 缓存击穿一般发生在缓存应用重启, 所有缓存被清空时,以及短时间内大量缓存失效时. 大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用. 在秒杀和大促开始前,如果准备不充分,用户发起大量请求也会造成服务提供者的不可用.
而形成 重试加大流量 的原因有:
在服务提供者不可用后, 用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单. 服务调用端的会存在大量服务异常后的重试逻辑. 这些重试都会进一步加大请求流量.
最后, 服务调用者不可用 产生的主要原因是:
当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源. 一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了.
针对造成服务雪崩的不同原因, 可以使用不同的应对策略:
流量控制 的具体措施包括:
因为Nginx的高性能, 目前一线互联网公司大量采用Nginx+Lua的网关进行流量控制, 由此而来的OpenResty也越来越热门.
用户交互限流的具体措施有: 1. 采用加载动画,提高用户的忍耐等待时间. 2. 提交按钮添加强制等待时间机制.
改进缓存模式 的措施包括:
服务自动扩容 的措施主要有:
服务调用者降级服务 的措施包括:
资源隔离主要是对调用服务的线程池进行隔离.
我们根据具体业务,将依赖服务分为: 强依赖和若依赖. 强依赖服务不可用会导致当前业务中止,而弱依赖服务的不可用不会导致当前业务的中止.
不可用服务的调用快速失败一般通过 超时机制, 熔断器 和熔断后的 降级方法 来实现.
Hystrix [hɪst'rɪks]的中文含义是豪猪, 因其背上长满了刺,而拥有自我保护能力. Netflix的 Hystrix 是一个帮助解决分布式系统交互时超时处理和容错的类库, 它同样拥有保护系统的能力.
Hystrix的设计原则包括:
货船为了进行防止漏水和火灾的扩散,会将货仓分隔为多个,
种资源隔离减少风险的方式被称为:Bulkheads(舱壁隔离模式). Hystrix将同样的模式运用到了服务调用者上.
在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如: 商品详情展示服务会依赖商品服务, 价格服务, 商品评论服务
调用三个依赖服务会共享商品详情服务的线程池. 如果其中的商品评论服务不可用, 就会出现线程池里所有线程都因等待响应而被阻塞, 从而造成服务雪崩
Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离, 从而避免服务雪崩. 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用.
服务的健康状况 = 请求失败数 / 请求总数. 熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的.
熔断器的开关能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待. 并且熔断器能在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能.
Hystrix使用命令模式(继承HystrixCommand类)来包裹具体的服务调用逻辑(run方法), 并在命令模式中添加了服务调用失败后的降级逻辑(getFallback). 同时我们在Command的构造方法中可以定义当前服务线程池和熔断器的相关参数. 如下代码所示:
public class Service1HystrixCommand extends HystrixCommand<Response> {
private Service1 service;
private Request request;
public Service1HystrixCommand(Service1 service, Request request){
supper(
Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ServiceGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("servcie1query"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("service1ThreadPool"))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(20))//服务线程池数量
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerErrorThresholdPercentage(60)//熔断器关闭到打开阈值
.withCircuitBreakerSleepWindowInMilliseconds(3000)//熔断器打开到关闭的时间窗长度
))
this.service = service;
this.request = request;
);
}
@Override
protected Response run(){
return service1.call(request);
}
@Override
protected Response getFallback(){
return Response.dummy();
}
}
在使用了Command模式构建了服务对象之后, 服务便拥有了熔断器和线程池的功能.
Hystrix的Metrics中保存了当前服务的健康状况, 包括服务调用总次数和服务调用失败次数等. 根据Metrics的计数, 熔断器从而能计算出当前服务的调用失败率, 用来和设定的阈值比较从而决定熔断器的状态切换逻辑. 因此Metrics的实现非常重要.
Hystrix在这些版本中的使用自己定义的滑动窗口数据结构来记录当前时间窗的各种事件(成功,失败,超时,线程池拒绝等)的计数. 事件产生时, 数据结构根据当前时间确定使用旧桶还是创建新桶来计数, 并在桶中对计数器经行修改. 这些修改是多线程并发执行的, 代码中有不少加锁操作,逻辑较为复杂.
Hystrix在这些版本中开始使用RxJava的Observable.window()实现滑动窗口. RxJava的window使用后台线程创建新桶, 避免了并发创建桶的问题. 同时RxJava的单线程无锁特性也保证了计数变更时的线程安全. 从而使代码更加简洁. 以下为我使用RxJava的window方法实现的一个简易滑动窗口Metrics, 短短几行代码便能完成统计功能,足以证明RxJava的强大:
@Test
public void timeWindowTest() throws Exception{
Observable<Integer> source = Observable.interval(50, TimeUnit.MILLISECONDS).map(i -> RandomUtils.nextInt(2));
source.window(1, TimeUnit.SECONDS).subscribe(window -> {
int[] metrics = new int[2];
window.subscribe(i -> metrics[i]++,
InternalObservableUtils.ERROR_NOT_IMPLEMENTED,
() -> System.out.println("窗口Metrics:" + JSON.toJSONString(metrics)));
});
TimeUnit.SECONDS.sleep(3);
}
引入jar包:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.4.3.RELEASE</version>
</dependency>
导入配置:
server:
port: 11111
#default可替换
hystrix:
command:
default:
execution:
isolation:
#线程池隔离还是信号量隔离 默认是THREAD 信号量是SEMAPHORE
strategy: THREAD
semaphore:
#使用信号量隔离时,支持的最大并发数 默认10
maxConcurrentRequests: 10
thread:
#command的执行的超时时间 默认是1000
timeoutInMilliseconds: 1000
#HystrixCommand.run()执行超时时是否被打断 默认true
interruptOnTimeout: true
#HystrixCommand.run()被取消时是否被打断 默认false
interruptOnCancel: false
timeout:
#command执行时间超时是否抛异常 默认是true
enabled: true
fallback:
#当执行失败或者请求被拒绝,是否会尝试调用hystrixCommand.getFallback()
enabled: true
isolation:
semaphore:
#如果并发数达到该设置值,请求会被拒绝和抛出异常并且fallback不会被调用 默认10
maxConcurrentRequests: 10
circuitBreaker:
#用来跟踪熔断器的健康性,如果未达标则让request短路 默认true
enabled: true
#一个rolling window内最小的请求数。如果设为20,那么当一个rolling window的时间内
#(比如说1个rolling window是10秒)收到19个请求,即使19个请求都失败,也不会触发circuit break。默认20
requestVolumeThreshold: 5
# 触发短路的时间值,当该值设为5000时,则当触发circuit break后的5000毫秒内
#都会拒绝request,也就是5000毫秒后才会关闭circuit,放部分请求过去。默认5000
sleepWindowInMilliseconds: 5000
#错误比率阀值,如果错误率>=该值,circuit会被打开,并短路所有请求触发fallback。默认50
errorThresholdPercentage: 50
#强制打开熔断器,如果打开这个开关,那么拒绝所有request,默认false
forceOpen: false
#强制关闭熔断器 如果这个开关打开,circuit将一直关闭且忽略
forceClosed: false
metrics:
rollingStats:
#设置统计的时间窗口值的,毫秒值,circuit break 的打开会根据1个rolling window的统计来计算。若rolling window被设为10000毫秒,
#则rolling window会被分成n个buckets,每个bucket包含success,failure,timeout,rejection的次数的统计信息。默认10000
timeInMilliseconds: 10000
#设置一个rolling window被划分的数量,若numBuckets=10,rolling window=10000,
#那么一个bucket的时间即1秒。必须符合rolling window % numberBuckets == 0。默认10
numBuckets: 10
rollingPercentile:
#执行时是否enable指标的计算和跟踪,默认true
enabled: true
#设置rolling percentile window的时间,默认60000
timeInMilliseconds: 60000
#设置rolling percentile window的numberBuckets。逻辑同上。默认6
numBuckets: 6
#如果bucket size=100,window=10s,若这10s里有500次执行,
#只有最后100次执行会被统计到bucket里去。增加该值会增加内存开销以及排序的开销。默认100
bucketSize: 100
healthSnapshot:
#记录health 快照(用来统计成功和错误绿)的间隔,默认500ms
intervalInMilliseconds: 500
requestCache:
#默认true,需要重载getCacheKey(),返回null时不缓存
enabled: true
requestLog:
#记录日志到HystrixRequestLog,默认true
enabled: true
collapser:
default:
#单次批处理的最大请求数,达到该数量触发批处理,默认Integer.MAX_VALUE
maxRequestsInBatch: 2147483647
#触发批处理的延迟,也可以为创建批处理的时间+该值,默认10
timerDelayInMilliseconds: 10
requestCache:
#是否对HystrixCollapser.execute() and HystrixCollapser.queue()的cache,默认true
enabled: true
threadpool:
default:
#并发执行的最大线程数,默认10
coreSize: 10
#Since 1.5.9 能正常运行command的最大支付并发数
maximumSize: 10
#BlockingQueue的最大队列数,当设为-1,会使用SynchronousQueue,值为正时使用LinkedBlcokingQueue。
#该设置只会在初始化时有效,之后不能修改threadpool的queue size,除非reinitialising thread executor。
#默认-1。
maxQueueSize: -1
#即使maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝。
#因为maxQueueSize不能被动态修改,这个参数将允许我们动态设置该值。if maxQueueSize == -1,该字段将不起作用
queueSizeRejectionThreshold: 5
#Since 1.5.9 该属性使maximumSize生效,值须大于等于coreSize,当设置coreSize小于maximumSize
allowMaximumSizeToDivergeFromCoreSize: false
#如果corePoolSize和maxPoolSize设成一样(默认实现)该设置无效。
#如果通过plugin(https://github.com/Netflix/Hystrix/wiki/Plugins)使用自定义实现,该设置才有用,默认1.
keepAliveTimeMinutes: 1
metrics:
rollingStats:
#线程池统计指标的时间,默认10000
timeInMilliseconds: 10000
#将rolling window划分为n个buckets,默认10
numBuckets: 10
其中execution:isolation:strategy有些区别:
资源隔离。就是多个依赖服务的调用分别隔离到各自自己的资源池内。避免说对一个依赖服务的调用,因为依赖服务接口调用的失败或者延迟,导致所有的线程资源 都全部耗费在这个接口上。一旦某个服务的线程资源全部耗尽可能导致服务的崩溃,甚至故障蔓延。 2.资源隔离的方法 信号量semaphore,最多能容纳10个请求。一旦超过10个信号量最大容量,那么就会拒绝其他请求。 信号量与线程池资源隔离的区别: 线程池隔离技术并非控制tomcat等web容器的线程。更准确的说就是控制tomcat线程的执行。tomcat接到请求之后会调用hystrix线程池的线程去执行。当线程池满了之后会调用fallback降级。 tomcat其他的线程不会卡死,快速返回,然后可以支撑其他事情。同时hystrix处理timeout超时问题。 信号量隔离只是一个关卡,通过我的关卡的线程是固定的。容量满了之后。fallback降级。 区别:线程池隔离技术是用自己的线程去执行调用。信号量是直接让tomcat线程去执行依赖服务。
上图是默认的配置,我们可以对自己的配置进行分组:
针对不同的组在配置文件里面加上不同的配置就好了,在@MyCommand注解里面指定group为abc就行;其他的配置也是这个规则,还有默认的配置是default;这样可以把一个组的配置独立出来,便于配置,而且开发者也会方便很多,代码简洁;
下面是代码:
package cn.chinotan.controller;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @program: test
* @description: hystrix控制器
* @author: xingcheng
* @create: 2018-11-03 19:27
**/
@RestController
@RequestMapping("/hystrix")
public class HystrixController {
@HystrixCommand(fallbackMethod = "helloFallback")
@RequestMapping("/sayHello")
public String sayHello(String name, Integer time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello, " + name;
}
@HystrixCommand(fallbackMethod = "hiFallback")
@RequestMapping("/sayHi")
public String sayHi(String name) {
if (StringUtils.isBlank(name)) {
throw new RuntimeException("name不能为空");
}
return "Good morning, " + name;
}
/**
* fallback
*/
public String helloFallback(String name, Integer time) {
System.out.println("helloFallback: " + name);
return "helloFallback" + name;
}
/**
* fallback
*/
public String hiFallback(String name) {
System.out.println("hiFallback: " + name);
return "hiFallback" + name;
}
}
package cn.chinotan.config;
import com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @program: test
* @description: HystrixConfig
* @author: xingcheng
**/
@Configuration
public class HystrixConfig {
/**
* 用来拦截处理HystrixCommand注解
* @return
*/
@Bean
public HystrixCommandAspect hystrixAspect() {
return new HystrixCommandAspect();
}
/**
* 用来像监控中心Dashboard发送stream信息
* @return
*/
@Bean
public ServletRegistrationBean hystrixMetricsStreamServlet() {
ServletRegistrationBean registration = new ServletRegistrationBean(new HystrixMetricsStreamServlet());
registration.addUrlMappings("/hystrix.stream");
return registration;
}
}
配置监控后台Hystrix-Dashboard
1.github上下载源码https://github.com/kennedyoliveira/standalone-hystrix-dashboard
2.参考其wiki文档,部署成功后,默认端口是7979;
3.点击 http://127.0.0.1:7979/hystrix-dashboard 打开页面
可以看到sayHello接口请求3次成功,熔断器现在是关闭状态,当我们调整time参数,使得接口超时后,看看接口的返回:
可以看到接口返回Fallback方法的内容,证明接口超时后跳到这个方法中去了,但是熔断器还没有打开,接下来进行多次频率高的接口访问:
可以看到熔断器打开了,此时接口会很快就返回失败回调方法的内容,如果过一会再次请求这个接口,time参数变小于超时时间,结果如下:
可以看到熔断器又关闭了,接口可以正常访问,这是为什么呢:
可以看到这个参数,rolling window内最小的请求数。如果设为20,那么当一个rolling window的时间内比如说1个rolling window是10秒)收到19个请求,即使19个请求都失败,也不会触发circuit break。默认20
参考文章:https://segmentfault.com/a/1190000005988895
(adsbygoogle = window.adsbygoogle || []).push({});