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

Posted

技术标签:

【中文标题】如何防止 Java 中的 SocketInputStream.socketRead0 挂起?【英文标题】:How to prevent hangs on SocketInputStream.socketRead0 in Java? 【发布时间】:2015-05-01 08:19:01 【问题描述】:

使用不同的 Java 库执行数百万个 HTTP 请求让我有线程挂起:

java.net.SocketInputStream.socketRead0()

这是native函数。

我尝试设置 Apche Http Client 和 RequestConfig 以使(我希望)一切可能的超时,但 我(可能无限)挂在 socketRead0 上。如何摆脱它们?

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

Windows 7 上的 JDK 1.8。

我的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,不仅那个单一的请求。

【问题讨论】:

好吧,他们会一直阻塞,直到数据到达或超时。你的意思是这些线程被永久阻塞而不超时? 是的,我的意思是它永远挂起(我检查了 6 小时场景) 你不认为这只是 OpenJDK 的错误吗?例如:bugs.openjdk.java.net/browse/JDK-8049846 截至 2017 年 2 月,仍然没有解决 Windows 挂起问题的迹象。相比之下,使用 JDK-8075484(2016 年 9 月的 JDK 9)和 JDK-8172578(2017 年 1 月的 JDK 8u152),Oracle 似乎已经修复了 linux、solaris、macosx 和 aix 中的问题。最接近的 Windows 错误似乎是 JDK-8000679。 Stuart Marks 决定在 2017 年 5 月关闭 JDK-8000679(此错误的 Windows 版本),遗憾地评论“这要么是 Java 网络代码中的错误,要么是 OS 网络层中的错误。关闭为不能复制。” 【参考方案1】:

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

您的超时设置在我看来完全没问题。某些请求似乎在java.net.SocketInputStream#socketRead0() 调用中不断被阻止的原因可能是由于服务器行为不端和您的本地配置的组合。套接字超时定义了两个连续的 i/o 读取操作(或换句话说,两个连续的传入数据包)之间的最大不活动时间。您的套接字超时设置为 5,000 毫秒。只要对方端点继续每 4,999 毫秒为块编码消息发送一个数据包,该请求就永远不会超时,并且最终会在java.net.SocketInputStream#socketRead0() 中阻止大部分时间发送。您可以通过在打开线路日志记录的情况下运行 HttpClient 来了解是否是这种情况。

【讨论】:

套接字读取超时定义了进入recv()方法和数据到达之间的最大间隔。它与读取操作之间或数据包之间的间隔无关。 正确。不会更改答案中的错误。计时器在您输入 recv() 或 read() 时启动,并在超时或数据或到达或 EOS 或发生错误时停止。与两次读取或两个数据包之间的间隔无关。您在上面写的内容开始没有意义。例如,这意味着您无法在第一次读取时获得超时。而且两次读取之间的时间与两个数据包之间的时间不同。 太棒了。您的精彩论点的问题在于 Java 试图抽象出低级 TCP/IP 机制并提供基于 I/O 流 API 的不同合同。 API 的使用者无法控制计时器、缓冲区或#recv() 方法。消费者可以看到当前执行线程在读操作中停留了多长时间。对于诸如 HTTP 内容正文之类的长数据流,重要的是操作解除阻塞需要多长时间,或者换句话说,在下一个读取操作开始并重置计时器之前,一个读取操作保持非活动状态多长时间。 您的“奇妙”答案的问题在于它是错误的,因为您可以通过实验轻松确定,而不是仅仅争论它,并发布更多关于两次读取之间间隔的未经证实的废话,或两个数据包,或任何其他你试图将其扭曲成的东西。我建议您在进一步辩论之前尝试一下。 当然@oleg 是对的:如果您连接的服务器非常慢,每 4.9 秒通过一个字节发送一个 1TB 的文件,您将在该 socketRead0() 上花费大量时间,而不会被超时踢出。一旦你在这种情况下有很多线程,你就耗尽了你的线程池,系统就会“关闭”。这就是为什么 HTTP/REST 是“微服务”之间通信的糟糕解决方案的原因之一。【参考方案2】:

您应该考虑使用非阻塞 HTTP 客户端,例如 Grizzly 或 Netty,它们没有用于挂起线程的阻塞操作。

【讨论】:

好主意,我可能会完成它,但我只是想澄清如何通过阻塞 Http 来实现这一点(调用 socketRead0,但不挂起)。所以接受了其他回应。谢谢。我只想补充一点,Apache Http Client 也有异步非阻塞版本。【参考方案3】:

作为Clint said,您应该考虑使用非阻塞HTTP 客户端,或者(看到您使用的是Apache Httpclient)实现Multithreaded request execution 以防止主应用程序线程可能挂起(这不能解决问题,但比重新启动您的应用程序更好,因为它被冻结了)。无论如何,您设置了 setStaleConnectionCheckEnabled 属性,但旧连接检查并非 100% 可靠,来自 Apache Httpclient 教程:

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

HttpClient 试图通过测试是否 连接“陈旧”,不再有效,因为它已关闭 在服务器端,在使用连接执行 HTTP 请求。陈旧的连接检查不是 100% 可靠的,并添加 每个请求执行需要 10 到 30 毫秒的开销。

Apache HttpComponents 团队建议实现 Connection eviction policy

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

看看 Connection eviction policy 部分的示例代码,并尝试在您的应用程序中实现它以及多线程请求执行,我认为这两种机制的实现将防止您不希望的挂起.

【讨论】:

感谢您的详细解答。关于驱逐政策的链接是我正在寻找的。我用整个连接管理器做了类似的事情,现在我知道如何在实际的单独连接上做到这一点。谢谢。但最后可能我会切换到非阻塞客户端。 驱逐策略旨在删除陈旧的 idle 连接。它不会对从池中租用并用于执行请求(并在读取操作中阻塞)的连接产生任何影响。 @oleg 如果是这样,我不接受答案。也许会出现一些新的东西。 如果你想知道发生了什么按照我在回答中的要求将挂起会话的电报给我【参考方案4】:

虽然这个问题提到了 Windows,但我在 Linux 上也有同样的问题。看来 JVM 实现阻塞套接字超时的方式存在缺陷:

https://bugs.openjdk.java.net/browse/JDK-8049846 https://bugs.openjdk.java.net/browse/JDK-8075484

总而言之,阻塞套接字的超时是通过在 Linux 上调用 poll(在 Windows 上调用 select)来确定数据是否可用,然后再调用 recv。但是,至少在 Linux 上,这两种方法都可以虚假地表明数据不可用,从而导致recv 无限期阻塞。

来自 poll(2) 手册页的错误部分:

请参阅 select(2) 的 BUGS 部分下关于虚假就绪通知的讨论。

来自 select(2) 手册页的错误部分:

在 Linux 下,select() 可能会将套接字文件描述符报告为“准备就绪” 用于阅读”,而随后的读取块。这可以 例如,当数据到达但经过检查时发生 校验和错误并被丢弃。可能还有其他情况 其中文件描述符被虚假报告为准备就绪。因此它 在不应阻塞的套接字上使用 O_NONBLOCK 可能更安全。

Apache HTTP 客户端代码有点难以理解,但 appears 连接过期仅针对 HTTP 保持活动连接(您已禁用)设置并且是无限期的,除非服务器另有说明。因此,正如 oleg 所指出的,Connection eviction policy 方法不适用于您的情况,并且一般不能依赖。

【讨论】:

看来这个bug已经在9月份修复了。您是否停止遇到此问题?【参考方案5】:

对于 Apache HTTP 客户端(阻塞),我发现最好的解决方案是 getConnectionManager()。并关闭它。

所以在高可靠性解决方案中,我只是在其他线程中安排关闭,如果请求未完成,我将从其他线程关闭

【讨论】:

【参考方案6】:

我有 50 多台机器,每天/机器发出大约 20 万个请求。他们正在运行 Amazon Linux AMI 2017.03。我以前有jdk1.8.0_102,现在我有jdk1.8.0_131。我同时使用 apacheHttpClient 和 OKHttp 作为抓取库。

每台机器运行 50 个线程,有时线程会丢失。在使用 Youkit java profiler 进行分析后,我得到了

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(早期访问)中。我已经将它安装在我们的一台机器上。现在我正在等待看到一些好的结果。

【讨论】:

感谢更新,请告知结果。 不走运。它被卡住了一夜。我将尝试在 oracle 上与他们联系以了解该错误。它被标记为已解决。当我厌倦了每天重新启动机器时,还可以找到一种解决方法(中止来自另一个线程的连接)。 @Stefan 感谢您的信息。如果您收到针对 Windows JDK 的错误,请在此 *** 问题上发布错误编号。【参考方案7】:

我在使用 apache 通用 http 客户端时遇到了同样的问题。

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

为了重现它,需要在一个新线程中执行来自问题的请求,注意细节:

在单独的线程中运行请求,在不同的线程中关闭请求并释放它的连接,中断挂起的线程 不要在 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)

它可能对谁感兴趣,它很容易重现,中断线程而不中止请求和释放连接(比率约为 1/100)。 Windows 10 版本 10.0。 jdk8.151-x64。

【讨论】:

【参考方案8】:

我觉得所有这些答案都太具体了。

我们必须注意,这可能是一个真正的 JVM 错误。应该可以获取文件描述符并关闭它。所有这些超时谈话都太高级了。您不希望在连接失败的情况下超时,您想要的是能够硬中断这个卡住的线程并停止或中断它。

JVM 实现 SocketInputStream.socketRead 函数的方式是设置一些内部默认超时,甚至应该低至 1 秒。然后当超时来临时,立即循环回socketRead0。在此期间,Thread.interrupt 和 Thread.stop 命令可以生效。

当然,更好的方法是不做任何阻塞等待,而是使用带有文件描述符列表的 select(2) 系统调用,当任何一个有可用数据时,让它执行读操作。

只要看看互联网上所有这些人都遇到了线程卡在 java.net.SocketInputStream#socketRead0 中的问题,这是关于 java.net.SocketInputStream 最流行的话题!

因此,虽然该错误尚未修复,但我想知道我能想出的最肮脏的技巧来打破这种情况。类似于连接调试器接口以获取 socketRead 调用的堆栈帧并获取 FileDescriptor,然后进入该堆栈以获取 int fd 编号,然后对该 fd 进行本机 close(2) 调用。

我们有机会这样做吗? (不要告诉我“这不是好的做法”)——如果是这样,我们就去做吧!

【讨论】:

【参考方案9】:

我今天遇到了同样的问题。基于@Sergei Voitovich,我试图让它仍然使用 Apache Http Client。

由于我使用的是 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();
    

结果非常好。我们曾经有过一些连接在 sockedRead0 中挂了 7 个小时的情况!现在,它永远不会超过定义的超时时间,并且它在生产环境中每天处理数百万个请求时不会出现任何问题。

【讨论】:

以上是关于如何防止 Java 中的 SocketInputStream.socketRead0 挂起?的主要内容,如果未能解决你的问题,请参考以下文章

如何防止java中的内存泄漏

如何防止 Java Swing BoxLayout 中的 JTextFields 扩展?

Java / Swing:JScrollPane中的JTextArea,如何防止自动滚动?

如何使用 Java 和 JDBC 防止一对多关系中的重复?

如何使用 Java 防止 XSS 攻击或 Rest API JSON 中的不可信数据?

不知道如何防止 java.lang.***Error