1、Request Context
首先,有一个概念,叫做reqeust context,请求上下文,一般来说,在一个web应用中,hystrix
我们会在一个filter里面,对每一个请求都施加一个请求上下文,就是说,tomcat容器内,每一次请求,就是一次请求上下文
然后在这次请求上下文中,我们会去执行N多代码,调用N多依赖服务,有的依赖服务可能还会调用好几次
在一次请求上下文中,如果有多个command,参数都是一样的,调用的接口也是一样的,其实结果可以认为也是一样的
那么这个时候,我们就可以让第一次command执行,返回的结果,被缓存在内存中,然后这个请求上下文中,后续的其他对这个依赖的调用全部从内存中取用缓存结果就可以了
不用在一次请求上下文中反复多次的执行一样的command,提升整个请求的性能
HystrixCommand和HystrixObservableCommand都可以指定一个缓存key,然后hystrix会自动进行缓存,接着在同一个request context内,再次访问的时候,就会直接取用缓存
多次调用一个command,那么只会执行一次,后面都是直接取缓存
对于请求缓存(request caching),请求合并(request collapsing),请求日志(request log),等等技术,都必须自己管理HystrixReuqestContext的声明周期
在一个请求执行之前,都必须先初始化一个request context
HystrixRequestContext context = HystrixRequestContext.initializeContext();
然后在请求结束之后,需要关闭request context
context.shutdown();
一般来说,在java web来的应用中,都是通过filter过滤器来实现的
例如 查询多个商品信息
http://localhost:8081/getProductInfos?productIds=1,1,2,2
@RequestMapping("/getProductInfos")
@ResponseBody
public String getProductInfos(String productIds){
for(String productId : productIds.split(",")){
GetProductInfoCommand command = new GetProductInfoCommand(Long.valueOf(productId));
ProductInfo productInfo = command.execute();
System.out.println(productInfo);
System.out.println("是否是存缓存中获取的结果:"+command.isResponseFromCache());
}
return "success";
}
/**
* 获取商品信息
*/
public class GetProductInfoCommand extends HystrixCommand<ProductInfo> {
private Long productId;
static final HystrixCommandKey KEY = HystrixCommandKey.Factory.asKey("GetProductInfoCommand");
public GetProductInfoCommand(Long productId) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
.andCommandKey(KEY)
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetProductInfoPool"))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(15)
.withQueueSizeRejectionThreshold(10))
);
this.productId = productId;
}
@Override
protected ProductInfo run() throws Exception {
String url = "http://localhost:8082/getProductInfo?productId=" + productId;
String response = HttpClientUtils.sendGetRequest(url);
System.out.println(response);
ProductInfo productInfo = JSONObject.parseObject(response, ProductInfo.class);
System.out.println("调用接口查询 productId="+productId);
return productInfo;
}
}
请求日志输出
调用接口查询 productId=1
ProductInfo(id=1, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:03, cityId=1, cityName=null)
是否是存缓存中获取的结果:false
{"cityId":1,"color":"红色,白色,黑色","id":1,"modifiedTime":"2022-08-31 10:10:03","name":"iphone7","pictureList":"a.jpg,b.jpg","price":5588,"service":"售后服务","shopId":1,"size":"S","specification":"iphone7的规格"}
调用接口查询 productId=1
ProductInfo(id=1, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:03, cityId=1, cityName=null)
是否是存缓存中获取的结果:false
{"cityId":1,"color":"红色,白色,黑色","id":2,"modifiedTime":"2022-08-31 10:10:03","name":"iphone7","pictureList":"a.jpg,b.jpg","price":5588,"service":"售后服务","shopId":1,"size":"S","specification":"iphone7的规格"}
调用接口查询 productId=2
ProductInfo(id=2, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:03, cityId=1, cityName=null)
是否是存缓存中获取的结果:false
{"cityId":1,"color":"红色,白色,黑色","id":2,"modifiedTime":"2022-08-31 10:10:03","name":"iphone7","pictureList":"a.jpg,b.jpg","price":5588,"service":"售后服务","shopId":1,"size":"S","specification":"iphone7的规格"}
调用接口查询 productId=2
ProductInfo(id=2, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:03, cityId=1, cityName=null)
是否是存缓存中获取的结果:false
我们希望相同的商品id请求,不在执行查询操作,而是直接走requestContext缓存
1.新建filter
/**
* 请求上下文过滤器
*/
public class HystrixRequestContextServletFilter implements Filter {
@Override
public void init(FilterConfig config) {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try{
chain.doFilter(request, response);
}finally {
context.shutdown();
}
}
@Override
public void destroy() {
}
}
2.添加到容器中
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean registration = new FilterRegistrationBean(new HystrixRequestContextServletFilter());
registration.addUrlPatterns("/*");
return registration;
}
3.在GetProductInfoCommand中,新增cacheKey
/*hystrix 每次都会将返回的数据,保存到request context中,key 就是getCahceKey()*/
@Override
protected String getCacheKey() {
return "product_info_" + productId;
}
只要没有cacheKey,就不会走requstContext上下文,从中获取缓存
此时再访问,日志为
调用接口查询 productId=1
ProductInfo(id=1, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:28, cityId=1, cityName=null)
是否是存缓存中获取的结果:false
ProductInfo(id=1, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:28, cityId=1, cityName=null)
是否是存缓存中获取的结果:true
{"cityId":1,"color":"红色,白色,黑色","id":2,"modifiedTime":"2022-08-31 10:10:28","name":"iphone7","pictureList":"a.jpg,b.jpg","price":5588,"service":"售后服务","shopId":1,"size":"S","specification":"iphone7的规格"}
调用接口查询 productId=2
ProductInfo(id=2, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:28, cityId=1, cityName=null)
是否是存缓存中获取的结果:false
ProductInfo(id=2, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:28, cityId=1, cityName=null)
是否是存缓存中获取的结果:true
2、fallback降级机制
hystrix调用各种接口,或者访问外部依赖,mysql,redis,zookeeper,kafka,等等,如果出现了任何异常的情况
情况1: error
比如说报错了,访问mysql报错,redis报错,zookeeper报错,kafka报错,
情况2 :reject
对每个外部依赖,无论是服务接口,中间件,资源隔离,对外部依赖只能用一定量的资源去访问,线程池/信号量,如果资源池已满
情况3 : timeout
访问外部依赖的时候,访问时间过长,可能就会导致超时,报一个TimeoutException异常,
上述三种情况,都是我们说的异常情况,对外部依赖的东西访问的时候出现了异常,发送异常事件到短路器中去进行统计
情况4:circuit breaker
如果短路器发现异常事件的占比达到了一定的比例,直接开启短路,circuit breaker
上述四种情况,都会去调用fallback降级机制
fallback,降级机制,你之前都是必须去调用外部的依赖接口,或者从mysql中去查询数据的,但是为了避免说可能外部依赖会有故障
比如,你可以再内存中维护一个ehcache,作为一个纯内存的基于LRU自动清理的缓存,数据也可以放入缓存内
如果说外部依赖有异常,fallback这里,直接尝试从ehcache中获取数据
比如说,本来你是从mysql,redis,或者其他任何地方去获取数据的,获取调用其他服务的接口的,结果人家故障了,人家挂了,fallback,可以返回一个默认值
两种最经典的降级机制:纯内存数据,默认值
@RequestMapping("/getProductInfo")
@ResponseBody
public String getProductInfo(Long productId){
HystrixCommand<ProductInfo> hystrixCommand = new GetProductInfoCommand(productId);
ProductInfo productInfo = hystrixCommand.execute();
Long cityId = productInfo.getCityId();
GetCityNameCommand getCityNameCommand = new GetCityNameCommand(cityId);
String cityName = getCityNameCommand.execute();
productInfo.setCityName(cityName);
Long brandId = productInfo.getBrandId();
GetBrandNameCommand getBrandNameCommand = new GetBrandNameCommand(brandId);
//例如获取品牌名称是报错,降级处理
String brandName = getBrandNameCommand.execute();
productInfo.setBrandName(brandName);
System.out.println(productInfo);
return "success";
}
/**
* 品牌缓存
*/
public class BrandCache {
private static Map<Long/*品牌id*/,String/*品牌名称*/> brandMap = new HashMap<>();
static {
brandMap.put(1l, "华为");
}
public static String getBrandName(Long brandId){
return brandMap.get(brandId);
}
}
/**
* 获取品牌名称的command
*/
public class GetBrandNameCommand extends HystrixCommand<String> {
private Long brandId;
public GetBrandNameCommand(Long brandId){
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("BrandInfoService"))
.andCommandKey(HystrixCommandKey.Factory.asKey("GetBrandNameCommand"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetBrandInfoPool"))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(15)
.withQueueSizeRejectionThreshold(10))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
/*同时调用fallback的并发线程不能超过15个,默认是10,超过线程将被reject掉*/
.withFallbackIsolationSemaphoreMaxConcurrentRequests(15))
);
this.brandId = brandId;
}
@Override
protected String run() throws Exception {
throw new Exception();
}
@Override
protected String getFallback() {
System.out.println("降级----从获取获取");
return BrandCache.getBrandName(brandId);
}
}
HystrixObservableCommand,是实现resumeWithFallback方法
fallback.isolation.semaphore.maxConcurrentRequests
这个参数设置了HystrixCommand.getFallback()最大允许的并发请求数量,默认值是10,也是通过semaphore信号量的机制去限流
如果超出了这个最大值,那么直接被reject
3、短路器
1、如果经过短路器的流量超过了一定的阈值,HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
举个例子,可能看起来是这样子的,要求在10s内,经过短路器的流量必须达到20个;在10s内,经过短路器的流量才10个,那么根本不会去判断要不要短路
2、如果断路器统计到的异常调用的占比超过了一定的阈值,HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
如果达到了上面的要求,比如说在10s内,经过短路器的流量(只要执行一个command,这个请求就一定会经过短路器),达到了30个;同时其中异常的访问数量,占到了一定的比例,比如说60%的请求都是异常(报错,timeout,reject),会开启短路
3、然后断路器从close状态转换到open状态
4、断路器打开的时候,所有经过该断路器的请求全部被短路,不调用后端服务,直接走fallback降级
5、经过了一段时间之后,HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds(),会half-open,让一条请求经过短路器,看能不能正常调用。如果调用成功了,那么就自动恢复,转到close状态
短路器,会自动恢复的,half-open,半开状态
默认 在滑动窗口的10s内,有超过20个请求,且有超过50%的请求异常,超时,被拒绝,则开启短路器,断路器开启以后,就只能调用fallback进行降级,且并发降低调用默认是10个线程; 超过5s后,短路器变为half-open状态,让部分请求绕过短路器进行业务访问,如果成功,则关闭短路器
6、circuit breaker短路器的配置
(1)circuitBreaker.enabled
控制短路器是否允许工作,包括跟踪依赖服务调用的健康状况,以及对异常情况过多时是否允许触发短路,默认是true
HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(boolean value)
(2)circuitBreaker.requestVolumeThreshold
设置一个rolling window,滑动窗口中,最少要有多少个请求时,才触发开启短路
举例来说,如果设置为20(默认值),那么在一个10秒的滑动窗口内,如果只有19个请求,即使这19个请求都是异常的,也是不会触发开启短路器的
HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(int value)
(3)circuitBreaker.sleepWindowInMilliseconds
设置在短路之后,需要在多长时间内直接reject请求,然后在这段时间之后,再重新导holf-open状态,尝试允许请求通过以及自动恢复,默认值是5000毫秒
HystrixCommandProperties.Setter()
.withCircuitBreakerSleepWindowInMilliseconds(int value)
(4)circuitBreaker.errorThresholdPercentage
设置异常请求量的百分比,当异常请求达到这个百分比时,就触发打开短路器,默认是50,也就是50%
HystrixCommandProperties.Setter()
.withCircuitBreakerErrorThresholdPercentage(int value)
(5)circuitBreaker.forceOpen
如果设置为true的话,直接强迫打开短路器,相当于是手动短路了,手动降级,默认false
HystrixCommandProperties.Setter()
.withCircuitBreakerForceOpen(boolean value)
(6)circuitBreaker.forceClosed
如果设置为ture的话,直接强迫关闭短路器,相当于是手动停止短路了,手动升级,默认false
HystrixCommandProperties.Setter()
.withCircuitBreakerForceClosed(boolean value)
public GetProductInfoCommand(Long productId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
.andCommandKey(KEY)
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetProductInfoPool"))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(15)
.withQueueSizeRejectionThreshold(10))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
/*10s内经过的流量*/
.withCircuitBreakerRequestVolumeThreshold(30)
/*默认50%,设置40%的错误,超时,被拒绝,则开启短路器*/
.withCircuitBreakerErrorThresholdPercentage(40)
/*默认5s,这里设置3s尝试恢复*/
.withCircuitBreakerSleepWindowInMilliseconds(3000))
);
this.productId = productId;
}
线程池机制的优点如下:
(1)任何一个依赖服务都可以被隔离在自己的线程池内,即使自己的线程池资源填满了,也不会影响任何其他的服务调用
(2)服务可以随时引入一个新的依赖服务,因为即使这个新的依赖服务有问题,也不会影响其他任何服务的调用
(3)当一个故障的依赖服务重新变好的时候,可以通过清理掉线程池,瞬间恢复该服务的调用,而如果是tomcat线程池被占满,再恢复就很麻烦
(4)如果一个client调用库配置有问题,线程池的健康状况随时会报告,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机
(5)如果一个服务本身发生了修改,需要重新调整配置,此时线程池的健康状况也可以随时发现,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机
(6)基于线程池的异步本质,可以在同步的调用之上,构建一层异步调用层
简单来说,最大的好处,就是资源隔离,确保说,任何一个依赖服务故障,不会拖垮当前的这个服务
线程池机制的缺点:
(1)线程池机制最大的缺点就是增加了cpu的开销
除了tomcat本身的调用线程之外,还有hystrix自己管理的线程池
(2)每个command的执行都依托一个独立的线程,会进行排队,调度,还有上下文切换
(3)Hystrix官方自己做了一个多线程异步带来的额外开销,通过对比多线程异步调用+同步调用得出,Netflix API每天通过hystrix执行10亿次调用,每个服务实例有40个以上的线程池,每个线程池有10个左右的线程
(4)最后发现说,用hystrix的额外开销,就是给请求带来了3ms左右的延时,最多延时在10ms以内,相比于可用性和稳定性的提升,这是可以接受的
我们可以用hystrix semaphore技术来实现对某个依赖服务的并发访问量的限制,而不是通过线程池/队列的大小来限制流量
sempahore技术可以用来限流和削峰,但是不能用来对调研延迟的服务进行timeout和隔离
execution.isolation.strategy,设置为SEMAPHORE,那么hystrix就会用semaphore机制来替代线程池机制,来对依赖服务的访问进行限流
如果通过semaphore调用的时候,底层的网络调用延迟很严重,那么是无法timeout的,只能一直block住
/**
* 获取商品信息
*/
public class GetProductInfoCommand extends HystrixCommand<ProductInfo> {
private Long productId;
static final HystrixCommandKey KEY = HystrixCommandKey.Factory.asKey("GetProductInfoCommand");
/**
* withCoreSize:设置你的线程池的大小
* withMaxQueueSize:设置的是你的等待队列,缓冲队列的大小
* withQueueSizeRejectionThreshold:如果withMaxQueueSize<withQueueSizeRejectionThreshold,
* 那么取的是withMaxQueueSize,反之,取得是withQueueSizeRejectionThreshold
* @param productId
*/
public GetProductInfoCommand(Long productId) {
// super(HystrixCommandGroupKey.Factory.asKey("GetProductInfoGroup"));
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
.andCommandKey(KEY)
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetProductInfoPool"))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
/*线程池数量*/
.withCoreSize(10)
/*等待队列长度*/
.withMaxQueueSize(12)
.withQueueSizeRejectionThreshold(8))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
/*10s内经过的流量*/
.withCircuitBreakerRequestVolumeThreshold(30)
/*默认50%,设置40%的错误,超时,被拒绝,则开启短路器*/
.withCircuitBreakerErrorThresholdPercentage(40)
/*默认5s,这里设置3s尝试恢复*/
.withCircuitBreakerSleepWindowInMilliseconds(3000)
/*执行超时时间,设置的时间事宜大一些,否则如果请求在缓存队列中等待的时间长了,也会超时*/
.withExecutionTimeoutInMilliseconds(10000)
/*最多30个并发执行fallback*/
.withFallbackIsolationSemaphoreMaxConcurrentRequests(30))
);
this.productId = productId;
}
@Override
protected ProductInfo run() throws Exception {
if(productId == -1){
throw new RuntimeException();
}
if(productId == -2){
Thread.sleep(2000);
}
String url = "http://localhost:8082/getProductInfo?productId=" + productId;
String response = HttpClientUtils.sendGetRequest(url);
System.out.println(response);
ProductInfo productInfo = JSONObject.parseObject(response, ProductInfo.class);
System.out.println("调用接口查询 productId="+productId);
return productInfo;
}
public static void flushCache(Long productId){
HystrixRequestCache.getInstance(KEY,
HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf("product_info_" + productId));
}
@Override
protected ProductInfo getFallback() {
System.out.println("走降级了");
ProductInfo productInfo = new ProductInfo();
productInfo.setName("降级商品");
return productInfo;
}
}
public class RejectTest {
public static void main(String[] args) {
for(int i=0;i< 25;i++){
new TestThread(i).start();
}
}
private static class TestThread extends Thread{
private int index;
public TestThread(int index){
this.index = index;
}
@Override
public void run() {
String response = HttpClientUtils.sendGetRequest("http://localhost:8081/getProductInfo?productId=-2");
System.out.println("第"+index+"次 请求,返回: "+response);
}
}
}
@RequestMapping("/getProductInfo")
@ResponseBody
public String getProductInfo(Long productId){
HystrixCommand<ProductInfo> hystrixCommand = new GetProductInfoCommand(productId);
ProductInfo productInfo = hystrixCommand.execute();
Long cityId = productInfo.getCityId();
GetCityNameCommand getCityNameCommand = new GetCityNameCommand(cityId);
String cityName = getCityNameCommand.execute();
productInfo.setCityName(cityName);
return JSON.toJSONString(productInfo);
}
结果发现有10+8个线程请求到了数据,而7个线程降级处理了