性能:Apache HttpAsyncClient与多线程URLConnection

Err*_*ric 5 java performance asynchronous http apache-httpasyncclient

我正在尝试选择最好的方法来并行进行大量的http请求。以下是我到目前为止拥有的两种方法:

  1. 使用Apache HttpAsyncClient和CompletableFutures:

    try (CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom()
    .setMaxConnPerRoute(2000).setMaxConnTotal(2000)
    .setUserAgent("Mozilla/4.0")
    .build()) {
    httpclient.start();
    HttpGet request = new HttpGet("http://bing.com/");
    long start = System.currentTimeMillis();
    CompletableFuture.allOf(
            Stream.generate(()->request).limit(1000).map(req -> {
                CompletableFuture<Void> future = new CompletableFuture<>();
                httpclient.execute(req, new FutureCallback<HttpResponse>() {
                    @Override
                    public void completed(final HttpResponse response) {
                        System.out.println("Completed with: " + response.getStatusLine().getStatusCode())
                        future.complete(null);
                    }
                    ...
                });
                System.out.println("Started request");
                return future;
    }).toArray(CompletableFuture[]::new)).get();
    
    Run Code Online (Sandbox Code Playgroud)
  2. 常规的每请求线程数方法:

    long start1 = System.currentTimeMillis();
    URL url = new URL("http://bing.com/");
    ExecutorService executor = Executors.newCachedThreadPool();
    
    Stream.generate(()->url).limit(1000).forEach(requestUrl ->{
        executor.submit(()->{
            try {
                URLConnection conn = requestUrl.openConnection();
                System.out.println("Completed with: " + conn.getResponseCode());
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        System.out.println("Started request");
    });
    
    Run Code Online (Sandbox Code Playgroud)

在多次运行中,我注意到传统方法的完成速度几乎是异步/未来方法的两倍

尽管我期望专用线程能够运行得更快,但是差异应该是这么明显还是异步实现有问题?如果没有,那么正确的方法是什么?

Has*_*ash 5

这个问题取决于很多因素:

  • 硬件
  • 操作系统(及其配置)
  • JVM 实现
  • 网络设备
  • 服务器行为

第一个问题 - 差异应该如此显着吗?

取决于负载、池大小和网络,但在每个方向上它可能比观察到的因子 2 多得多(有利于异步或线程解决方案)。根据您后来的评论,差异更多是因为不当行为,但为了论证起见,我将解释可能的情况。

专用线程可能是一个相当大的负担。(中断处理和线程调度由操作系统完成,以防您使用 Oracle [HotSpot] JVM,因为这些任务是委托的。)如果线程过多,操作系统/系统可能会变得无响应,从而减慢您的批处理速度(或其他任务)。有很多关于线程管理的管理任务,这就是为什么线程(和连接)池是一个东西。虽然一个好的操作系统应该能够处理几千个并发线程,但总是有可能发生一些限制或(内核)事件。

这是池化和异步行为派上用场的地方。例如,有 10 个物理线程池完成所有工作。如果某些东西被阻塞(在这种情况下等待服务器响应),它会进入“阻塞”状态(参见图片)并且以下任务让物理线程做一些工作。当一个线程被通知(数据到达)时,它变成“可运行的”(从这一点池机制能够拿起它[这可能是操作系统或 JVM 实现的解决方案])。为了进一步阅读线程状态,我推荐W3Rescue。为了更好地理解线程池,我推荐这篇baeldung 文章

线程转换

第二个问题 - 异步实现有问题吗?如果不是,那么在这里进行的正确方法是什么?

实现是可以的,没有问题。行为与线程方式不同。这些情况下的主要问题主要是 SLA-s(服务级别协议)是什么。如果您是该服务的唯一“客户”,那么基本上您必须在延迟或吞吐量之间做出决定,但该决定只会影响您。大多数情况下并非如此,因此我建议使用某种受支持的池化您正在使用的库。

第三个问题 - 但是我刚刚注意到,当您将响应流作为字符串读取时,所花费的时间大致相同。我想知道这是为什么?

在这两种情况下,消息最有可能完全到达(可能响应不是流,只是几个 http 包),但是如果您只读取不需要响应本身被解析并加载到 CPU 寄存器的标头,从而减少读取接收到的实际数据的延迟。我认为这是一个很酷的延迟表示(): 到达次数

这是一个很长的答案,所以 TL.DR.:缩放是一个非常核心的话题,它取决于很多事情:

  • 硬件:物理内核数量、多线程容量、内存速度、网络接口
  • 操作系统(及其配置):线程管理、中断处理
  • JVM 实现:线程管理(内部或外包给 OS),更不用说 GC 和 JIT 配置
  • 网络设备:一些限制来自给定 IP 的并发连接,一些池非HTTPS连接并充当代理
  • 服务器行为:池工作人员或按请求工作人员等

在您的情况下,服务器很可能是瓶颈,因为两种方法在更正的情况下给出了相同的结果 ( HttpResponse::getStatusLine().getStatusCode() and HttpURLConnection::getResponseCode())。为了给出正确的答案,您应该使用一些工具(如JMeterLoadRunner等)来衡量您的服务器性能,然后相应地调整您的解决方案。这篇文章更多是关于数据库连接池,但逻辑也适用于这里。