Java 11 HttpClient Http2 流太多错误

Posted

技术标签:

【中文标题】Java 11 HttpClient Http2 流太多错误【英文标题】:Java 11 HttpClient Http2 Too many streams Error 【发布时间】:2019-07-21 21:47:01 【问题描述】:

我正在使用 Java 11 的 HttpClient 将请求发布到 HTTP2 服务器。 HttpClient 对象被创建为 Singleton Spring bean,如下所示。

@Bean
    public HttpClient getClient() 
                return HttpClient.newBuilder().version(Version.HTTP_2).executor(Executors.newFixedThreadPool(20)).followRedirects(Redirect.NORMAL)
                .connectTimeout(Duration.ofSeconds(20)).build();
    

I am using the sendAsync method to send the requests asynchronously.

When I try to hit the server continuously, I am receiving the error after certain time "java.io.IOException: too many concurrent streams". I used Fixed threadpool in the Client building to try to overcome this error, but it is still giving the same error.

The Exception stack is..

java.util.concurrent.CompletionException: java.io.IOException: too many concurrent streams
    at java.base/java.util.concurrent.CompletableFuture.encodeRelay(CompletableFuture.java:367) ~[?:?]
    at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1108) ~[?:?]
    at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2235) ~[?:?]
    at java.net.http/jdk.internal.net.http.MultiExchange.responseAsyncImpl(MultiExchange.java:345) ~[java.net.http:?]
    at java.net.http/jdk.internal.net.http.MultiExchange.lambda$responseAsync0$2(MultiExchange.java:250) ~[java.net.http:?]
    at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1072) ~[?:?]
    at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506) ~[?:?]
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1705) ~[?:?]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?]
    at java.base/java.lang.Thread.run(Thread.java:834) [?:?]
Caused by: java.io.IOException: too many concurrent streams
    at java.net.http/jdk.internal.net.http.Http2Connection.reserveStream(Http2Connection.java:440) ~[java.net.http:?]
    at java.net.http/jdk.internal.net.http.Http2ClientImpl.getConnectionFor(Http2ClientImpl.java:103) ~[java.net.http:?]
    at java.net.http/jdk.internal.net.http.ExchangeImpl.get(ExchangeImpl.java:88) ~[java.net.http:?]
    at java.net.http/jdk.internal.net.http.Exchange.establishExchange(Exchange.java:293) ~[java.net.http:?]
    at java.net.http/jdk.internal.net.http.Exchange.responseAsyncImpl0(Exchange.java:425) ~[java.net.http:?]
    at java.net.http/jdk.internal.net.http.Exchange.responseAsyncImpl(Exchange.java:330) ~[java.net.http:?]
    at java.net.http/jdk.internal.net.http.Exchange.responseAsync(Exchange.java:322) ~[java.net.http:?]
    at java.net.http/jdk.internal.net.http.MultiExchange.responseAsyncImpl(MultiExchange.java:304) ~[java.net.http:?]

有人可以帮我解决这个问题吗?

服务器是Tomcat9,它的最大并发流是默认的。

【问题讨论】:

【参考方案1】:

不幸的是,@sbordet 建议的Semaphore 方法对我不起作用。我试过这个:

var semaphore = semaphores.computeIfAbsent(getRequestKey(request), k -> new Semaphore(MAX_CONCURRENT_REQUESTS_NUMBER));

CompletableFuture.runAsync(semaphore::acquireUninterruptibly, WAITING_POOL)
                .thenComposeAsync(ignored -> httpClient.sendAsync(request, responseBodyHandler), ASYNC_POOL)
                .whenComplete((response, e) -> semaphore.release());

无法保证在将执行传递到下一个CompletableFuture(释放信号量)时释放连接流。对我来说,该方法在正常执行的情况下有效,但是如果有任何异常,似乎在调用semaphore.release() 后连接流可能会关闭。

最后,我最终使用了OkHttp。它处理这个问题(如果并发流的数量达到max_concurrent_streams,它只是等到一些流被释放)。它还处理GOAWAY 帧。在 Java HttpClient 的情况下,我必须实现重试逻辑来处理这个问题,因为如果服务器发送 GOAWAY 帧,它只会抛出 IOException

【讨论】:

你能解释一下这个例子中getRequestKey(request)是如何实现的吗? @Scot 我刚刚以与 Java 实现类似的方式实现了它。详情请参考jdk.internal.net.http.Http2Connection#keyString 这是否假设单个主机:端口组合将具有单个信号量(无论有多少连接)?我对单个主机:端口具有许多连接的情况感兴趣,并且我不确定是否有一种方法可以为每个唯一的连接实例创建一个窗口/信号量。也许客户端概念中内置了一种反应方式,但我还没有弄清楚。 @Scot 这个问题是关于 HTTP 2 多路复用和 Java 客户端的。我很久以前就研究过这个问题,所以我不能确定,但​​我记得如果主机支持 HTTP 2 多路复用,Java HTTP 客户端只使用一个连接。因此,每个 scheme:host:port 使用一个信号量与这种情况有关。【参考方案2】:

当我尝试连续访问服务器时

服务器有一个max_concurrent_streams 的设置,在 HTTP/2 连接的初始建立期间与客户端通信。

如果您使用sendAsync 盲目地“连续访问服务器”,则您不会等待先前的请求完成,最终您会超过max_concurrent_streams 的值并收到上述错误。

解决办法是并发发送多个小于max_concurrent_streams的请求;之后,您只会在前一个请求完成时发送一个新请求。 这可以使用Semaphore 或类似的东西在客户端上轻松实现。

【讨论】:

感谢您的澄清,我现在明白了这个问题。我会用一些障碍来限制请求。为了进一步理解,当我使用 Fixedthreadpool 而不是默认的 Cachedthreadpool 时,它限制了连接数,因为每个连接都限制为在服务器端或客户端配置的最大流数。我说的对吗? 线程池不绑定连接数。使用 HTTP/2,可能只有 一个 连接到服务器。您可能会限制并发 requests 的数量,但我不会依赖它,因为它高度依赖于实现。您升级 Java 版本,固定线程池可能不会像以前的 Java 版本那样限制并发请求。我会很明确地使用Semaphore,就像我在答案中解释的那样,这样它就可以与任何Java版本一起使用。

以上是关于Java 11 HttpClient Http2 流太多错误的主要内容,如果未能解决你的问题,请参考以下文章

如何使 .net HttpClient 使用 http 2.0?

java 11 HttpClient 导致无休止的 SSL 循环

JAVA - HttpClient(JDK11)

Java11 中找不到 jdk.incubator.httpclient 模块

在 Java 11 及更高版本中使用 HttpClient 时如何跟踪 HTTP 303 状态代码?

如何在 Xamarin.Forms 上调用 HTTP2