因为工作上存在大量发起Http请求的场景,出于性能优化的考量,对Java async Http client进行了调研和简单的测试,分享总结于此。在这之前感觉Java没多少这种异步Http客户端,基本没怎么见过,异步Http更多的还是在服务端的处理,但是在进一步的调研过程中发现还是有不少能用的库。最先考虑的是JDK 9开始自带的Http客户端,后面又有看到Async Http Client、Apache Http Client 5、jetty client等,再加上假异步的Ok Http。

背景知识

IO模型

说到底这还是属于网络请求响应的范畴,离不开对IO模型的理解。大部分资料描述的IO模型描述都是server端的角度出发,不过client端也大差不差,从Blocking IO到Non Blocking IO再到Multiplexing IO。这里精简的描述下这几种模型以及它们的关联,具体不展开,可以参考别的资料,推荐看看这篇,也顺手从那而盗几张图片。

最基本的BIO图示如下:

以Http1.1协议来说,客户端线程发送请求后,就进入上图的阻塞读取流程,具体的方式可以简单表述为给socket注册一个回调函数,然后让出CPU进入阻塞休眠,socket收到响应数据后触发回调函数,回调函数唤醒阻塞的线程进行数据拷贝处理,应用程序继续执行逻辑。

再说NIO,网络数据的收和发本质上并没有关联,这两个方向本来就是异步的,在协议、操作系统的阻塞唤醒机制下才转换成了同步的形式。到应用代码层面来说,就是一个轮询的表现形式,图示如下:

对于非阻塞IO,在客户端场景下并没有直接使用的形式。NIO的核心观念是有就处理、没有就返回,这种模式的优点是可以通过少量线程来管理更多的连接,缺点是轮询会产生大量的上下文切换造成性能损耗。这种形式在并发场景下通常会被下面所描述的IO多路复用所替代。

高性能服务端、异步客户端基本上都是使用这种形式。操作系统在内核层面提供了一些工具方法比如select、poll、epoll,解决了NIO的轮询损耗问题,这样少量线程就能高效地处理大量连接,提高系统性能。在用户代码层面,我们通常需要在进行http调用时给到回调处理方法。

IO多路复用在线程模型上的典型实现就是Reactor线程模型,具体又可分为:

  • 单线程模式:连接、读写、业务处理全部在一个线程中处理
  • 单reactor多线程模式:reactor主线程负责连接、读、写,worker线程池负责业务处理
  • 主从reactor多线程模式:reactor主线程负责连接,reactor子线程池负责读写,worker线程池负责业务。如下图所示。

连接池

以Http1.1协议来说,每个TCP连接不能够并发地进行http请求交互,为了保证请求高效执行在并发时必然会使用多个TCP连接,而建立TCP连接相对而言是比较重的操作,所以Java Http Client都会有连接池的管理,这一点无论是普通客户端还是异步客户端都是如此。在并发量较高的情况下,对连接池的配置调整就不能忽视。

除此之外,Http客户端普遍有连接数限制的配置,包括单个Host和总量,这一点在并发和测试场景都需要注意。

测试说明

在理解IO模型后,可以看到异步客户端并不能加快单次请求响应的速度,甚至还有可能造成某些请求延迟增加,它最重要的价值是提高效率,期待它能够在较高并发的情况下减少cpu的损耗以及能够支持更高的吞吐量。
在少量并发或者无并发的情况下,异步客户端并不适用,异步处理也有损耗,可以说大部分场景并不需要使用异步Http客户端
异步性能的测试比较麻烦,本人也并不熟悉知晓测量异步客户端性能的标准方法,只能按照想象中相对合理的方式进行验证。测试环境对结果有较大影响,受限于时间和条件,这边只能是一个受限情况下的描述,比较片面,也不做进一步的分析和图表展示。

测试方式与结果

具体代码见:https://github.com/tonybro233/java-async-http-client-benchmark

库版本信息

  • Async Http Client:3.0.0.Beta1
  • Apache Http Client:5.2.1
  • JDK Http Client:OpenJDK 11.0.11 Corretto
  • Jetty Client:11.0.13
  • Ok Http:4.10.0

测试环境

  • 客户端:MacOS intel i5 4c8g
  • 服务端:Linux amd64 1c2g

测试方式

对异步Http客户端的测试本人目前没有看到什么特别好的办法,因为要针对异步进行测试,第一个排除的就是通过使用异步转同步的方式来进行考量,比如如下这种形式。

1
2
3
4
forint i = 0; i < 1000; i++) {
Future futureRes = client.asyncGet(...);
futureRes.get();
}

首先考虑使用JMH,但是对于JMH这套东西,无法直接拿来即用,它无法直接对这种异步调用进行考量,最后通过一些取巧的方法以及JMH提供的工具来做测试。

因为JMH无法拿来即用,期间也考虑手动编写测试代码,提供更详细的数据,但是这种形式肯定是没有JMH来的严谨。所以最后是两种形式都测,拿来一起考虑。

没有测试CPU的使用情况,只做了吞吐量相关的测试。部分客户端不支持Http2,服务端看起来也没有直接支持,都是使用http1.1来测。异步执行如果不进行限速,很容易就发生队列溢出、OOM、文件打开数量溢出等情况,所以必须要进行手动限制发请求的速度。这也说明异步环境下,如果没有背压机制很容易把系统打垮。

结果展示

代码测试时,3个线程一共发送30000个请求,每个线程请求间隔为1ms,可知发送速度为 3000 qps。

JMH测试时,会测试不同发送速度下的情况,间隔从 800us 到 300us,单线程执行请求发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 人工代码测试
ahc Analyse: time=9840ms ops=3048.78 minLatency=0ms maxLatency=1267ms avgLatency=230.918ms totalCnt=30000 success=28901 fail=1099
apache Analyse: time=11818ms ops=2538.5 minLatency=13ms maxLatency=336ms avgLatency=37.115ms totalCnt=30000 success=30000 fail=0
jdk Analyse: time=11364ms ops=2639.915 minLatency=14ms maxLatency=754ms avgLatency=77.865ms totalCnt=30000 success=30000 fail=0
jetty Analyse: time=17859ms ops=1679.825 minLatency=14ms maxLatency=285ms avgLatency=27.078ms totalCnt=30000 success=30000 fail=0
ok Analyse: time=12327ms ops=2433.682 minLatency=17ms maxLatency=1059ms avgLatency=444.362ms totalCnt=30000 success=30000 fail=0


// JMH 测试
Benchmark (ioThreads) (parkUs) (threads) (url) Mode Cnt Score Error Units
JMH_Async_Ahc.throughput:success 2 400 N/A <url> thrpt 5 2009.386 ± 64.015 ops
JMH_Async_Apache.throughput:success 4 400 N/A <url> thrpt 5 1211.992 ± 3109.381 ops
JMH_Async_Jdk.throughput:success N/A 400 200 <url> thrpt 5 457.336 ± 1193.343 ops
JMH_Async_Jetty.throughput:success 2 400 500 <url> thrpt 5 1128.708 ± 120.255 ops
JMH_Async_Ok.throughput:success N/A 400 200 <url> thrpt 5 2083.072 ± 2278.559 ops

JMH_Async_Ahc.throughput:fail 2 400 N/A <url> thrpt 5 ≈ 0 ops
JMH_Async_Apache.throughput:fail 4 400 N/A <url> thrpt 5 303.466 ± 937.938 ops
JMH_Async_Jdk.throughput:fail N/A 400 200 <url> thrpt 5 1695.590 ± 1379.885 ops
JMH_Async_Jetty.throughput:fail 2 400 500 <url> thrpt 5 0.440 ± 3.788 ops
JMH_Async_Ok.throughput:fail N/A 400 200 <url> thrpt 5 ≈ 0 ops

简单评价

  • Async Http Client:基于Netty,从结果来看性能十分的强,排到头名,可配置性也还不错,缺点是目前不支持http2,并且没有大组织背书,最近还更换了维护人
  • Apache Http Client:从version5开始支持异步http客户端,同步和异步是分开两种实现,apache的名头还是大,可配置性很强。
  • JDK Http Client:优点jdk原生,接口主要使用java.util.concurrent.CompletableFuturejava.util.concurrent.Flow,缺点是可配置性很差,也没法魔改,现在使用流行度不是很高。
  • Jetty Client:Jetty的API是我觉得最舒服的,整个异步化程度很高,背后的eclipse基金会也很强,但是不知为何在我的测试中性能很有问题。
  • Ok Http:OkHttp的异步就是放到线程池中去同步执行,所以我称之为假异步,4.0版本使用kotlin重写了一遍,但是对于使用者无感。作为优秀的Http客户端实现,流行程度比较高,也可以看到同步客户端性能很棒,再次说明非特殊场景并不需要使用异步http客户端。