如何使用Hystrix实现服务请求限流?
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/131/ )。
概述
限流就是为某个方法增加流量阈值,当超过该阈值时,外部请求转而去调用备用方法,进而降低了原方法的压力,防止原方法因为过高的流量崩溃或给系统带来较大的时延。
环境配置
通过Hystrix就可以方便地实现这一操作。接下来,我们给出简单的示例。
首先,准备SpringBoot项目,然后在其中引入适用于SpringBoot的hystrix包:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
注意,如果这时候启动提示如下的错误,则基本是SpringBoot版本与hystrix版本不匹配导致的。因为hystrix是基于SpringCloud的,而SpringCloud和SpringBoot存在版本对应关系。如果对应不一致,则可能导致出现下面的错误。
java.lang.NoSuchMethodError: 'void org.springframework.boot.builder.SpringApplicationBuilder.<init>(java.lang.Object[])'
这里我们给出一个可用的对应版本,即SpringBoot的2.3.5.RELEASE和spring-cloud-starter-netflix-hystrix的2.2.7.RELEASE。它们这一对是肯定没有问题的。
然后,我们需要在SpringBoot的主类上增加@EnableHystrix注解,以启用Hystrix。如下所示。
@SpringBootApplication
@EnableHystrix
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
环境配置就是这么简单,接下来我们就可以写业务代码了。
功能实现
例如我们编写如下的方法,也就是原方法。在原方法中,我们故意让它处理时长较长,以模拟复杂的操作。
@HystrixCommand(
commandKey = "LimiterService_queryUserNameById",
fallbackMethod = "fallBackForSayHi",
threadPoolProperties = {
// 核心线程设置为5
@HystrixProperty(name = HystrixPropertiesManager.CORE_SIZE, value = "5"),
// 最大排队队列长度为10
@HystrixProperty(name = HystrixPropertiesManager.MAX_QUEUE_SIZE, value = "10"),
// 队列中最多有2个任务排队,超过该数目的任务则拒绝
@HystrixProperty(name = HystrixPropertiesManager.QUEUE_SIZE_REJECTION_THRESHOLD, value = "2"),
},
commandProperties = {
// 关掉熔断器,避免熔断器影响
@HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ENABLED, value = "false"),
// 超时判定时间设置为2000ms,即下方的1000ms延时不会引发超时
@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "2000"),
}
)
String queryUserNameById(int i) throws InterruptedException {
System.out.println("正在查询用户名,用户编号为" + i + "。该查询所在的线程名:" + Thread.currentThread().getName());
// 故意增加处理时延,以模拟堵塞
Thread.sleep(1000);
return "易哥" + i;
}
我们在原方法上增加@HystrixCommand注解。其中比较关键的是fallbackMethod参数,我们指向了该原方法对应的备用方法,即当原方法发生错误时要调用的方法。备用方法如下所示。
String fallBackForSayHi(int i) {
System.out.println("平台用户" + i);
return "平台用户" + i;
}
然后,原方法上还有threadPoolProperties注解和commandProperties注解,关于这些注解的详细介绍大家可以参考《高性能架构之道(第二版)》这本书,其中讲解的比较细致。本文的示例代码也来自这本书。
然后,我们使用下面的方法并发调用目标方法100次,每两次调用之间间隔100ms。
/**
* 演示Hystrix的限流功能
* 该方法会使用多线程方式调用目标方法100次。每次间隔时间100ms
*
* @return 100次调用的目标方法返回值的拼接结果
* @throws Exception 运行中发生的异常
*/
@RequestMapping("/limiter")
public String limiter() throws Exception {
StringBuilder stringBuilder = new StringBuilder();
// 创建100个调用limitService.sayHi方法的任务
List<Callable<String>> callableList = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
int finalI = i;
callableList.add(() -> limiterService.queryUserNameById(finalI));
}
// 创建100个FutureTask以接收全部任务的返回值
List<FutureTask<String>> futureTaskList = new ArrayList<>();
for (Callable<String> callable : callableList) {
futureTaskList.add(new FutureTask<>(callable));
}
// 以100ms为间隔,依次触发任务
for (FutureTask<String> futureTask : futureTaskList) {
Thread thread = new Thread(futureTask);
thread.start();
Thread.sleep(100);
}
// 接收任务的返回值
for (FutureTask<String> futureTask : futureTaskList) {
stringBuilder.append(futureTask.get()).append("\r\n");
}
return stringBuilder.toString();
}
则可以在控制台上看到如下的输出片段:
正在查询用户名,用户编号为2。该查询所在的线程名:hystrix-LimiterService-1
正在查询用户名,用户编号为1。该查询所在的线程名:hystrix-LimiterService-2
正在查询用户名,用户编号为3。该查询所在的线程名:hystrix-LimiterService-3
正在查询用户名,用户编号为4。该查询所在的线程名:hystrix-LimiterService-4
正在查询用户名,用户编号为5。该查询所在的线程名:hystrix-LimiterService-5
平台用户8
平台用户9
平台用户10
平台用户11
正在查询用户名,用户编号为7。该查询所在的线程名:hystrix-LimiterService-1
正在查询用户名,用户编号为6。该查询所在的线程名:hystrix-LimiterService-2
正在查询用户名,用户编号为12。该查询所在的线程名:hystrix-LimiterService-3
正在查询用户名,用户编号为13。该查询所在的线程名:hystrix-LimiterService-4
正在查询用户名,用户编号为14。该查询所在的线程名:hystrix-LimiterService-5
通过上述输出可以判断,用户编号1到5的调用直接进入线程执行,用户编号6、7的调用则进入了队列排队。而后续用户编号8到11的调用则直接进入了备用方法。与我们的参数设置一致。
然后,可以收集到如下的返回结果。
易哥1
易哥2
易哥3
易哥4
易哥5
易哥6
易哥7
平台用户8
平台用户9
平台用户10
平台用户11
易哥12
易哥13
易哥14
关于限流,大家常疑惑的点在于限流策略的选择。我们示例中使用的就是线程限流:目标方法的执行并发取决于线程池的配置。也可以采用信号量限流,那样目标方法的执行并发取决于信号量。关于两者的对比大家可以参考《高性能架构之道(第二版)》这本书。整篇文章的示例代码也来自这本书。
参数详解
以上实现的是最简单的隔离示例,也仅仅使用了最简单的参数,而其他参数都是用的默认值。但是这个作为示例是可以的,真正使用时,大家还是要对Hystrix的各个参数有详细的了解,至少也要知道大体的含义和默认值。否则,后续使用中,可能会遇到不少百思不得其解的地方。
总之,Hystrix要比这个强大的多。大家还是要好好学一下。
下面是我对@HystrixCommand参数的介绍,主要是摘抄参考了《高性能架构之道(第二版)》这本书。上面的示例也是参考的书里的。下面的参数我这里列的不是很全,如果要细致学习的话,十分建议大家去看下这本书。
HystrixCommand的主要参数如下:
- groupKey 组名。通过为命令指定一个组名,我们就可以将多个指令分到同一个组中。这样,我们可以使用配置文件为该组设置参数,而不用在每个指令中一一设定参数。通过组名,我们也可以对指标汇总分类。默认为该注解所处类的类名。
- commandKey 指令名。用来唯一指定一个指令。这样,我们可以使用配置文件为某个指令设置参数,也可以在指标展示时知道指标具体来自哪个指令。默认为该注解所在的方法的方法名。如果程序中注解所在方法的名字不是唯一的,那强烈建议通过commandKey指定一个唯一的名字,以防止多个指令间配置信息的互相干扰。
- fallbackMethod 备用方法名。用来指定该方法的备用方法,当该方法发生错误或超时时,将执行该备用方法。注意,备用方法必须和该方法有相同的入参类型和返回值类型。
- commandProperties 命令参数。在这里可以为该指令设置多个命令参数。具体命令参数的介绍详见书中的命令参数小节。
- ignoreExceptions 要忽略的异常。通常,当原方法执行抛出异常时,Hystrix会自动帮我们调用备用方法。通过该参数指定一些异常,就可以让Hystrix遇到这些异常时,将它们抛出而不触发备用方法。
- raiseHystrixExceptions 异常包装设置。该配置只有两个选项,要么为空,要么填写RUNTIME_EXCEPTION。如果填写RUNTIME_EXCEPTION,那原方法执行中抛出的所有未被忽略(参见ignoreExceptions配置)的异常会被包装进HystrixRuntimeException中抛出。因为有些场景下,外部调用方需要捕获的异常的类型是相同的,以便于处理。默认值为空,这种情况下原方法的异常会被正常抛出而不被包装。
- defaultFallback 默认备用方法。它比备用方法(参见fallbackMethod配置)的优先级更低,因此只在没有配置备用方法的时候生效。默认备用方法不允许有入参,且返回值类型必须要和原方法一致。
其中,执行器参数如下;
- execution.isolation.strategy 隔离策略,有以下两种策略,默认值为THREAD。
- THREAD:每次调用都会启用线程池中的独立线程,并发请求数目受线程池中线程数的限制。
- SEMAPHORE: 调用会在原线程上执行,并发请求数目受信号量计数的限制,
线程隔离策略支持超时后的快速失败,即当原方法延迟过大时会直接失败或调用备用方法返回,而不需要等原方法全部执行完毕。当原方法中存在网络调用等容易引发超时的操作时,最好使用THREAD隔离策略。不过,相比于SEMAPHORE策略,其开销略大,因为SEMAPHORE不需要创建额外的线程。
- execution.timeout.enabled 是否启用超时。如果设置为true,则会在原方法执行超时时启用备用方法。默认值为true。
- execution.isolation.semaphore.maxConcurrentRequests 在使用SEMAPHORE策略时,该参数用来设置允许请求的最大数值。超过该数值的调用将被拒绝。默认值为10。
- execution.isolation.thread.timeoutInMilliseconds 在启用超时的情况下,该参数用来设置超时时间的毫秒数。若原方法执行超过该时间,将执行备用方法。默认值1000。该参数对于THREAD和SEMAPHORE两种隔离策略均有效。THREAD策略因为使用独立的线程,可以实现在超时后快速失败或者调用备用方法返回。而SEMAPHORE策略因为使用的是调用方的线程,必须要等原方法执行完成,无法做到快速失败。
- execution.isolation.thread.interruptOnTimeout 在使用THREAD策略时,设置当原方法执行超时后,是中断原方法还是继续执行原方法中的后续内容。默认为true,即中断原方法。
- execution.isolation.thread.interruptOnCancel 在使用THREAD策略时,设置当原方法被取消后,是中断原方法还是继续执行原方法中的后续内容。默认为false,即继续执行原方法中的后续内容。
OK,以上就是Hystrix实现请求隔离的示例。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。