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

时间:2015-02-28 18:08:44

标签: java sockets http timeout apache-httpclient-4.x

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

java.net.SocketInputStream.socketRead0()

哪个是native功能。

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

Hung比率约为每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,不仅是那个请求。

9 个答案:

答案 0 :(得分: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方法在你的案例中不起作用,而且一般都不能依赖。

答案 1 :(得分:13)

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

  

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

     

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

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

  

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

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

答案 2 :(得分:5)

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

答案 3 :(得分: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中的

(早期访问)。我已将它安装在我们的一台机器上。现在我等着看到一些好的结果。

答案 4 :(得分:2)

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

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

答案 5 :(得分:2)

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

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

答案 6 :(得分:2)

我使用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-64。

答案 7 :(得分:1)

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

我们必须注意,这可能是一个真正的JVM错误。应该可以获取文件描述符并关闭它。所有这些超时对话都太高了。您不希望超时到连接失败的程度,您想要的是一种硬中断此阻塞线程并停止或中断它的能力。

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

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

只需在Internet上四处看看所有这些人都遇到了线程阻塞在java.net.SocketInputStream#socketRead0中的麻烦,这是关于java.net.SocketInputStream的最受欢迎的话题!

因此,尽管该错误未修复,但我想知道我可以拿出最肮脏的技巧来解决这种情况。类似于与调试器接口连接以获取socketRead调用的堆栈框架并获取FileDescriptor,然后闯入该接口以获取int fd编号,然后对该fd进行本机close(2)调用。

我们有机会这样做吗? (不要告诉我“这不是好习惯”)-如果是这样,那就去做吧!

答案 8 :(得分:1)

我今天也面临同样的问题。基于@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个小时的情况!现在,它永远不会超过定义的超时时间,并且每天都可以处理数百万个请求,而不会出现任何问题。