web-dev-qa-db-fra.com

Comment empêcher les blocages sur SocketInputStream.socketRead0 en Java?

L'exécution de millions de requêtes HTTP avec différentes Java m'indique les discussions suspendues sur:

Java.net.SocketInputStream.socketRead0()

Quelle est la fonction native.

J'ai essayé de configurer Apche Http Client et RequestConfig pour avoir des délais d'attente (j'espère) tout ce qui est possible, mais quand même, j'ai (probablement infini) accroché sur socketRead0. Comment s'en débarrasser?

Le rapport de Hung est d'environ 1 pour 10 000 requêtes (sur 10 000 hôtes différents) et peut durer éternellement (j'ai confirmé que le blocage du fil était toujours valide après 10 heures).

JDK 1.8 sur Windows 7.

Mon HttpClient fabrique:

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

Mon RequestConfig fabrique:

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

source OpenJDK socketRead0

Note: En fait, j'ai quelques "trucs" - je peux programmer .getConnectionManager().shutdown() dans d'autres Thread avec annulation de Future si la requête est correctement terminée, mais elle est obsolète et tue également tout HttpClient, pas seulement cette requête unique.

49
Piotr Müller

Pour Apache HTTP Client (blocage), la meilleure solution consiste à getConnectionManager (). et l'arrêter.

Donc, dans la solution à haute fiabilité, je planifie simplement l'arrêt dans un autre thread et si la requête ne se termine pas, je m'arrête à partir d'un autre thread

2
Piotr Müller

Bien que cette question mentionne Windows, j'ai le même problème sous Linux. Il semble qu'il y ait une faille dans la manière dont la JVM implémente les délais d'attente de socket bloquants:

Pour résumer, le délai de blocage des sockets est mis en œuvre en appelant poll sous Linux (et select sous Windows) pour déterminer que les données sont disponibles avant d'appeler recv. Cependant, au moins sous Linux, les deux méthodes peuvent indiquer de manière non-conforme que des données sont disponibles quand elles ne le sont pas, ce qui conduit à un blocage indéfini de recv.

Extrait de la section BUGS de la page de manuel poll (2):

Voir la discussion sur les notifications de préparation parasite dans la section BUGS de select (2).

Dans la section BOGUES de la page de manuel select (2):

Sous Linux, select () peut signaler un descripteur de fichier de socket comme étant "prêt pour la lecture", tout en conservant une lecture ultérieure. Cela peut par exemple se produire lorsque les données sont arrivées mais qu'après contrôle, la somme de contrôle est erronée et sont ignorées. Il peut y avoir d'autres circonstances dans lesquelles un descripteur de fichier est faussement signalé comme étant prêt. Il peut donc être plus sûr d’utiliser O_NONBLOCK sur des sockets qui ne devraient pas bloquer.

Le code du client HTTP Apache est un peu difficile à suivre, mais il apparaît que l’expiration de la connexion n’est définie que pour les connexions persistantes HTTP (que vous avez désactivé) et est indéfini, sauf indication contraire du serveur. Par conséquent, comme l'a souligné oleg, l'approche relative à l'expulsion de la connexion ne fonctionnera pas dans votre cas et ne peut être invoquée en général.

19
Trevor Robinson

Comme Clint, dit , vous devriez envisager un client HTTP non bloquant, ou (voyant que vous utilisez Apache Httpclient) implémenter un exécution de requête multithread pour empêcher tout blocage éventuel du thread principal de l'application (cela ne résout pas le problème, mais vaut mieux que de redémarrer votre application car il est gelé). Quoi qu'il en soit, vous définissez la propriété setStaleConnectionCheckEnabled mais la vérification de la connexion obsolète n'est pas fiable à 100%, selon le didacticiel Apache Httpclient:

L'un des inconvénients majeurs du modèle d'E/S à blocage classique est que le socket réseau peut réagir aux événements d'E/S uniquement lorsqu'il est bloqué lors d'une opération d'E/S. Lorsqu'une connexion est restituée au gestionnaire, elle peut être conservée, mais il est incapable de surveiller l'état du socket et de réagir à tout événement d'E/S. Si la connexion est fermée côté serveur, la connexion côté client est incapable de détecter le changement d'état de la connexion (et réagit de manière appropriée en fermant le socket à son extrémité).

HttpClient tente d'atténuer le problème en vérifiant si la connexion est "obsolète", ce qui n'est plus valide car elle était fermée côté serveur, avant d'utiliser la connexion pour exécuter une requête HTTP. La vérification des connexions obsolètes n’est pas fiable à 100% et ajoute 10 à 30 ms de temps système à chaque exécution de demande.

L’équipe Apache HttpComponents recommande la mise en œuvre d’un politique d’expulsion de connexion

La seule solution réalisable qui n'implique pas un modèle à un thread par socket pour les connexions inactives est un thread de moniteur dédié utilisé pour supprimer les connexions considérées comme expirées en raison d'une longue période d'inactivité. Le thread de moniteur peut régulièrement appeler la méthode ClientConnectionManager # closeExpiredConnections () pour fermer toutes les connexions expirées et expulser les connexions fermées du pool. Elle peut également éventuellement appeler la méthode ClientConnectionManager # closeIdleConnections () pour fermer toutes les connexions inactives au cours d'une période donnée.

Examinez l'exemple de code de la section relative aux règles d'éviction de connexion et essayez de le mettre en œuvre dans votre application en même temps que l'exécution de la demande multithread. Je pense que cette implémentation des deux mécanismes empêchera vos blocages indésirables.

13
vzamanillo

Vous devriez envisager un client HTTP non bloquant tel que Grizzly ou Netty qui ne disposent pas d'opérations de blocage pour suspendre un thread.

5
Clint

J'ai plus de 50 machines qui font environ 200 000 demandes/jour/machine. Ils exécutent Amazon Linux AMI 2017.03. J'avais auparavant jdk1.8.0_102, maintenant j'ai jdk1.8.0_131. J'utilise à la fois apacheHttpClient et OKHttp comme bibliothèques de récupération.

Chaque machine exécutait 50 threads et parfois, les threads étaient perdus. Après le profilage avec Youkit Java profiler je me suis

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

J'ai découvert qu'ils ont un correctif pour cela

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

jDK 8u152 (accès anticipé). Je l'ai installé sur l'une de nos machines. Maintenant, j'attends de bons résultats.

5
Stefan Matei

Étant donné que personne d'autre n'a répondu jusqu'à présent, voici ce que je pense

Votre réglage de délai d'attente me convient parfaitement. La raison pour laquelle certaines demandes semblent être constamment bloquées dans un appel Java.net.SocketInputStream#socketRead0() est probablement due à une combinaison de serveurs défectueux et de votre configuration locale. Le délai d'attente de socket définit une période maximale d'inactivité entre deux opérations de lecture d'E/S consécutives (ou, en d'autres termes, deux paquets entrants consécutifs). Le paramètre de délai d'attente de votre socket est 5 000 millisecondes. Tant que le point d'extrémité opposé continue d'envoyer un paquet toutes les 4 999 millisecondes pour un message codé par bloc, la requête n'expire jamais et finira par envoyer la plus grande partie de son temps bloqué dans Java.net.SocketInputStream#socketRead0(). Vous pouvez savoir si c'est le cas ou non en exécutant HttpClient avec l'enregistrement par fil activé.

2
ok2c

Je suis tombé sur le même problème en utilisant le client HTTP commun Apache.

Il existe une solution de contournement assez simple (qui ne nécessite pas de fermer le gestionnaire de connexions):

Pour la reproduire, il faut exécuter la requête de la question dans un nouveau fil en prenant en compte les détails:

  • exécuter la requête dans un thread séparé, fermer la requête et libérer sa connexion dans un thread différent, interrompre le thread en suspension
  • ne lancez pas EntityUtils.consumeQuietly(response.getEntity()) dans le bloc finally (car il se bloque sur une connexion "morte")

D'abord, ajoutez l'interface

interface RequestDisposer {
    void dispose();
}

Exécuter une requête HTTP dans un nouveau thread

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

Appelez dispose() dans le thread principal pour fermer la connexion suspendue.

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

Cela a résolu le problème pour moi.

Mon stacktrace ressemblait à ceci:

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)

Pour qui cela pourrait être intéressant, il est facilement reproductible, interrompez le thread sans abandonner la demande et libérer la connexion (le rapport est d'environ 1/100). Windows 10, version 10.0. jdk8.151-x64.

2
Sergey Voytovich