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

问题描述 投票:47回答:7

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

java.net.SocketInputStream.socketRead0()

这是native的功能。

我试图设置Apche Http Client和RequestConfig以便(我希望)有一些可能的超时,但我仍然(可能是无限的)挂在socketRead0上。如何摆脱它们?

挂起比率约为每10000个请求约1个(到10000个不同的主机)并且它可能永远持续(我已确认线程挂起仍然有效,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

注意:实际上我有一些“技巧” - 我可以在其他.getConnectionManager().shutdown()中安排Thread取消Future如果请求正确完成,但它被删除并且它也杀死了整个HttpClient,不仅仅是那个单一的请求。

java sockets http timeout apache-httpclient-4.x
7个回答
19
投票

虽然这个问题提到了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客户端代码有点难以理解,但appears表示连接到期仅为HTTP保持活动连接(已禁用)设置,并且不确定,除非服务器另行指定。因此,正如oleg所指出的那样,Connection eviction policy方法对你的情况不起作用,一般不能依赖。


13
投票

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

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

HttpClient尝试通过测试连接是否“陈旧”来缓解此问题,该连接在使用连接执行HTTP请求之前不再有效,因为它在服务器端关闭。过时的连接检查不是100%可靠,并且每次请求执行都会增加10到30 ms的开销。

Apache HttpComponents工作人员建议实施Connection eviction policy

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

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


5
投票

您应该考虑像GrizzlyNetty这样没有阻塞操作来挂起线程的非阻塞HTTP客户端。


5
投票

我有超过50台机器,每天/机器约200k个请求。他们正在运行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(早期访问)。我已将它安装在我们的一台机器上。现在我等着看到一些好的结果。


2
投票

鉴于到目前为止没有其他人做出回应,这是我的看法

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


2
投票

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

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


2
投票

我使用apache常见的http客户端碰到了同样的问题。

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

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

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

首先,添加界面

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-64。

© www.soinside.com 2019 - 2024. All rights reserved.