如何在Java中防止SocketInputStream.socketRead0挂起?

70

使用不同的Java库执行数百万个HTTP请求,会导致线程挂起:

java.net.SocketInputStream.socketRead0()

这是一个本地函数。

我尝试设置Apache Http Client和RequestConfig以在可能的情况下对所有内容进行超时,但仍然会出现socketRead0上有(可能是无限的)挂起。如何摆脱它们?

挂起比率约为每10000个请求(到10000个不同的主机)1个,它可能会永远持续下去(我确认线程挂起后仍然有效,持续了10个小时)。

JDK 1.8在Windows 7上。

我的HttpClient工厂:

SocketConfig socketConfig = SocketConfig.custom()
            .setSoKeepAlive(false)
            .setSoLinger(1)
            .setSoReuseAddress(true)
            .setSoTimeout(5000)
            .setTcpNoDelay(true).build();

    HttpClientBuilder builder = HttpClientBuilder.create();
    builder.disableAutomaticRetries();
    builder.disableContentCompression();
    builder.disableCookieManagement();
    builder.disableRedirectHandling();
    builder.setConnectionReuseStrategy(new NoConnectionReuseStrategy());
    builder.setDefaultSocketConfig(socketConfig);

    return HttpClientBuilder.create().build();

我的RequestConfig工厂:

    HttpGet request = new HttpGet(url);

    RequestConfig config = RequestConfig.custom()
            .setCircularRedirectsAllowed(false)
            .setConnectionRequestTimeout(8000)
            .setConnectTimeout(4000)
            .setMaxRedirects(1)
            .setRedirectsEnabled(true)
            .setSocketTimeout(5000)
            .setStaleConnectionCheckEnabled(true).build();
    request.setConfig(config);

    return new HttpGet(url);

OpenJDK socketRead0 source

注意:实际上我有一个“技巧”:我可以在其他Thread中安排.getConnectionManager().shutdown(),并在请求正常完成时取消Future。但是,这已经被弃用,并且它会关闭整个HttpClient,而不仅仅是单个请求。


HttpClientBuilderوœ‰builder.disableRedirectHandling(),而RequestConfigوœ‰.setRedirectsEnabled(true),è؟™وک¯و­£ç،®çڑ„هگ—ï¼ں - Anton Danilov
是的,但我认为这与此无关。Hung 在 socketRead0() 中,并且与除 Apache Http 之外的其他客户端有关。 - Piotr Müller
4
你不觉得这只是OpenJDK的一个bug吗?例如:https://bugs.openjdk.java.net/browse/JDK-8049846 - qwwdfsad
3
截至2017年2月,仍然没有Windows版本的卡顿修复方案。相比之下,自JDK-8075484(2016年9月的JDK 9)和JDK-8172578(2017年1月的JDK 8u152)以来,Oracle似乎已经在Linux、Solaris、MacOSX和AIX中修复了这个问题。最接近的Windows错误似乎是JDK-8000679。 - buzz3791
1
Stuart Marks决定在2017年5月关闭JDK-8000679(此错误的Windows版本),并遗憾地评论道:“这可能是Java网络代码或操作系统网络层中的错误。将其标记为无法重现。” - buzz3791
显示剩余7条评论
9个回答

21
尽管这个问题提到了Windows,但我在Linux上也遇到了同样的问题。看起来JVM实现阻塞套接字超时的方式存在缺陷。 总之,对于阻塞套接字的超时,Linux 上使用 poll(Windows 上使用 select)来确定在调用 recv 之前是否有可用数据。然而,在 Linux 上,这两种方法都可能出现虚假的数据可用指示,导致 recv 无限期阻塞。
从 poll(2) 的手册 BUGS 部分:

请参见 select(2) 的 BUGS 部分中关于虚假准备通知的讨论。

从 select(2) 的手册 BUGS 部分:

在 Linux 上,select() 可能会报告一个套接字文件描述符“准备好读取”,但是随后的读取仍然会阻塞。例如,当数据到达但在检查后具有错误的校验和并被丢弃时,就可能发生这种情况。在其他情况下,文件描述符也可能被虚假地报告为已准备好。因此,在不应该阻塞的套接字上使用 O_NONBLOCK 更安全。

Apache HTTP Client的代码有点难以理解,但是它似乎只为HTTP keep-alive连接设置了连接过期时间(而您已经禁用了该功能),并且除非服务器另有规定,否则该时间是无限的。因此,正如oleg所指出的那样,连接淘汰策略方法在您的情况下不起作用,并且通常也不能依靠。


1
看起来这个 bug 在九月份已经被修复了。你是否停止遇到这个问题了? - Arya

17

正如Clint所说,您应该考虑使用非阻塞HTTP客户端或(鉴于您正在使用Apache Httpclient)实现多线程请求执行以防止主应用程序线程可能挂起的情况(这不解决问题,但比重启应用程序要好,因为它没有被冻结)。无论如何,您设置了setStaleConnectionCheckEnabled属性,但是陈旧的连接检查并不可靠,根据Apache Httpclient教程:

经典阻塞I / O模型的主要缺点之一是,网络套接字只能在阻塞的I / O操作中响应I / O事件。当连接释放回管理器时,它可以保持活动状态,但无法监视套接字的状态并对任何I / O事件做出反应。如果服务器端关闭连接,则客户端端连接无法检测连接状态的更改(并通过关闭其端上的套接字做出适当反应)。

HttpClient尝试通过测试连接是否“过时”来减轻问题,即在使用连接执行HTTP请求之前,该连接不再有效,因为它已在服务器端关闭。陈旧的连接检查并不可靠,并为每个请求执行添加10到30毫秒的开销。

Apache HttpComponents团队建议实现连接剔除策略

不涉及空闲连接的每个套接字模型的唯一可行解决方案是使用专用监视器线程,用于剔除由于长时间不活动而被视为过期的连接。监视器线程可以定期调用ClientConnectionManager#closeExpiredConnections()方法以关闭所有过期的连接并从池中剔除已关闭的连接。它还可以选择调用ClientConnectionManager#closeIdleConnections()方法以关闭所有已闲置一段时间的连接。

请查看连接剔除策略部分的示例代码,并尝试在您的应用程序中实现它,以及多线程请求执行,我认为两种机制的实现将防止您不希望发生的挂起。


感谢您详细的回答。关于驱逐策略的链接正是我所需要的。我已经在整个连接管理器上做了类似的事情,现在我知道如何在实际的单独连接上进行操作了。谢谢。但最终我可能会转向非阻塞客户端。 - Piotr Müller
2
驱逐策略旨在清除陈旧的“空闲”连接。它对从池中租用并用于执行请求(并在读取操作中被阻止)的连接没有任何影响。 - ok2c
@oleg 如果是这样,我已经取消了答案的接受。也许会有新的东西出现。 - Piotr Müller
如果您想找出发生了什么,请按照我的回答要求获取挂起会话的线路日志。 - ok2c

5
我有50多台机器,每天每台机器约产生20万个请求。它们正在运行Amazon Linux AMI 2017.03。我之前使用的是jdk1.8.0_102,现在使用的是jdk1.8.0_131。我同时使用apacheHttpClient和OKHttp作为爬取库。
每台机器都在运行50个线程,有时候这些线程会丢失。通过Youkit Java分析工具进行分析后,我得到了以下结果:
ScraperThread42 State: RUNNABLE CPU usage on sample: 0ms
java.net.SocketInputStream.socketRead0(FileDescriptor, byte[], int, int, int) SocketInputStream.java (native)
java.net.SocketInputStream.socketRead(FileDescriptor, byte[], int, int, int) SocketInputStream.java:116
java.net.SocketInputStream.read(byte[], int, int, int) SocketInputStream.java:171
java.net.SocketInputStream.read(byte[], int, int) SocketInputStream.java:141
okio.Okio$2.read(Buffer, long) Okio.java:139
okio.AsyncTimeout$2.read(Buffer, long) AsyncTimeout.java:211
okio.RealBufferedSource.indexOf(byte, long) RealBufferedSource.java:306
okio.RealBufferedSource.indexOf(byte) RealBufferedSource.java:300
okio.RealBufferedSource.readUtf8LineStrict() RealBufferedSource.java:196
okhttp3.internal.http1.Http1Codec.readResponse() Http1Codec.java:191
okhttp3.internal.connection.RealConnection.createTunnel(int, int, Request, HttpUrl) RealConnection.java:303
okhttp3.internal.connection.RealConnection.buildTunneledConnection(int, int, int, ConnectionSpecSelector) RealConnection.java:156
okhttp3.internal.connection.RealConnection.connect(int, int, int, List, boolean) RealConnection.java:112
okhttp3.internal.connection.StreamAllocation.findConnection(int, int, int, boolean) StreamAllocation.java:193
okhttp3.internal.connection.StreamAllocation.findHealthyConnection(int, int, int, boolean, boolean) StreamAllocation.java:129
okhttp3.internal.connection.StreamAllocation.newStream(OkHttpClient, boolean) StreamAllocation.java:98
okhttp3.internal.connection.ConnectInterceptor.intercept(Interceptor$Chain) ConnectInterceptor.java:42
okhttp3.internal.http.RealInterceptorChain.proceed(Request, StreamAllocation, HttpCodec, Connection) RealInterceptorChain.java:92
okhttp3.internal.http.RealInterceptorChain.proceed(Request) RealInterceptorChain.java:67
okhttp3.internal.http.BridgeInterceptor.intercept(Interceptor$Chain) BridgeInterceptor.java:93
okhttp3.internal.http.RealInterceptorChain.proceed(Request, StreamAllocation, HttpCodec, Connection) RealInterceptorChain.java:92
okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(Interceptor$Chain) RetryAndFollowUpInterceptor.java:124
okhttp3.internal.http.RealInterceptorChain.proceed(Request, StreamAllocation, HttpCodec, Connection) RealInterceptorChain.java:92
okhttp3.internal.http.RealInterceptorChain.proceed(Request) RealInterceptorChain.java:67
okhttp3.RealCall.getResponseWithInterceptorChain() RealCall.java:198
okhttp3.RealCall.execute() RealCall.java:83

我发现他们已经为此提供了修复。

https://bugs.openjdk.java.net/browse/JDK-8172578

我已经在我们的一台机器上安装了JDK 8u152(早期访问版本),现在我正在等待看到一些好的结果。


感谢更新,请通知结果。 - Piotr Müller
没成功。它在过夜时卡住了。我会尝试联系 Oracle 有关这个 bug 的问题。它被标记为已解决。另外,我找到了一个解决方法(从另一个线程中断连接),因为我厌倦了每天重新启动机器。 - Stefan Matei
1
@Stefan 感谢提供信息。如果您在Windows JDK上收到错误报告,请在此stackoverflow问题中发布错误号。 - buzz3791
仍然在 Windows 上的 Java 8 U181 上发生。 - chiperortiz

5

您应该考虑使用像GrizzlyNetty这样的非阻塞式HTTP客户端,它们不会有阻塞操作来占用线程。


好主意,我可能会用那个完成,但我只是想澄清如何通过阻塞Http来实现这一点(以调用socketRead0,但不挂起)。所以接受其他响应。谢谢。我只想补充一点,Apache Http Client也有异步非阻塞版本。 - Piotr Müller

3

我在使用Apache Common Http Client时遇到了同样的问题。

有一个非常简单的解决方法(不需要关闭连接管理器):

要重现此问题,需要在新线程中执行来自问题的请求,并注意细节:

  • 在单独的线程中运行请求,在不同的线程中关闭请求并释放其连接,中断挂起的线程
  • 不要在finally块中运行EntityUtils.consumeQuietly(response.getEntity())(因为它会在“死”连接上挂起)

首先添加接口

interface RequestDisposer {
    void dispose();
}

在新线程中执行HTTP请求

final AtomicReference<RequestDisposer> requestDisposer = new AtomicReference<>(null);  

final Thread thread = new Thread(() -> {
    final HttpGet request = new HttpGet("http://my.url");
    final RequestDisposer disposer = () -> {
        request.abort();
        request.releaseConnection();
    };
    requestDiposer.set(disposer);

    try (final CloseableHttpResponse response = httpClient.execute(request))) {
        ...
    } finally {
      disposer.dispose();
    } 
};)
thread.start()

在主线程中调用dispose()以关闭挂起的连接。

requestDisposer.get().dispose(); // better check if it's not null first
thread.interrupt();
thread.join();

我做了这个,问题解决了。

我的堆栈跟踪看起来像这样:

java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:139)
at org.apache.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:155)
at org.apache.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:284)
at org.apache.http.impl.io.ChunkedInputStream.getChunkSize(ChunkedInputStream.java:253)
at org.apache.http.impl.io.ChunkedInputStream.nextChunk(ChunkedInputStream.java:227)
at org.apache.http.impl.io.ChunkedInputStream.read(ChunkedInputStream.java:186)
at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:137)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)

对于可能感兴趣的人,以下是关于 IT 技术的相关翻译内容。该问题很容易被重现,即可在不终止请求和释放连接的情况下中断线程 (比例约为1/100)。此问题出现在 Windows 10 版本 10.0 平台上,并使用了 JDK8.151-x64。


3
对于Apache HTTP Client(阻塞式),我发现最好的解决方案是使用getConnectionManager()并关闭它。
因此,在高可靠性的解决方案中,我只需在另一个线程中安排关闭,并在请求未完成的情况下从其他线程关闭。

3

鉴于目前没有其他人回复,这是我的看法。

你的超时设置看起来非常好。某些请求看起来在java.net.SocketInputStream#socketRead0()调用中不断被阻塞,很可能是由于服务器行为不当和本地配置的组合原因。套接字超时定义了两个连续 I/O 读取操作(换句话说,两个连续传入数据包)之间的不活动最长时间。你的套接字超时设置是5000毫秒。只要对端每4999毫秒发送一个分块编码消息,该请求就永远不会超时,并且最终将大部分时间都被阻塞在java.net.SocketInputStream#socketRead0()中。通过打开线路日志,可以找出是否存在这种情况。


1
Socket读取超时定义了进入recv()方法和数据到达之间的最大时间间隔。它与读取操作之间或数据包之间的时间间隔无关。 - user207421
正确。不会改变您答案中的错误。计时器在输入recv()或read()时开始计时,并在到期、数据到达、EOS或发生错误时停止计时。与两次读取或两个数据包之间的时间间隔无关。您上面所写的根本没有意义。它暗示了您不能在第一次读取时超时,例如。而且,两次读取之间的时间并不是首先两个数据包之间的时间。 - user207421
1
你“神奇”的回答存在问题,因为它是错误的。你可以通过实验轻松地确定这一点,而不仅仅是争论,并发布更多关于两次读取或两个数据包之间间隔的毫无根据的胡言乱语,或者其他你试图扭曲的东西。我建议你在进一步讨论之前先尝试一下。 - user207421
这是你的答案:这是你的断言:它已经受到了质疑。你需要证明它。或者更确切地说,你需要证明其中一个,因为你已经声称了两个相互矛盾的立场。当你有证据或可接受的来源来支持你决定维持哪一个时,请告诉我们。但是它们不能同时正确。我只是暗示我并不是在猜测这件事。 - user207421
2
当然,@oleg是正确的:如果您连接的服务器非常慢,以每4.9秒一个字节的速度发送1TB文件,则会在socketRead0()上花费很多时间被阻塞,而不会因超时而被踢出。一旦您有很多线程处于这种情况下,您已经耗尽了线程池,系统就会“崩溃”。这是HTTP / REST作为“微服务”之间通信的糟糕解决方案之一的原因。 - stolsvik
显示剩余4条评论

3
我觉得所有这些答案都过于具体了。我们必须注意到,这可能是一个真正的JVM bug。应该能够获得文件描述符并关闭它。所有这些超时谈论都太高级了。您不希望超时导致连接失败,您想要的是一种硬停止这个卡住的线程并停止或中断它的能力。
JVM应该实现SocketInputStream.socketRead函数的方式是设置一些内部默认超时时间,甚至可以低至1秒钟。然后当超时到来时,立即循环回到socketRead0。在此同时,Thread.interrupt和Thread.stop命令可以生效。
当然,更好的方法是根本不进行任何阻塞等待,而是使用具有文件描述符列表的select(2)系统调用,当其中任何一个有可用数据时,让它执行读取操作。
只要看看整个互联网上所有在java.net.SocketInputStream#socketRead0中卡住的线程的人们遇到的问题,这绝对是关于java.net.SocketInputStream最热门的话题!
因此,在此错误未修复的情况下,我想知道我能想出的最恶劣的把戏来打破这种情况。比如连接调试器界面以获取socketRead调用的堆栈帧并抓取FileDescriptor,然后打入该fd的int编号并在该fd上进行本地close(2)调用。
我们有机会做到这一点吗?(不要告诉我“这不是好的做法”)——如果可以,让我们来做吧!

2
我今天遇到了同样的问题。根据@Sergei Voitovich的建议,我尝试使用Apache Http客户端来解决这个问题。
由于我正在使用Java 8,所以更容易设置超时时间以中止连接。
以下是实现草案:
private HttpResponse executeRequest(Request request){
    InterruptibleRequestExecution requestExecution = new InterruptibleRequestExecution(request, executor);
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    try {
        return executorService.submit(requestExecution).get(<your timeout in milliseconds>, TimeUnit.MILLISECONDS);
    } catch (TimeoutException | ExecutionException e) {
        // Your request timed out, you can throw an exception here if you want
        throw new UsefulExceptionForYourApplication(e);
    } catch (InterruptedException e) {
        // Always remember to call interrupt after catching InterruptedException
        Thread.currentThread().interrupt();
        throw new UsefulExceptionForYourApplication(e);
    } finally {
        // This method forces to stop the Thread Pool (with single thread) created by Executors.newSingleThreadExecutor() and makes the pending request to abort inside the thread. So if the request is hanging in socketRead0 it will stop and also the thread will be terminated
        forceStopIdleThreadsAndRequests(requestExecution, executorService);
    }
}

private void forceStopIdleThreadsAndRequests(InterruptibleRequestExecution execution,
                                             ExecutorService executorService) {
    execution.abortRequest();
    executorService.shutdownNow();
}

以上代码将创建一个新的线程来使用org.apache.http.client.fluent.Executor执行请求。超时时间可以很容易地进行配置。

线程的执行定义在InterruptibleRequestExecution中,您可以在下面看到。

private static class InterruptibleRequestExecution implements Callable<HttpResponse> {
    private final Request request;
    private final Executor executor;
    private final RequestDisposer disposer;

    public InterruptibleRequestExecution(Request request, Executor executor) {
        this.request = request;
        this.executor = executor;
        this.disposer = request::abort;
    }

    @Override
    public HttpResponse call() {
        try {
            return executor.execute(request).returnResponse();
        } catch (IOException e) {
            throw new UsefulExceptionForYourApplication(e);
        } finally {
            disposer.dispose();
        }
    }

    public void abortRequest() {
        disposer.dispose();
    }

    @FunctionalInterface
    interface RequestDisposer {
        void dispose();
    }
}

结果非常好。我们曾经遇到过一些连接在socketRead0中挂起了7个小时的情况!现在,它从不超过定义的超时时间,并且在每天处理数百万请求的生产环境中工作良好,没有任何问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接