有一个概念,叫做reqeust context,请求上下文,一般来说,在一个web应用中,
我们会在一个filter里面,对每一个请求都施加一个请求上下文,就是说,tomcat容器内,每一次请求,就是一次请求上下文
在一次请求上下文中,如果有多个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过滤器来实现的
public class HystrixRequestContextServletFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { chain.doFilter(request, response); } finally { context.shutdown(); } } } @Bean public FilterRegistrationBean indexFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean(new IndexFilter()); registration.addUrlPatterns("/"); return registration; }
对批量查询商品数据的接口,可以用request cache做一个优化,就是说一次请求,就是一次request context,对相同的商品查询只能执行一次,其余的都走request cache
public class CommandUsingRequestCache extends HystrixCommand<Boolean> { private final int value; protected CommandUsingRequestCache(int value) { super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); this.value = value; } @Override protected Boolean run() { return value == 0 || value % 2 == 0; } @Override protected String getCacheKey() { return String.valueOf(value); } } @Test public void testWithCacheHits() { HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { CommandUsingRequestCache command2a = new CommandUsingRequestCache(2); CommandUsingRequestCache command2b = new CommandUsingRequestCache(2); assertTrue(command2a.execute()); // this is the first time we've executed this command with // the value of "2" so it should not be from cache assertFalse(command2a.isResponseFromCache()); assertTrue(command2b.execute()); // this is the second time we've executed this command with // the same value so it should return from cache assertTrue(command2b.isResponseFromCache()); } finally { context.shutdown(); } // start a new request context context = HystrixRequestContext.initializeContext(); try { CommandUsingRequestCache command3b = new CommandUsingRequestCache(2); assertTrue(command3b.execute()); // this is a new request context so this // should not come from cache assertFalse(command3b.isResponseFromCache()); } finally { context.shutdown(); } } 缓存的手动清理 public static class GetterCommand extends HystrixCommand<String> { private static final HystrixCommandKey GETTER_KEY = HystrixCommandKey.Factory.asKey("GetterCommand"); private final int id; public GetterCommand(int id) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetSetGet")) .andCommandKey(GETTER_KEY)); this.id = id; } @Override protected String run() { return prefixStoredOnRemoteDataStore + id; } @Override protected String getCacheKey() { return String.valueOf(id); } /** * Allow the cache to be flushed for this object. * * @param id * argument that would normally be passed to the command */ public static void flushCache(int id) { HystrixRequestCache.getInstance(GETTER_KEY, HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf(id)); } } public static class SetterCommand extends HystrixCommand<Void> { private final int id; private final String prefix; public SetterCommand(int id, String prefix) { super(HystrixCommandGroupKey.Factory.asKey("GetSetGet")); this.id = id; this.prefix = prefix; } @Override protected Void run() { // persist the value against the datastore prefixStoredOnRemoteDataStore = prefix; // flush the cache GetterCommand.flushCache(id); // no return value return null; } }
短路器,如果打开了,fallback降级机制
hystrix调用各种接口,或者访问外部依赖,mysql,redis,zookeeper,kafka,等等,如果出现了任何异常的情况
比如说报错了,访问mysql报错,redis报错,zookeeper报错,kafka报错,error
对每个外部依赖,无论是服务接口,中间件,资源隔离,对外部依赖只能用一定量的资源去访问,线程池/信号量,如果资源池已满,reject
访问外部依赖的时候,访问时间过长,可能就会导致超时,报一个TimeoutException异常,timeout
上述三种情况,都是我们说的异常情况,对外部依赖的东西访问的时候出现了异常,发送异常事件到短路器中去进行统计
如果短路器发现异常事件的占比达到了一定的比例,直接开启短路,circuit breaker
上述四种情况,都会去调用fallback降级机制
fallback,降级机制,你之前都是必须去调用外部的依赖接口,或者从mysql中去查询数据的,但是为了避免说可能外部依赖会有故障
比如,你可以再内存中维护一个ehcache,作为一个纯内存的基于LRU自动清理的缓存,数据也可以放入缓存内
如果说外部依赖有异常,fallback这里,直接尝试从ehcache中获取数据
比如说,本来你是从mysql,redis,或者其他任何地方去获取数据的,获取调用其他服务的接口的,结果人家故障了,人家挂了,fallback,可以返回一个默认值
两种最经典的降级机制:纯内存数据,默认值
run()抛出异常,超时,线程池或信号量满了,或短路了,都会调用fallback机制
public class CommandHelloFailure extends HystrixCommand<String> { private final String name; public CommandHelloFailure(String name) { super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); this.name = name; } @Override protected String run() { throw new RuntimeException("this command always fails"); } @Override protected String getFallback() { return "Hello Failure " + name + "!"; } } @Test public void testSynchronous() { assertEquals("Hello Failure World!", new CommandHelloFailure("World").execute()); } HystrixObservableCommand,是实现resumeWithFallback方法 2、fallback.isolation.semaphore.maxConcurrentRequests 这个参数设置了HystrixCommand.getFallback()最大允许的并发请求数量,默认值是10,也是通过semaphore信号量的机制去限流 如果超出了这个最大值,那么直接被reject HystrixCommandProperties.Setter() .withFallbackIsolationSemaphoreMaxConcurrentRequests(int value)
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,半开状态
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(HystrixCommandKey.Factory.asKey("GetProductInfoCommand")) .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetProductInfoPool")) .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter() .withCoreSize(15) .withQueueSizeRejectionThreshold(10)) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() .withCircuitBreakerRequestVolumeThreshold(30) .withCircuitBreakerErrorThresholdPercentage(40) .withCircuitBreakerSleepWindowInMilliseconds(3000)) ); this.productId = productId; }
测试:
public class CircuitBreakerTest { public static void main(String[] args) throws Exception { for(int i = 0; i < 15; i++) { String response = HttpClientUtils.sendGetRequest("http://localhost:8081/getProductInfo?productId=1"); System.out.println("第" + (i + 1) + "次请求,结果为:" + response); } for(int i = 0; i < 25; i++) { String response = HttpClientUtils.sendGetRequest("http://localhost:8081/getProductInfo?productId=-1"); System.out.println("第" + (i + 1) + "次请求,结果为:" + response); } Thread.sleep(5000); // 等待了5s后,时间窗口统计了,发现异常比例太多,就短路了 for(int i = 0; i < 10; i++) { String response = HttpClientUtils.sendGetRequest("http://localhost:8081/getProductInfo?productId=-1"); System.out.println("第" + (i + 1) + "次请求,结果为:" + response); } // 统计单位,有一个时间窗口的,我们必须要等到那个时间窗口过了以后,才会说,hystrix看一下最近的这个时间窗口 // 比如说,最近的10秒内,有多少条数据,其中异常的数据有没有到一定的比例 // 如果到了一定的比例,那么才会去短路 System.out.println("尝试等待3秒钟。。。。。。"); Thread.sleep(5000); for(int i = 0; i < 10; i++) { String response = HttpClientUtils.sendGetRequest("http://localhost:8081/getProductInfo?productId=1"); System.out.println("第" + (i + 1) + "次请求,结果为:" + response); } } }
1、线程池隔离技术的设计原则
Hystrix采取了bulkhead舱壁隔离技术,来将外部依赖进行资源隔离,进而避免任何外部依赖的故障导致本服务崩溃
线程池隔离,学术名称:bulkhead,舱壁隔离
外部依赖的调用在单独的线程中执行,这样就能跟调用线程隔离开来,避免外部依赖调用timeout耗时过长,导致调用线程被卡死
Hystrix对每个外部依赖用一个单独的线程池,这样的话,如果对那个外部依赖调用延迟很严重,最多就是耗尽那个依赖自己的线程池而已,不会影响其他的依赖调用
Hystrix选择用线程池机制来进行资源隔离,要面对的场景如下:
(1)每个服务都会调用几十个后端依赖服务,那些后端依赖服务通常是由很多不同的团队开发的
(2)每个后端依赖服务都会提供它自己的client调用库,比如说用thrift的话,就会提供对应的thrift依赖
(3)client调用库随时会变更
(4)client调用库随时可能会增加新的网络请求的逻辑
(5)client调用库可能会包含诸如自动重试,数据解析,内存中缓存等逻辑
(6)client调用库一般都对调用者来说是个黑盒,包括实现细节,网络访问,默认配置,等等
(7)在真实的生产环境中,经常会出现调用者,突然间惊讶的发现,client调用库发生了某些变化
(8)即使client调用库没有改变,依赖服务本身可能有会发生逻辑上的变化
(9)有些依赖的client调用库可能还会拉取其他的依赖库,而且可能那些依赖库配置的不正确
(10)大多数网络请求都是同步调用的
(11)调用失败和延迟,也有可能会发生在client调用库本身的代码中,不一定就是发生在网络请求中
简单来说,就是你必须默认client调用库就很不靠谱,而且随时可能各种变化,所以就要用强制隔离的方式来确保任何服务的故障不能影响当前服务
线程池机制的优点如下:
(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住
一旦请求数量超过了semephore限定的数量之后,就会立即开启限流。
如果你不对各种依赖接口的调用,做超时的控制,来给你的服务提供安全保护措施,那么很可能你的服务就被各种垃圾的依赖服务的性能给拖死了
大量的接口调用很慢,大量线程就卡死了,资源隔离,线程池的线程卡死了,超时的控制
(1)execution.isolation.thread.timeoutInMilliseconds
手动设置timeout时长,一个command运行超出这个时间,就被认为是timeout,然后将hystrix command标识为timeout,同时执行fallback降级逻辑
默认是1000,也就是1000毫秒
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(int value)
(2)execution.timeout.enabled
控制是否要打开timeout机制,默认是true
HystrixCommandProperties.Setter()
.withExecutionTimeoutEnabled(boolean value)