目标
在一个线程中大量调用http 同步请求,进行异步调用,不希望一个http 请求消耗一个线程的做法。在常用的的OK http,Apache http 的异步方案是采用线程池异步,一个请求对应一个线程, 并发请求量大时候,线程耗尽,只能要等待,性能不好,需要找一个可以固定线程池的真正异步非阻塞的client。
解决
AsyncHttpClient 基于netty 框架开发,做到了类似go 的协程的异步http 能力,固定CPU 核数的线程池,可以支持大量http 异步请求,在大量并发http 请求下,相比ok http,Apache http 性能要好很多
介绍
基于netty框架,使用操作系统native的高性能异步库 epoll,Kqueue进行网络IO异步管理,可以做到用少量线程,同时处理大量网络请求。该client实现了Http和 websocket协议(后续可以基于该库底层实现应用消息协议的的client)
pom.xml引入依赖
<dependency>
<groupId>org.asynchttpclient</groupId>
<artifactId>async-http-client</artifactId>
<version>2.12.3</version>
</dependency>
测试
使用时,需要引入依赖
import org.asynchttpclient.*;
import static org.asynchttpclient.Dsl.*;
1:批量请求方法
public static List<ListenableFuture<Response>> AsyncBatchHttpClient(List<RequestBuilder> requests) {
List<ListenableFuture<Response>> respons = new ArrayList();
for(RequestBuilder request:requests)
{
ListenableFuture<Response> whenRes = asyncHttpClient
.executeRequest(request.addHeader("Content-Type","application/json")
.setReadTimeout(10000));
respons.add(whenRes);
}
return respons;
}
请求返回的事一个ListenableFuture<Response>,可以进行很方便的future 异步处理,如果要批量等待全部完成,可以参考以下处理:
public static List<String> waitAllDone(List<ListenableFuture<Response>> respons) {
List<String> results = new ArrayList<>();
Iterator<ListenableFuture<Response>> it = respons.iterator();
try {
while(it.hasNext()) {
ListenableFuture<Response> respon = it.next();
try {
Response result = respon.get();
results.add(result.getResponseBody());
} catch (ExecutionException e) {
LOGGER.info("result get error:"+e.getMessage() );
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return results;
}
准备2种接口:一个是快速返回的空接口,一个是允许随机sleep 模拟实际请求的接口,在一个单独的spring boot工程,单独服务启动
/**
* 随机休眠,模拟实际请求 * @return
*/
@RequestMapping(method = RequestMethod.GET, value = "/app/testGet")
public String appRandomTestGet(@RequestParam(value = "max", required = false) Integer max) {
try {
Thread.sleep(RandomUtils.nextLong(0,max));
} catch (InterruptedException e) {
e.printStackTrace();
}
return "appRandomTestGet max:"+max+" ok";
}
/**
* 快速返回接口 * @return
*/
@RequestMapping(method = RequestMethod.GET, value = "/app/fastGet")
public String appFastGet() {
return "appFastGet ok";
}
在写请求服务,这里用上面封装的批量请求方法,先测试fastGet
@Scheduled(cron = "0/4 * * * * ?")
public void testhttp(){
List<RequestBuilder> requests = new ArrayList<>();
int size =5000;
//同时请求快速http 请求
for(int i=0;i<size;i++) {
requests.add(get("http://10.215.40.137:7071/app/fastGet"));
}
httpdemo(requests,"fastGet-"+size); requests = new ArrayList<>();
size =1000;
//同时请求快速http 请求
for(int i=0;i<size;i++) {
requests.add(get("http://10.215.40.137:7071/app/fastGet"));
}
httpdemo(requests,"fastGet-"+size); }
public void httpdemo(List<RequestBuilder> requests,String type ) {
long start = System.currentTimeMillis();
List<ListenableFuture<Response>> respons = AsyncBatchHttpClient(requests);
waitAllDone(respons);
LOGGER.info(type +" all done use:"+(System.currentTimeMillis()-start));
}
单线程同时请求1000以上
如日志所示,5000个请求在180~350ms,大部分在220ms左右。请求1000个在25~50ms,大部分在30ms左右,取决于被压服务最大返回时间,这里被压服务只有一个进 程,所以会有点慢,符合预期,如果是okHttp 或者Apache http 会慢很多倍
查看线程数量是否随着请求量增大而增大: asyncHttp相关的线程 保持16个不变,测试机器CPU8核,猜测是处理请求和结果的线程池为2倍CPU核数
如下线程监控截图
使用多线程模拟真实场景,依旧如此,性能不错