【参考】
- https://my.oschina.net/workit/blog/4832826
- https://filia-aleks.medium.com/microservice-performance-battle-spring-mvc-vs-webflux-80d39fd81bf0
- https://stackoverflow.com/questions/65120202/is-using-async-and-completablefuture-in-controller-can-increase-performance-of
- https://github.com/AndreasKl/spring-boot-mvc-completablefuture
【本文内容】
- Spring mvc利用
CompletableFuture
实现异步调用,探讨了以下问题:- 对比同步和异步单个请求的处理时间:这个异步是纯后端的异步,对前端是无感的,异步也并不会带来响应时间上的优化,原来该执行多久照样还是需要执行多久。
- 在Controller中使用CompletableFuture,一定会提高效率吗?通过ab压力测试显示,不一定。
- 同步请求 vs 异步请求所暂用的资源:异步处理暂用更多的资源。
- CompletableFuture的默认线程池存在的问题:原因是ForkJoinPool默认的并发数太小。
- 通过创建AOP,查看异步状态下的线程详细情况。
1. Spring mvc 异步请求 vs 同步请求
通常情况下Spring Controller返回请求数据的API长这样:
@GetMapping("/sync_result")
public String getResultSync() {
sleep(500L);
return "result is ready";
}
但Spring也支持异步的调用,如返回Callable
,或方法上加@Async
,或返回CompletableFuture
。这个功能实际上是Servlet 3.0之于异步请求的支持的实现。
例如返回CompletableFuture
:
@GetMapping("/async_result")
public CompletableFuture<String> getResultAsyc() {
return CompletableFuture.supplyAsync(() -> {
sleep(500L);
return "result is ready";
});
}
1.1 对比同步和异步单个请求的处理时间
这个异步是纯后端的异步,对前端是无感的,异步也并不会带来响应时间上的优化,原来该执行多久照样还是需要执行多久。
下图为F12的debug截图,可以看到对于单个请求,无论是同步的请求,还是异步的请求(使用CompletableFuture
或Callable
),都没有明显的差距:
1.2 在Controller中使用CompletableFuture,一定会提高效率吗?
默认情况下,Tomcat 的核心线程是 10,最大线程数是 200, 我们能及时回收线程,也就意味着我们能处理更多的请求,能够增加我们的吞吐量,这也是异步 Servlet 的主要作用。
那么使用CompletableFuture
一定会提高吞吐量吗?
这里我们用apache bench做实验,关于ab,请参考:https://www.jianshu.com/p/557df0992795
测试两个case:
- 同步请求数据,API为:
/sync_request
- 利用
CompletableFuture
异步请求数据,但使用自定义的线程池,线程数为500,API为:/async_request/custom_pool
@GetMapping("/async_result/custom_pool")
public CompletableFuture<String> getResultAsycWithCustomPool() {
return CompletableFuture.supplyAsync(() -> {
sleep(500L);
return "result is ready";
}, executorService);
}
我们做如下请求测试:
使用ab压力测试工具进行100/200/400/800的并发量测试,方法内延时时间分别为:10ms/100ms/500ms:
ab命令为:
ab -k -n 2000 -c 100 localhost:8080/sync_result
ab -k -n 4000 -c 200 localhost:8080/sync_result
ab -k -n 8000 -c 400 localhost:8080/sync_result
ab -k -n 16000 -c 800 localhost:8080/sync_result
ab结果,表中的时间为每个request所需的时间(毫秒):ab -k -n 2000 -c 100 localhost:8080/async_result/custom_pool
ab -k -n 4000 -c 200 localhost:8080/async_result/custom_pool
ab -k -n 8000 -c 400 localhost:8080/async_result/custom_pool
ab -k -n 16000 -c 800 localhost:8080/async_result/custom_pool
可以看到:
- 并发用户数不高的情况下(如100-200个用户)时,同步请求和异步请求所需的时间差不多。
- 而在高并发的情况下(如用户>=400)时,异步请求的优势较为明显。
1.3 同步请求 vs 异步请求所暂用的资源
以下是请求处理时间为500ms时(sleep time=500ms),不同用户并发数(100/200/400/800)下,并且请求为【同步】时的Jconsole:可以看到异步时占用的内存更多,线程数也更多,占用的cpu更高。
2. CompletableFuture的默认线程池存在的问题
在上述#1.2做压力测试的时,使用的是500个核心线程的线程池,那么如果我们不自定义线程池,而是使用CompletableFuture默认的线程池,效率会如何呢?
尝试请求处理时间为10ms时,使用默认线程池的表现:
ab -k -n 2000 -c 100 localhost:8080/async_result
结果每个request平均需要162.894ms!!!
而同步的状态下,#1.2的测试结果为16.368ms,使用500个核心线程池时异步状态下的结果为18.103ms。
为什么CompletableFuture
在默认线程池下这么慢?
默认情况下,CompletableFuture
使用的是ForkJoinPool.commonPool()
,而ForkJoinPool
默认支持的并发数为cpu-1:
System.out.println("Parallelism:" + ForkJoinPool.commonPool().getParallelism());
System.out.println("CPU: " + Runtime.getRuntime().availableProcessors());
在我的pc下,分别为7和8。即8核的情况下ForkJoinPool默认的可并行级别为7。
而7个并行级别显然是太低了,还不如同步。这也是为什么在使用CompletableFuture
作为Controller返回值时,推荐加上线程池的原因。
3. 异步状态下的线程详细情况
代码借鉴了开头参考列表中的第1个github repository。
首先我们自定义一个注解类:
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AsyncTimed {
}
为了更好的观察,为注解类加一个aop,需要引入aop包以及google的guava包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
@Pointcut
定义的包名为AsyncTimed所在的包名:
@Slf4j
@Aspect
@Component
public class AsyncTimedAspect {
@Pointcut("@annotation(com.async.aspect.AsyncTimed)")
public void asyncTimedAnnotationPointcut() {
}
@Around("asyncTimedAnnotationPointcut()")
public Object methodsAnnotatedWithAsyncTime(final ProceedingJoinPoint joinPoint) throws Throwable {
return proceed(joinPoint);
}
private Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable {
final Object result = joinPoint.proceed();
if (!isCompletableFuture(result)) {
return result;
}
final String description = joinPoint.toString();
final Stopwatch watch = Stopwatch.createStarted();
String initiatingThread = Thread.currentThread().getName();
return ((CompletableFuture<?>) result).thenApply(__ -> {
String executingThread = Thread.currentThread().getName();
watch.stop();
log.info("description: {}", description);
log.info("watch: {}", watch.toString());
log.info("initiating thread: {}", initiatingThread);
log.info("executing thread: {}", executingThread);
return __;
});
}
private boolean isCompletableFuture(Object result) {
return CompletableFuture.class.isAssignableFrom(result.getClass());
}
}
定义Service类:
@Service
public class MessageService {
ExecutorService executorService = Executors.newFixedThreadPool(500);
@AsyncTimed
public CompletableFuture<String> getMessageWithDefaultPool() {
return CompletableFuture.supplyAsync(() -> worker());
}
@AsyncTimed
public CompletableFuture<String> getMessageWithCustomPool() {
return CompletableFuture.supplyAsync(() -> worker(), executorService);
}
public String worker() {
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "result is ready";
}
}
Controller类:
@RestController
@RequestMapping("message")
public class MessageController {
@Autowired
private MessageService messageService;
@GetMapping("/custom-pool")
public CompletableFuture<String> getResultAsyc() {
log.info("Try to execute request with custom pool: {}", Thread.currentThread().getName());
return messageService.getMessageWithCustomPool();
}
@GetMapping("/default-pool")
public CompletableFuture<String> getResultAsycWithDefaultPool() {
log.info("Try to execute request with default pool: {}", Thread.currentThread().getName());
return messageService.getMessageWithDefaultPool();
}
}
我们尝试调用API: http://localhost:8080/message/custom-pool:
可以看到在Controller层用的是http-nio线程池。而在返回结果的时候使用了我们自已定义的线程池。
而使用默认线程池时,返回结果用的是ForkJoinPool的commonPool:尝试扩大ForkJoinPool的并发数
ForkJoinPool forkJoinPool = new ForkJoinPool(500);
这时候测试请求处理时间在500ms,并发数为800,结果为平均每个request花费时间为837.612ms。
而在#1.2中使用自定义核心线程数为500时,同样条件下的花费时间:836.737ms。
可以看到异步请求,瓶颈还是在线程池中的线程数。
4. 和WebFlux对比
参考:https://www.baeldung.com/spring-mvc-async-vs-webflux
加入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
写一个测试:
@GetMapping("/flux_result")
public Mono getResult() {
Mono mono = Mono.defer(() -> Mono.just("Result is ready"))
.delaySubscription(Duration.ofMillis(500));
return mono;
}
第一章#1.2的测试加上Webflux,可以看出在高延时、高并发的情况下,Webflux还是有优势的:后续对Webflux,500ms延时,1000个用户和2000个用户下做了测试,平均每个request耗时分别是535.087ms和551.254ms,还是比较稳定的。