本文介绍了线上业务中的一些异步调用实践经验,包含 IO 模型介绍、CompletableFuture 的基本使用、RPC 异步调用、异步 HTTP 客户端 Spring WebClient 的使用等。RPC 使用前文介绍的手写 RPC 框架,该框架支持异步调用。
本文要点:
- 为什么需要异步调用
- CompletableFuture 基本使用
- RPC 异步调用
- HTTP 异步调用
- 编排 CompletableFuture 提高吞吐量
为什么异步
BIO 模型
首先我们先回顾一下 BIO 模型:
当用户进程调用了recvfrom 这个系统调用,kernel 就开始了 IO 的第一个阶段:准备数据。对于 network io 来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候 kernel 就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当 kernel 一直等到数据准备好了,它就会将数据从 kernel 中拷贝到用户内存,然后 kernel 返回结果,用户进程才解除 block 的状态,重新运行起来。所以,Blocking IO 的特点就是在 IO 执行的两个阶段都被 block 了。
同步调用
在同步调用的场景下,依次请求多个接口,耗时长、性能差,接口响应时长 T > T1+T2+T3+……+Tn。
减少同步等待
一般这个时候为了减少同步等待时间,会使用线程池来同时处理多个任务,接口的响应时间就是 MAX(T1,T2,T3):
大概代码如下:
Future<String> future = executorService.submit(() -> {
Thread.sleep(2000);
return "hello world";
});
while (true) {
if (future.isDone()) {
System.out.println(future.get());
break;
}
}
复制代码
同步模型中使用线程池确实能实现异步调用的效果,也能压缩同步等待的时间,但是也有一些缺陷:
- CPU 资源大量浪费在阻塞等待上,导致 CPU 资源利用率低。
- 为了增加并发度,会引入更多额外的线程池,随着 CPU 调度线程数的增加,会导致更严重的资源争用,上下文切换占用 CPU 资源。
- 线程池中的线程都是阻塞的,硬件资源无法充分利用,系统吞吐量容易达到瓶颈。
NIO 模型
为了解决 BIO 中的缺陷,引入 NIO 模型:
当用户进程发出 read 操作时,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 error。从用户进程角度讲 ,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦 kernel 中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,用户进程其实是需要不断的主动询问 kernel 数据好了没有。
异步优化思路
我们知道了 NIO 的调用方式比 BIO 好,那我们怎么能在业务编码中使用到 NIO 呢?自己动手将 BIO 替换成 NIO 肯定不现实,已有组件支持 NIO 的可以直接使用,不支持的继续使用自定义线程池。
- 通过 RPC NIO 异步调用、 HTTP 异步调用的方式降低线程数,从而降低调度(上下文切换)开销。
- 没有原生支持 NIO 异步调用的继续使用线程池。
- 引入 CompletableFuture 对业务流程进行编排,降低依赖之间的阻塞。
简述CompletableFuture
CompletableFuture 是 java.util.concurrent 库在 java 8 中新增的主要工具,同传统的 Future 相比,其支持流式计算、函数式编程、完成通知、自定义异常处理等很多新的特性。
常用 API 举例
supplyAsync
CompletableFuture<String> future = CompletableFuture.supplyAsync(()->{
try{
Thread.sleep(1000L);
return "hello world";
} catch (Exception e){
return "failed";
}
});
System.out.println(future.join());
// output
hello world
复制代码
开启异步任务,到另一个线程执行。
complete
CompletableFuture<String> future1 = new CompletableFuture<>();
future.complete("hello world"); //异步线程执行
future.whenComplete((res, throwable) -> {
System.out.println(res);
});
System.out.println(future1.join());
CompletableFuture<String> future2 = new CompletableFuture<>();
future.completeExceptionally(new Throwable("failed")); //异步线程执行
System.out.println(future2.join());
// output
hello world
hello world
Exception in thread "main"
java.util.concurrent.CompletionException:
java.lang.Throwable: failed
复制代码
complete 正常完成该 CompletableFuture。
completeExceptionally 异常完成该 CompletableFuture。
thenApply
String original = "Message";
CompletableFuture<String> cf =
CompletableFuture.completedFuture(original).thenApply(String::toUpperCase);
System.out.println(cf.join());
// output
MESSAGE
复制代码
任务后置处理。
图示:
thenCombine
CompletableFuture<String> cf =
CompletableFuture.completedFuture("Message").thenApply(String::toUpperCase);
CompletableFuture<String> cf1 =
CompletableFuture.completedFuture("Message").then