在分布式系统中,每个服务都可能会调用很多其他服务,被调用的那些服务就是依赖服务,有的时候某些依赖服务出现故障也是很正常的。Hystrix 可以让我们在分布式系统中对服务间的调用进行控制,加入一些调用延迟或者依赖故障的容错机制。Hystrix 通过将依赖服务进行资源隔离,进而阻止某个依赖服务出现故障时在整个系统所有的依赖服务调用中进行蔓延;同时Hystrix 还提供故障时的 fallback 降级机制。总而言之,Hystrix 通过这些方法帮助我们提升分布式系统的可用性和稳定性。
fail-fast
(快速失败)和快速恢复的支持。fail fast
来控制故障。bulkhead
(舱壁隔离技术)、swimlane
(泳道技术)、circuit breaker
(短路技术)来限制任何一个依赖服务的故障的影响。小型电商网站的页面展示采用页面全量静态化的思想。数据库中存放了所有的商品信息,页面静态化系统,将数据填充进静态模板中,形成静态化页面,推入 Nginx 服务器。用户浏览网站页面时,取用一个已经静态化好的 html 页面,直接返回回去,不涉及任何的业务逻辑处理。
大型电商网站商品详情页的系统设计中,当商品信息发生变更时,会将变更消息压入消息队列中。缓存服务从消息队列中消费此消息时,发现有信息发生变更,便通过调用接口,获取变更后的数据。将整合好的数据推送至 redis 中。Nginx 获取到最新的缓存数据,并且缓存到 Nginx 自己本地中。用户浏览网页时,动态将 Nginx 本地数据渲染到本地 html 模板并返回给用户。
虽然没有直接返回 html 页面那么快,但是因为数据在本地缓存,所以也很快,其实耗费的也就是动态渲染一个 html 页面的性能。如果 html 模板发生了变更,不需要将所有的页面重新静态化,直接将数据渲染进最新的 html 页面模板后响应即可。在这种架构下,我们需要保证系统的高可用性。如果系统访问量很高,Nginx 本地缓存过期失效了,redis 中的缓存也被 LRU 算法给清理掉了,那么会有较高的访问量,从缓存服务调用商品服务。但如果此时商品服务的接口发生故障,调用出现了延时,缓存服务全部的线程都被这个调用商品服务接口给耗尽了,每个线程去调用商品服务接口的时候,都会卡住很长时间,后面大量的请求过来都会卡在那儿,此时缓存服务没有足够的线程去调用其它一些服务的接口,从而导致整个大量的商品详情页无法正常显示。这其实就是一个商品接口服务故障导致缓存服务资源耗尽的现象。
Hystrix 里面核心的一项功能,其实就是所谓的资源隔离,要解决的最最核心的问题,就是将多个依赖服务的调用分别隔离到各自的资源池内。避免说对某一个依赖服务的调用,因为依赖服务的接口调用的延迟或者失败,导致服务所有的线程资源全部耗费在这个服务的接口调用上。一旦说某个服务的线程资源全部耗尽的话,就可能导致服务崩溃,甚至说这种故障会不断蔓延。Hystrix 实现资源隔离,主要有两种技术:
默认情况下,Hystrix 使用线程池模式。
1.线程隔离
线程池隔离技术,并不是说去控制类似 tomcat 这种 web 容器的线程。更加严格的意义上来说,Hystrix 的线程池隔离技术,控制的是 tomcat 线程的执行。Hystrix 线程池满后,会确保说,tomcat 的线程不会因为依赖服务的接口调用延迟或故障而被 hang 住,tomcat 其它的线程不会卡死,可以快速返回,然后支撑其它的事情。
线程池隔离技术,是用 Hystrix 自己的线程去执行调用;而信号量隔离技术,是直接让 tomcat 线程去调用依赖服务。信号量隔离,只是一道关卡,信号量有多少,就允许多少个 tomcat 线程通过它,然后去执行。
2.信号量机制
信号量的资源隔离只是起到一个开关的作用,比如,服务 A 的信号量大小为 10,那么就是说它同时只允许有 10 个 tomcat 线程来访问服务 A,其它的请求都会被拒绝,从而达到资源隔离和限流保护的作用。
3.线程隔离与信号量隔离区别
4.测试信号量隔离
一般来说,缓存服务,可能会将一些量特别少、访问又特别频繁的数据,放在自己的纯内存中。举个栗子。一般我们在获取到商品数据之后,都要去获取商品是属于哪个地理位置、省、市、卖家等,可能在自己的纯内存中,比如就一个 Map 去获取。对于这种直接访问本地内存的逻辑,比较适合用信号量做一下简单的隔离。优点在于,不用自己管理线程池啦,不用 care timeout 超时啦,也不需要进行线程的上下文切换啦。信号量做隔离的话,性能相对来说会高一些。假如这是本地缓存,我们可以通过 cityId,拿到 cityName。
public class LocationCache {
private static Map cityMap = new HashMap<>();
static {
cityMap.put(1L, "北京");
}
/**
* 通过cityId 获取 cityName
*
* @param cityId 城市id
* @return 城市名
*/
public static String getCityName(Long cityId) {
return cityMap.get(cityId);
}
}
写一个 GetCityNameCommand,策略设置为信号量。run() 方法中获取本地缓存。我们目的就是对获取本地缓存的代码进行资源隔离。
public class GetCityNameCommand extends HystrixCommand {
private Long cityId;
public GetCityNameCommand(Long cityId) {
// 设置信号量隔离策略
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetCityNameGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)));
this.cityId = cityId;
}
@Override
protected String run() {
// 需要进行信号量隔离的代码
return LocationCache.getCityName(cityId);
}
}
在接口层,通过创建 GetCityNameCommand,传入 cityId,执行 execute() 方法,那么获取本地 cityName 缓存的代码将会进行信号量的资源隔离。
@RequestMapping("/getProductInfo")
@ResponseBody
public String getProductInfo(Long productId) {
HystrixCommand getProductInfoCommand = new GetProductInfoCommand(productId);
// 通过command执行,获取最新商品数据
ProductInfo productInfo = getProductInfoCommand.execute();
Long cityId = productInfo.getCityId();
GetCityNameCommand getCityNameCommand = new GetCityNameCommand(cityId);
// 获取本地内存(cityName)的代码会被信号量进行资源隔离
String cityName = getCityNameCommand.execute();
productInfo.setCityName(cityName);
System.out.println(productInfo);
return "success";
}
1.execution.isolation.strategy
指定了 HystrixCommand.run() 的资源隔离策略:THREAD
or SEMAPHORE
,一种基于线程池,一种基于信号量。
// 线程池隔离
HystrixCommandProperties.Setter().withExecutionIsolationStrategy(ExecutionIsolationStrategy.THREAD)
// 信号量隔离
HystrixCommandProperties.Setter().withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)
线程池机制,每个 command 运行在一个线程中,限流是通过线程池的大小来控制的;信号量机制,command 是运行在调用线程中,通过信号量的容量来进行限流。默认的策略就是线程池。线程池其实最大的好处就是对于网络访问请求,如果有超时的话,可以避免调用线程阻塞住。而使用信号量的场景,通常是针对超大并发量的场景下,每个服务实例每秒都几百的 QPS
,那么此时你用线程池的话,线程一般不会太多,可能撑不住那么高的并发,如果要撑住,可能要耗费大量的线程资源,那么就是用信号量,来进行限流保护。一般用信号量常见于那种基于纯内存的一些业务逻辑服务,而不涉及到任何网络访问请求。
2.command key & command group
每一个 command,都可以设置一个自己的名称 command key,同时可以设置一个自己的组 command group。command group 是一个非常重要的概念,默认情况下,就是通过 command group 来定义一个线程池的,而且还会通过 command group 来聚合一些监控和报警信息。同一个 command group 中的请求,都会进入同一个线程池中。
3.command thread pool
ThreadPoolKey 代表了一个 HystrixThreadPool,用来进行统一监控、统计、缓存。默认的 ThreadPoolKey 就是 command group 的名称。每个 command 都会跟它的 ThreadPoolKey 对应的 ThreadPool 绑定在一起。如果不想直接用 command group,也可以手动设置 ThreadPool 的名称。
4.command key & command group & command thread pool
command key ,代表了一类 command,一般来说,代表了底层的依赖服务的一个接口。
command group ,代表了某一个底层的依赖服务,这是很合理的,一个依赖服务可能会暴露出来多个接口,每个接口就是一个 command key。command group 在逻辑上去组织起来一堆 command key 的调用、统计信息、成功次数、timeout 超时次数、失败次数等,可以看到某一个服务整体的一些访问情况。一般来说,推荐根据一个服务区划分出一个线程池,command key 默认都是属于同一个线程池的。
比如说你以一个服务为粒度,估算出来这个服务每秒的所有接口加起来的整体 QPS
在 100 左右,你调用这个服务,当前这个服务部署了 10 个服务实例,每个服务实例上,其实用这个 command group 对应这个服务,给一个线程池,量大概在 10 个左右就可以了,你对整个服务的整体的访问 QPS 就大概在每秒 100 左右。但是,如果说 command group 对应了一个服务,而这个服务暴露出来的几个接口,访问量很不一样,差异非常之大。你可能就希望在这个服务 command group 内部,包含的对应多个接口的 command key,做一些细粒度的资源隔离。就是说,对同一个服务的不同接口,使用不同的线程池。
逻辑上来说,多个 command key 属于一个command group,在做统计的时候,会放在一起统计。每个 command key 有自己的线程池,每个接口有自己的线程池,去做资源隔离和限流。说白点,就是说如果你的 command key 要用自己的线程池,可以定义自己的 thread pool key,就 ok 了。
5.coreSize
设置线程池的大小,默认是 10。一般来说,用这个默认的 10 个线程大小就够了。
HystrixThreadPoolProperties.Setter().withCoreSize(int value);
6.queueSizeRejectionThreshold
如果说线程池中的 10 个线程都在工作中,没有空闲的线程来做其它的事情,此时再有请求过来,会先进入队列积压。如果说队列积压满了,再有请求过来,就直接 reject,拒绝请求,执行 fallback 降级的逻辑,快速返回。控制 queue 满了之后 reject 的 threshold,因为 maxQueueSize 不允许热修改,因此提供这个参数可以热修改,控制队列的最大大小。
HystrixThreadPoolProperties.Setter().withQueueSizeRejectionThreshold(int value);
7.execution.isolation.semaphore.maxConcurrentRequests
设置使用 SEMAPHORE 隔离策略的时候允许访问的最大并发量,超过这个最大并发量,请求直接被 reject。这个并发量的设置,跟线程池大小的设置,应该是类似的,但是基于信号量的话,性能会好很多,而且 Hystrix 框架本身的开销会小很多。默认值是 10,尽量设置的小一些,因为一旦设置的太大,而且有延时发生,可能瞬间导致 tomcat 本身的线程资源被占满。
HystrixCommandProperties.Setter().withExecutionIsolationSemaphoreMaxConcurrentRequests(int value);
1.创建 command
一个 HystrixCommand 或 HystrixObservableCommand 对象,代表了对某个依赖服务发起的一次请求或者调用。创建的时候,可以在构造函数中传入任何需要的参数。
// 创建 HystrixCommand
HystrixCommand hystrixCommand = new HystrixCommand(arg1, arg2);
// 创建 HystrixObservableCommand
HystrixObservableCommand hystrixObservableCommand = new HystrixObservableCommand(arg1, arg2);
2.调用 command 执行方法
执行 command,就可以发起一次对依赖服务的调用。要执行 command,可以在 4 个方法中选择其中的一个:execute()、queue()、observe()、toObservable()。其中 execute() 和 queue() 方法仅仅对 HystrixCommand 适用。
3.检查是否开启缓存
从这一步开始,就进入到 Hystrix 底层运行原理啦,看一下 Hystrix 一些更高级的功能和特性。如果这个 command 开启了请求缓存 Request Cache,而且这个调用的结果在缓存中存在,那么直接从缓存中返回结果。否则,继续往后的步骤。
4.检查是否开启了断路器
检查这个 command 对应的依赖服务是否开启了断路器。如果断路器被打开了,那么 Hystrix 就不会执行这个 command,而是直接去执行 fallback 降级机制,返回降级结果。
5.检查线程池/队列/信号量是否已满
如果这个 command 线程池和队列已满,或者 semaphore 信号量已满,那么也不会执行 command,而是直接去调用 fallback 降级机制,同时发送 reject 信息给断路器统计。
6.执行 command
调用 HystrixObservableCommand 对象的 construct() 方法,或者 HystrixCommand 的 run() 方法来实际执行这个 command。如果是采用线程池方式,并且 HystrixCommand.run() 或者 HystrixObservableCommand.construct() 的执行时间超过了 timeout 时长的话,那么 command 所在的线程会抛出一个 TimeoutException,这时会执行 fallback 降级机制,不会去管 run() 或 construct() 返回的值了。另一种情况,如果 command 执行出错抛出了其它异常,那么也会走 fallback 降级。这两种情况下,Hystrix 都会发送异常事件给断路器统计。注意,我们是不可能终止掉一个调用严重延迟的依赖服务的线程的,只能说给你抛出来一个 TimeoutException。如果没有 timeout,也正常执行的话,那么调用线程就会拿到一些调用依赖服务获取到的结果,然后 Hystrix 也会做一些 logging 记录和 metric 度量统计。
7.断路健康检查
Hystrix 会把每一个依赖服务的调用成功、失败、Reject、Timeout 等事件发送给 circuit breaker 断路器。断路器就会对这些事件的次数进行统计,根据异常事件发生的比例来决定是否要进行断路(熔断)。如果打开了断路器,那么在接下来一段时间内,会直接断路,返回降级结果。如果在之后,断路器尝试执行 command,调用没有出错,返回了正常结果,那么 Hystrix 就会把断路器关闭。
8.调用 fallback 降级机制
在以下几种情况中,Hystrix 会调用 fallback 降级机制。
一般在降级机制中,都建议给出一些默认的返回值,比如静态的一些代码逻辑,或者从内存中的缓存中提取一些数据,在这里尽量不要再进行网络请求了。在降级中,如果一定要进行网络调用的话,也应该将那个调用放在一个 HystrixCommand 中进行隔离。
如果没有实现 fallback,或者 fallback 抛出了异常,Hystrix 会返回一个 Observable,但是不会返回任何数据。不同的 command 执行方式,其 fallback 为空或者异常时的返回结果不同。
Hystrix command 执行时 8 大步骤第三步,就是检查 Request cache 是否有缓存。首先,有一个概念,叫做 Request Context 请求上下文,一般来说,在一个 web 应用中,如果我们用到了 Hystrix,我们会在一个 filter 里面,对每一个请求都施加一个请求上下文。就是说,每一次请求,就是一次请求上下文。然后在这次请求上下文中,我们会去执行 N 多代码,调用 N 多依赖服务,有的依赖服务可能还会调用好几次。
在一次请求上下文中,如果有多个 command,参数都是一样的,调用的接口也是一样的,而结果可以认为也是一样的。那么这个时候,我们可以让第一个 command 执行返回的结果缓存在内存中,然后这个请求上下文后续的其它对这个依赖的调用全部从内存中取出缓存结果就可以了。这样的话,好处在于不用在一次请求上下文中反复多次执行一样的 command,避免重复执行网络请求,提升整个请求的性能。
举个栗子。比如说我们在一次请求上下文中,请求获取 productId 为 1 的数据,第一次缓存中没有,那么会从商品服务中获取数据,返回最新数据结果,同时将数据缓存在内存中。后续同一次请求上下文中,如果还有获取 productId 为 1 的数据的请求,直接从缓存中取就好了。
HystrixCommand 和 HystrixObservableCommand 都可以指定一个缓存 key,然后 Hystrix 会自动进行缓存,接着在同一个 request context 内,再次访问的话,就会直接取用缓存。
下面,我们结合一个具体的业务场景,来看一下如何使用 request cache 请求缓存技术。当然,以下代码只作为一个基本的 Demo 而已。
现在,假设我们要做一个批量查询商品数据的接口,在这个里面,我们是用 HystrixCommand 一次性批量查询多个商品 id 的数据。但是这里有个问题,如果说 Nginx 在本地缓存失效了,重新获取一批缓存,传递过来的 productIds 都没有进行去重,比如 productIds=1,1,1,2,2
,那么可能说,商品 id 出现了重复,如果按照我们之前的业务逻辑,可能就会重复对 productId=1 的商品查询三次,productId=2 的商品查询两次。我们对批量查询商品数据的接口,可以用 request cache 做一个优化,就是说一次请求,就是一次 request context,对相同的商品查询只执行一次,其余重复的都走 request cache。
1.定义 HystrixRequestContextFilter 类,实现 Filter 接口。
/**
* Hystrix 请求上下文过滤器
*/
public class HystrixRequestContextFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
filterChain.doFilter(servletRequest, servletResponse);
} catch (IOException | ServletException e) {
e.printStackTrace();
} finally {
context.shutdown();
}
}
@Override
public void destroy() {
}
}
一个请求经过过滤器获取的request context,在同一个request context中相同参数调用服务,只会第一次执行请求调用服务,其他的都会走request cache
2.然后将该 filter 对象注册到 SpringBoot Application 中。
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new HystrixRequestContextFilter());
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
3.command 重写 getCacheKey() 方法
在 GetProductInfoCommand 中,重写 getCacheKey() 方法,这样的话,每一次请求的结果,都会放在 Hystrix 请求上下文中。下一个同一个 productId 的数据请求,直接取缓存,无须再调用 run() 方法。
public class GetProductInfoCommand extends HystrixCommand {
private Long productId;
private static final HystrixCommandKey KEY = HystrixCommandKey.Factory.asKey("GetProductInfoCommand");
public GetProductInfoCommand(Long productId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
.andCommandKey(KEY));
this.productId = productId;
}
@Override
protected ProductInfo run() {
String url = "http://localhost:8081/getProductInfo?productId=" + productId;
String response = HttpClientUtils.sendGetRequest(url);
System.out.println("调用接口查询商品数据,productId=" + productId);
return JSONObject.parseObject(response, ProductInfo.class);
}
/**
* 每次请求的结果,都会放在Hystrix绑定的请求上下文上
*
* @return cacheKey 缓存key
*/
@Override
public String getCacheKey() {
return "product_info_" + productId;
}
/**
* 将某个商品id的缓存清空
* flushCache() 方法,用于我们开发手动删除缓存
*
* @param productId 商品id
*/
public static void flushCache(Long productId) {
HystrixRequestCache.getInstance(KEY,
HystrixConcurrencyStrategyDefault.getInstance()).clear("product_info_" + productId);
}
}
4.controller 调用 command
在一次 web 请求上下文中,传入商品 id 列表,查询多条商品数据信息。对于每个 productId,都创建一个 command。如果 id 列表没有去重,那么重复的 id,第二次查询的时候就会直接走缓存。
@Controller
public class CacheController {
/**
* 一次性批量查询多条商品数据的请求
*
* @param productIds 以,分隔的商品id列表
* @return 响应状态
*/
@RequestMapping("/getProductInfos")
@ResponseBody
public String getProductInfos(String productIds) {
for (String productId : productIds.split(",")) {
// 对每个productId,都创建一个command
GetProductInfoCommand getProductInfoCommand = new GetProductInfoCommand(Long.valueOf(productId));
ProductInfo productInfo = getProductInfoCommand.execute();
System.out.println("是否是从缓存中取的结果:" + getProductInfoCommand.isResponseFromCache());
}
return "success";
}
}
http://localhost:8080/getProductInfos?productIds=1,1,1,2,2,5,第一次查询 productId=1 的数据,会调用接口进行查询,不是从缓存中取结果。而随后再出现查询 productId=1 的请求,就直接取缓存了,这样的话,效率明显高很多。
5.删除缓存
我们写一个 UpdateProductInfoCommand,在更新商品信息之后,手动调用之前写的 flushCache(),手动将缓存删除。
public class UpdateProductInfoCommand extends HystrixCommand {
private Long productId;
public UpdateProductInfoCommand(Long productId) {
super(HystrixCommandGroupKey.Factory.asKey("UpdateProductInfoGroup"));
this.productId = productId;
}
@Override
protected Boolean run() throws Exception {
// 这里执行一次商品信息的更新
// ...
// 然后清空缓存
GetProductInfoCommand.flushCache(productId);
return true;
}
}
1.两种最经典的降级机制
在 HystrixCommand
,降级逻辑的书写,是通过实现 getFallback() 接口;而在 HystrixObservableCommand
中,则是实现 resumeWithFallback() 方法。
现在,我们用一个简单的栗子,来演示 fallback 降级是怎么做的。比如,有这么个场景。我们现在有个包含 brandId 的商品数据,假设正常的逻辑是这样:拿到一个商品数据,根据 brandId 去调用品牌服务的接口,获取品牌的最新名称 brandName。假如说,品牌服务接口挂掉了,那么我们可以尝试从本地内存中,获取一份稍过期的数据,先凑合着用。
2.本地缓存获取数据
/**
* 品牌名称本地缓存
*
*/
public class BrandCache {
private static Map brandMap = new HashMap<>();
static {
brandMap.put(1L, "Nike");
}
/**
* brandId 获取 brandName
* @param brandId 品牌id
* @return 品牌名
*/
public static String getBrandName(Long brandId) {
return brandMap.get(brandId);
}
}
3.实现 GetBrandNameCommand
在 GetBrandNameCommand 中,run() 方法的正常逻辑是去调用品牌服务的接口获取到品牌名称,如果调用失败,报错了,那么就会去调用 fallback 降级机制。这里,我们直接模拟接口调用报错,给它抛出个异常。而在 getFallback() 方法中,就是我们的降级逻辑,我们直接从本地的缓存中,获取到品牌名称的数据。
/**
* 获取品牌名称的command
*
*/
public class GetBrandNameCommand extends HystrixCommand {
private Long brandId;
public GetBrandNameCommand(Long brandId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("BrandService"))
.andCommandKey(HystrixCommandKey.Factory.asKey("GetBrandNameCommand"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
// 设置降级机制最大并发请求数
.withFallbackIsolationSemaphoreMaxConcurrentRequests(15)));
this.brandId = brandId;
}
@Override
protected String run() throws Exception {
// 这里正常的逻辑应该是去调用一个品牌服务的接口获取名称
// 如果调用失败,报错了,那么就会去调用fallback降级机制
// 这里我们直接模拟调用报错,抛出异常
throw new Exception();
}
@Override
protected String getFallback() {
return BrandCache.getBrandName(brandId);
}
}
FallbackIsolationSemaphoreMaxConcurrentRequests
用于设置 fallback 最大允许的并发请求量,默认值是 10,是通过 semaphore 信号量的机制去限流的。如果超出了这个最大值,那么直接 reject。
4.CacheController 调用接口
在 CacheController 中,我们通过 productInfo 获取 brandId,然后创建 GetBrandNameCommand 并执行,去尝试获取 brandName。这里执行会报错,因为我们在 run() 方法中直接抛出异常,Hystrix 就会去调用 getFallback() 方法走降级逻辑。
@Controller
public class CacheController {
@RequestMapping("/getProductInfo")
@ResponseBody
public String getProductInfo(Long productId) {
HystrixCommand getProductInfoCommand = new GetProductInfoCommand(productId);
ProductInfo productInfo = getProductInfoCommand.execute();
Long brandId = productInfo.getBrandId();
HystrixCommand getBrandNameCommand = new GetBrandNameCommand(brandId);
// 执行会抛异常报错,然后走降级
String brandName = getBrandNameCommand.execute();
productInfo.setBrandName(brandName);
System.out.println(productInfo);
return "success";
}
}
1.RequestVolumeThreshold
HystrixCommandProperties.Setter().withCircuitBreakerRequestVolumeThreshold(int)
表示在滑动窗口中,至少有多少个请求,才可能触发断路。Hystrix 经过断路器的流量超过了一定的阈值,才有可能触发断路。比如说,要求在 10s 内经过断路器的流量必须达到 20 个,而实际经过断路器的流量才 10 个,那么根本不会去判断要不要断路。
2.ErrorThresholdPercentage
HystrixCommandProperties.Setter().withCircuitBreakerErrorThresholdPercentage(int)
表示异常比例达到多少,才会触发断路,默认值是 50(%)。如果断路器统计到的异常调用的占比超过了一定的阈值,比如说在 10s 内,经过断路器的流量达到了 30 个,同时其中异常访问的数量也达到了一定的比例,比如 60% 的请求都是异常(报错 / 超时 / reject),就会开启断路。
3.SleepWindowInMilliseconds
HystrixCommandProperties.Setter().withCircuitBreakerSleepWindowInMilliseconds(int)
断路开启,也就是由 close 转换到 open 状态(close -> open)。那么之后在 SleepWindowInMilliseconds
时间内,默认5s,所有经过该断路器的请求全部都会被断路,不调用后端服务,直接走 fallback 降级机制。而在该参数时间过后,断路器会变为 half-open
半开闭状态,尝试让一条请求经过断路器,看能不能正常调用。如果调用成功了,那么就自动恢复,断路器转为 close 状态。
4.Enabled
HystrixCommandProperties.Setter().withCircuitBreakerEnabled(boolean)
控制是否允许断路器工作,包括跟踪依赖服务调用的健康状况,以及对异常情况过多时是否允许触发断路。默认值是 true
。
5.ForceOpen
HystrixCommandProperties.Setter().withCircuitBreakerForceOpen(boolean)
如果设置为 true 的话,直接强迫打开断路器,相当于是手动断路了,手动降级,默认值是 false
。
6.ForceClosed
HystrixCommandProperties.Setter().withCircuitBreakerForceClosed(boolean)
如果设置为 true,直接强迫关闭断路器,相当于手动停止断路了,手动升级,默认值是 false
。
7.测试案例
在 GetProductInfoCommand 中配置 Setter 断路器相关参数。
run() 方法中,我们判断一下 productId 是否为 -1,是的话,直接抛出异常。这么写,我们之后测试的时候就可以传入 productId=-1,模拟服务执行异常了。在降级逻辑中,我们直接给它返回降级商品就好了。
public class GetProductInfoCommand extends HystrixCommand {
private Long productId;
private static final HystrixCommandKey KEY = HystrixCommandKey.Factory.asKey("GetProductInfoCommand");
public GetProductInfoCommand(Long productId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
.andCommandKey(KEY)
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
// 是否允许断路器工作
.withCircuitBreakerEnabled(true)
// 滑动窗口中,最少有多少个请求,才可能触发断路
.withCircuitBreakerRequestVolumeThreshold(20)
// 异常比例达到多少,才触发断路,默认50%
.withCircuitBreakerErrorThresholdPercentage(40)
// 断路后多少时间内直接reject请求,之后进入half-open状态,默认5000ms
.withCircuitBreakerSleepWindowInMilliseconds(3000)));
this.productId = productId;
}
@Override
protected ProductInfo run() throws Exception {
System.out.println("调用接口查询商品数据,productId=" + productId);
if (productId == -1L) {
throw new Exception();
}
String url = "http://localhost:8081/getProductInfo?productId=" + productId;
String response = HttpClientUtils.sendGetRequest(url);
return JSONObject.parseObject(response, ProductInfo.class);
}
@Override
protected ProductInfo getFallback() {
ProductInfo productInfo = new ProductInfo();
productInfo.setName("降级商品");
return productInfo;
}
}
断路测试类,我们在测试类中,前 30 次请求,传入 productId=-1,然后休眠 3s,之后 70 次请求,传入 productId=1。
@SpringBootTest
@RunWith(SpringRunner.class)
public class CircuitBreakerTest {
@Test
public void testCircuitBreaker() {
String baseURL = "http://localhost:8080/getProductInfo?productId=";
for (int i = 0; i < 30; ++i) {
// 传入-1,会抛出异常,然后走降级逻辑
HttpClientUtils.sendGetRequest(baseURL + "-1");
}
TimeUtils.sleep(3);
System.out.println("After sleeping...");
for (int i = 31; i < 100; ++i) {
// 传入1,走服务正常调用
HttpClientUtils.sendGetRequest(baseURL + "1");
}
}
}
测试发现:前 30 次请求,我们传入的 productId 为 -1,所以服务执行过程中会抛出异常。我们设置了最少 20 次请求通过断路器并且异常比例超出 40% 就触发断路。因此执行了 21 次接口调用,每次都抛异常并且走降级,21 次过后,断路器就被打开了。之后的 9 次请求,都不会执行 run() 方法,而是直接走降级逻辑,调用 getFallback() 执行。休眠了 3s 后,我们在之后的 70 次请求中,都传入 productId 为 1。由于我们前面设置了 3000ms 过后断路器变为 half-open
状态。因此 Hystrix 会尝试执行请求,发现成功了,那么断路器关闭,之后的所有请求也都能正常调用了。
1.线程池隔离技术的设计
Hystrix 采用了 Bulkhead Partition 舱壁隔离技术,来将外部依赖进行资源隔离,进而避免任何外部依赖的故障导致本服务崩溃。舱壁隔离,是说将船体内部空间区隔划分成若干个隔舱,一旦某几个隔舱发生破损进水,水流不会在其间相互流动,如此一来船舶在受损时,依然能具有足够的浮力和稳定性,进而减低立即沉船的危险。Hystrix 对每个外部依赖用一个单独的线程池,这样的话,如果对那个外部依赖调用延迟很严重,最多就是耗尽那个依赖自己的线程池而已,不会影响其他的依赖调用。
2.Hystrix 应用线程池机制的场景
简单来说,就是你必须默认 client 调用库很不靠谱,而且随时可能发生各种变化,所以就要用强制隔离的方式来确保任何服务的故障不会影响当前服务。
3.线程池机制的优点
简单来说,最大的好处,就是资源隔离,确保说任何一个依赖服务故障,不会拖垮当前的这个服务。
4.线程池机制的缺点
我们可以用 Hystrix semaphore 技术来实现对某个依赖服务的并发访问量的限制,而不是通过线程池/队列的大小来限制流量。sempahore 技术可以用来限流和削峰,但是不能用来对调研延迟的服务进行 timeout 和隔离。execution.isolation.strategy
设置为 SEMAPHORE
,那么 Hystrix 就会用 semaphore 机制来替代线程池机制,来对依赖服务的访问进行限流。如果通过 semaphore 调用的时候,底层的网络调用延迟很严重,那么是无法 timeout 的,只能一直 block 住。一旦请求数量超过了 semephore 限定的数量之后,就会立即开启限流。
5.限流测试
假设一个线程池大小为 8,等待队列的大小为 10。timeout 时长我们设置长一些,20s。在 command 内部,写死代码,做一个 sleep,比如 sleep 3s。
如果只设置了线程池大小,另外两个 queue 相关参数没有设置的话,等待队列是处于关闭的状态。
public class GetProductInfoCommand extends HystrixCommand {
private Long productId;
private static final HystrixCommandKey KEY = HystrixCommandKey.Factory.asKey("GetProductInfoCommand");
public GetProductInfoCommand(Long productId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
.andCommandKey(KEY)
// 线程池相关配置信息
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
// 设置线程池大小为8
.withCoreSize(8)
// 设置等待队列大小为10
.withMaxQueueSize(10)
.withQueueSizeRejectionThreshold(12))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(true)
.withCircuitBreakerRequestVolumeThreshold(20)
.withCircuitBreakerErrorThresholdPercentage(40)
.withCircuitBreakerSleepWindowInMilliseconds(3000)
// 设置超时时间
.withExecutionTimeoutInMilliseconds(20000)
// 设置fallback最大请求并发数
.withFallbackIsolationSemaphoreMaxConcurrentRequests(30)));
this.productId = productId;
}
@Override
protected ProductInfo run() throws Exception {
System.out.println("调用接口查询商品数据,productId=" + productId);
if (productId == -1L) {
throw new Exception();
}
// 请求过来,会在这里hang住3秒钟
if (productId == -2L) {
TimeUtils.sleep(3);
}
String url = "http://localhost:8081/getProductInfo?productId=" + productId;
String response = HttpClientUtils.sendGetRequest(url);
System.out.println(response);
return JSONObject.parseObject(response, ProductInfo.class);
}
@Override
protected ProductInfo getFallback() {
ProductInfo productInfo = new ProductInfo();
productInfo.setName("降级商品");
return productInfo;
}
}
我们模拟 25 个请求。前 8 个请求,调用接口时会直接被 hang 住 3s,那么后面的 10 的请求会先进入等待队列中等待前面的请求执行完毕。最后的 7 个请求过来,会直接被 reject,调用 fallback 降级逻辑。
@SpringBootTest
@RunWith(SpringRunner.class)
public class RejectTest {
@Test
public void testReject() {
for (int i = 0; i < 25; ++i) {
new Thread(() -> HttpClientUtils.sendGetRequest("http://localhost:8080/getProductInfo?productId=-2")).start();
}
// 防止主线程提前结束执行
TimeUtils.sleep(50);
}
}
从执行结果中,我们可以明显看出一共打印出了 7 个降级商品。这也就是请求数超过线程池+队列的数量而直接被 reject 的结果。
ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
调用接口查询商品数据,productId=-2
调用接口查询商品数据,productId=-2
调用接口查询商品数据,productId=-2
调用接口查询商品数据,productId=-2
调用接口查询商品数据,productId=-2
调用接口查询商品数据,productId=-2
调用接口查询商品数据,productId=-2
调用接口查询商品数据,productId=-2
ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null)
{"id": -2, "name": "iphone7手机", "price": 5599, "pictureList":"a.jpg,b.jpg", "specification": "iphone7的规格", "service": "iphone7的售后服务", "color": "红色,白色,黑色", "size": "5.5", "shopId": 1, "modifiedTime": "2017-01-01 12:00:00", "cityId": 1, "brandId": 1}
// 后面都是一些正常的商品信息