Jak zapobiec zawieszeniu na SocketInputStream.socketRead0 w Javie?

Wykonywanie milionów żądań HTTP z różnymi bibliotekami Javy daje mi wątki powieszone na:

java.net.SocketInputStream.socketRead0()

Czyli native funkcja.

Próbowałem skonfigurować Apche Http Client i RequestConfig aby mieć timeouty NA (mam nadzieję) wszystko, co jest możliwe, ale mimo to, mam (prawdopodobnie nieskończony) wisi na socketRead0. Jak się ich pozbyć?

Współczynnik zawieszania wynosi około ~1 na 10000 żądań (do 10000 różnych hostów) i może trwać prawdopodobnie wiecznie (potwierdziłem wątek zawieszony jako nadal ważny po 10 godzinach).

JDK 1.8 Na Windows 7.

Moja HttpClient fabryka:

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();

Moja RequestConfig fabryka:

    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

Uwaga: właściwie mam pewien "trick" - mogę zaplanować .getConnectionManager().shutdown() w innym Thread z anulowaniem Future, jeśli żądanie zostało poprawnie zakończone, ale jest zepsute, a także zabija całe HttpClient, nie tylko to pojedyncze żądanie.

Author: Fabio Bonfante, 2015-02-28

7 answers

Choć to pytanie dotyczy Windowsa, to mam ten sam problem na Linuksie. Wygląda na to, że istnieje wada w sposobie, w jaki JVM implementuje blokowanie timeoutów gniazd:

Podsumowując, timeout blokowania gniazd jest zaimplementowany przez wywołanie poll w Linuksie (i select W Windows) w celu ustalenia, czy dane są dostępne przed wywołaniem recv. Jednak, przynajmniej w Linuksie, obie metody mogą skrupulatnie wskazywać, że dane są dostępne, gdy ich nie ma, co prowadzi do recv blokowania w nieskończoność.

Z sekcji błędów strony Man poll(2):

Zobacz Omówienie fałszywych powiadomień o gotowości w sekcji błędy select(2).

Z sekcji błędów strony man select(2):

Pod Linuksem select () może zgłosić deskryptor pliku gniazda jako " ready do czytania", podczas gdy jednak kolejne Czytaj bloki. To może na przykład, gdy dane dotarły, ale po zbadaniu błędna suma kontrolna i jest odrzucana. Mogą zaistnieć inne okoliczności w którym deskryptor pliku jest zgłaszany jako gotowy. W ten sposób bezpieczniejsze może być użycie O_NONBLOCK na gniazdach, które nie powinny się blokować.

Kod klienta HTTP Apache jest nieco trudny do naśladowania, ale wydaje się że wygaśnięcie połączenia jest ustawione tylko dla połączeń HTTP keep-alive (które zostały wyłączone) i jest nieokreślony, chyba że serwer określi inaczej. Dlatego, jak zauważył oleg, podejście do eksmisji połączeń nie będzie działać w Twoim przypadku i nie można na nim polegać w ogóle.

 15
Author: Trevor Robinson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-18 16:48:41

Jak powiedział Clint, powinieneś rozważyć nieblokującego klienta HTTP lub (widząc, że używasz Httpclient Apache) zaimplementować wielowątkowe wykonanie żądania , aby zapobiec ewentualnemu zawieszeniu głównego wątku aplikacji (nie rozwiązuje to problemu, ale jest lepsze niż ponowne uruchomienie aplikacji, ponieważ jest zamrożona). W każdym razie Ustawiłeś Właściwość setStaleConnectionCheckEnabled, ale Sprawdzanie połączenia nie jest w 100% niezawodne, z samouczka Apache Httpclient:

Jeden z głównych wadami klasycznego modelu blokującego We / Wy są że gniazdo sieciowe może reagować na zdarzenia We/Wy tylko wtedy, gdy jest zablokowane w operacji wejścia / Wyjścia. Po zwolnieniu połączenia z powrotem do menedżera, może być utrzymywany przy życiu, jednak nie jest w stanie monitorować stanu socket i reagować na wszelkie zdarzenia We/Wy. Jeśli połączenie zostanie zamknięte na po stronie serwera, połączenie po stronie klienta nie jest w stanie wykryć zmiana stanu połączenia (i odpowiednio reagować zamykając gniazdo na jego end).

HttpClient próbuje złagodzić problem, sprawdzając, czy połączenie jest "stare", które nie jest już ważne, ponieważ zostało zamknięte po stronie serwera, przed użyciem połączenia do wykonania Żądanie HTTP. Sprawdzanie połączenia nie jest w 100% niezawodne i dodaje 10 do 30 ms na każde żądanie wykonania.

Załoga Apache HttpComponents zaleca wdrożenie Zasady eksmisji połączeń

Jedyne możliwe rozwiązanie, które nie obejmuje jednego wątku na model gniazda dla połączeń bezczynnych to dedykowany gwint monitora używany do eksmisji połączeń, które są uważane za wygasłe z powodu długiego okresu bezczynności. Wątek monitora może okresowo wywoływać Clientconnectionmanager # closeexpiredconnections () metoda zamykająca wszystkie wygasłe połączenia i eksmitować zamknięte połączenia z basenu. Może opcjonalnie również wywołanie ClientConnectionManager#closeIdleConnections() metoda na Zamknij wszystkie połączenia, które były bezczynne w danym okres czasu.

Spójrz na przykładowy kod sekcji Connection eviction policy i spróbuj zaimplementować go w swojej aplikacji wraz z wykonaniem żądania wielowątkowego, myślę, że implementacja obu mechanizmów zapobiegnie niepożądanemu zawieszeniu.

 6
Author: vzamanillo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:24

Mam ponad 50 maszyn, które wykonują około 200 tysięcy zapytań / dzień / maszyna. Działają Amazon Linux AMI 2017.03. Wcześniej miałem jdk1.8. 0_102, teraz mam jdk1.8. 0_131. Używam zarówno apacheHttpClient, jak i OKHttp jako bibliotek skrobania.

Każda maszyna uruchamiała 50 wątków, a czasami wątki się gubią. Po profilowaniu z Youkit java profiler dostałem

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

Dowiedziałem się, że mają do tego fix

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

W JDK 8u152 (wczesny dostęp). Zainstalowałem go na jednej z naszych maszyn. Teraz czekam na dobre wyniki.
 4
Author: Stefan Matei,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-20 12:25:57

Powinieneś rozważyć nieblokującego klienta HTTP, takiego jak Grizzly lub Netty , który nie ma operacji blokowania zawieszenia wątku.

 3
Author: Clint,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-09 20:04:13

Dla klienta HTTP Apache (blokowanie) uznałem, że najlepszym rozwiązaniem jest getConnectionManager (). i wyłącz to.

Więc w rozwiązaniu o wysokiej niezawodności po prostu planuję wyłączenie w innym wątku i w przypadku, gdy żądanie się nie zakończy, wyłączam się z innego wątku

 1
Author: Piotr Müller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-28 08:09:55

Natknąłem się na ten sam problem używając Apache common http client.

Istnieje dość proste obejście (które nie wymaga wyłączania Menedżera połączeń):

Musimy wykonać żądanie z pytania w nowym wątku z pewnymi dodatkowymi wydatkami:

  • Uruchom żądanie w osobnym wątku, Zamknij żądanie i zwolnij jego połączenie w innym wątku, przerwij wiszący wątek
  • nie uruchamiaj EntityUtils.consumeQuietly(response.getEntity()) w końcu blok (bo wisi na 'martwym' połączenie)

Najpierw dodaj interfejs

interface RequestDisposer {
    void dispose();
}

Wykonywanie żądań http w nowym wątku

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()

Wywołanie dispose() w głównym wątku, aby zamknąć połączenie wiszące

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

To naprawiło problem dla mnie.

Mój stacktrace wyglądał tak:

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)

Dla kogo może być interesujące, że łatwo odtwarza, przerywa wątek bez przerywania żądania i zwalniania połączenia (stosunek wynosi około 1/100). Windows 10, Wersja 10.0. jdk8. 151-x64.

 1
Author: Sergey Voitovich,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-20 11:48:42

Biorąc pod uwagę, że nikt inny nie odpowiedział do tej pory, oto moje ujęcie

Twoje ustawienie timeout wygląda jak dla mnie w porządku. Powodem, dla którego niektóre żądania wydają się być stale blokowane w wywołaniu java.net.SocketInputStream#socketRead0(), może być kombinacja niewłaściwych serwerów i Twojej lokalnej konfiguracji. Socket timeout określa maksymalny okres nieaktywności pomiędzy dwoma kolejnymi operacjami odczytu We / Wy (lub innymi słowy dwoma kolejnymi przychodzącymi pakietami). Ustawienie limitu czasu gniazda wynosi 5000 milisekund. As long ponieważ przeciwny punkt końcowy wysyła pakiet co 4999 milisekund dla zakodowanego fragmentu wiadomości, żądanie nigdy się nie skończy i zakończy się wysłaniem większości swojego czasu zablokowanym w java.net.SocketInputStream#socketRead0(). Możesz dowiedzieć się, czy tak jest, uruchamiając HttpClient z włączonym rejestrowaniem drutu.

 0
Author: oleg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-07 10:45:34