さまざまな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);
注:実際には「トリック」があります-リクエストが適切に終了した場合、.getConnectionManager().shutdown()
を他のThread
でキャンセルしてFuture
をキャンセルできますが、それは非難され、殺されます単一のリクエストだけでなく、HttpClient
全体。
Apache HTTPクライアント(ブロッキング)の場合、最善の解決策はgetConnectionManager()であることがわかりました。シャットダウンします。
したがって、高信頼性ソリューションでは、他のスレッドでシャットダウンをスケジュールし、リクエストが完了しない場合は、他のスレッドからシャットダウンします
この質問ではWindowsについて言及していますが、Linuxでも同じ問題があります。 JVMがブロッキングソケットタイムアウトを実装する方法に問題があるようです。
要約すると、ソケットをブロックするためのタイムアウトは、poll
を呼び出す前にLinuxでselect
(およびWindowsでrecv
)を呼び出してデータが利用できることを確認することで実装されます。ただし、少なくともLinuxでは、どちらの方法でもデータが利用可能ではないときに利用可能であると誤って示すことがあるため、recv
が無期限にブロックされます。
Poll(2)のマニュアルページのバグセクションから:
Select(2)のBUGSセクションにある偽の準備通知の説明を参照してください。
Select(2)manページのバグセクションから:
Linuxでは、select()はソケットファイル記述子を「ready for reading」として報告しますが、それでも後続の読み取りブロックはブロックします。これは、たとえば、データが到着したが、検査時に間違ったチェックサムがあり、破棄された場合に発生する可能性があります。ファイル記述子が準備完了として誤って報告される他の状況があるかもしれません。したがって、ブロックすべきでないソケットでO_NONBLOCKを使用する方が安全かもしれません。
Apache HTTPクライアントのコードを追うのは少し難しいですが、 appears 接続の有効期限はHTTPキープアライブ接続(無効にした)に対してのみ設定され、サーバーが特に指定しない限り無期限です。したがって、olegによって指摘されているように、 接続排除ポリシー アプローチはあなたの場合には機能せず、一般的に信頼することはできません。
クリントが言った のように、非ブロッキングHTTPクライアントを検討するか、(Apache Httpclientを使用していることを確認して) マルチスレッド要求の実行 を実装して、メインアプリケーションスレッド(これは問題を解決しませんが、フリーズしているためアプリを再起動するよりも優れています)。とにかく、setStaleConnectionCheckEnabled
プロパティを設定しますが、古い接続チェックは100%信頼性がありません、Apache Httpclientチュートリアルから:
従来のブロッキングI/Oモデルの主な欠点の1つは、ネットワークソケットがI/O操作でブロックされた場合にのみI/Oイベントに反応できることです。接続が解放されてマネージャに戻されると、接続は維持されますが、ソケットのステータスを監視したり、I/Oイベントに反応したりすることはできません。サーバー側で接続が閉じられると、クライアント側の接続は接続状態の変化を検出できません(そして、その側でソケットを閉じることで適切に反応します)。
HttpClientは、HTTPリクエストの実行に接続を使用する前に、接続が「古い」かどうか、つまりサーバー側で閉じられたために無効であるかどうかをテストすることにより、問題の軽減を試みます。古い接続チェックは100%の信頼性がなく、各リクエストの実行に10〜30ミリ秒のオーバーヘッドが追加されます。
Apache HttpComponentsの乗組員は、接続削除ポリシーの実装を推奨しています。
アイドル接続のソケットモデルごとに1つのスレッドを含まない唯一の実行可能なソリューションは、長期間の非アクティブが原因で期限切れと見なされた接続を排除するために使用される専用モニタースレッドです。監視スレッドは、ClientConnectionManager#closeExpiredConnections()メソッドを定期的に呼び出して、期限切れの接続をすべて閉じ、閉じた接続をプールから削除できます。また、オプションでClientConnectionManager#closeIdleConnections()メソッドを呼び出して、一定期間アイドル状態だったすべての接続を閉じることができます。
Connection eviction policyセクションのサンプルコードを見て、マルチスレッドリクエストの実行とともにアプリケーションに実装してみてください。両方のメカニズムが、望ましくないハングを防ぎます。
50台以上のマシンがあり、1日あたり約20万件のリクエストを処理しています。 Amazon Linux AMI 2017.03を実行しています。以前はjdk1.8.0_102でしたが、現在はjdk1.8.0_131です。スクレイピングライブラリとしてapacheHttpClientとOKHttpの両方を使用しています。
各マシンは50個のスレッドを実行しており、時々、スレッドが失われます。 Youkit Javaプロファイラーでプロファイリングした後、私は
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(初期アクセス)。私たちのマシンにインストールしました。今、私はいくつかの良い結果を待っています。
これまでに誰も応答しなかったので、ここに私の意見があります
あなたのタイムアウト設定は私には完璧に見えます。特定のリクエストがJava.net.SocketInputStream#socketRead0()
呼び出しで常にブロックされているように見える理由は、サーバーとローカル設定の動作が正しくないためです。ソケットタイムアウトは、2つの連続したI/O読み取り操作(つまり、2つの連続した着信パケット)の間の非アクティブの最大期間を定義します。ソケットのタイムアウト設定は5,000ミリ秒です。反対側のエンドポイントが、チャンクエンコードされたメッセージに対して4,999ミリ秒ごとにパケットを送信し続ける限り、リクエストはタイムアウトせず、Java.net.SocketInputStream#socketRead0()
でブロックされた時間のほとんどを送信します。ワイヤーログをオンにしてHttpClientを実行することで、これが事実であるかどうかを確認できます。
Apache共通HTTPクライアントを使用して同じ問題にぶつかりました。
非常に簡単な回避策があります(接続マネージャーをシャットダウンする必要はありません):
それを再現するには、詳細に注意を払いながら、新しいスレッドで質問からのリクエストを実行する必要があります。
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。