2020/03/13 追記
まだ結構みてくれているので、対応状況を追記します。
https://github.com/apache/httpcomponents-client/pull/178
上記で、対応されています。
https://github.com/apache/httpcomponents-client/blob/master/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultHttpRequestRetryStrategy.java#L123-L135
5.0を使えば解決はしそうです。まだRC版しか提供はされていません。
はじめに
Java8+Spring Boot 1.5.Xの頃は、99.9999999%以上で通信が成功していたのに、Java11 + Spring Boot 2.1.Xにしてから、通信エラーの発生率が0.002%くらい上がったぞ・・・?と思い、調べてみました。
もしかしたら、間違っているかもしれないので、色々指摘を貰えると本当に嬉しいです。助かります。
発生するエラー
javax.net.ssl.SSLException: Connection reset
at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:127)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:264)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:259)
at java.base/sun.security.ssl.SSLSocketImpl.handleException(SSLSocketImpl.java:1314)
at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:839)
at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:137)
at org.apache.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:153)
at org.apache.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:280)
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:138)
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56)
at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259)
at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163)
at org.apache.http.impl.conn.CPoolProxy.receiveResponseHeader(CPoolProxy.java:157)
at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:273)
at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:125)
at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:272)
at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186)
at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56)
at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:87)
at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:735)
... 81 common frames omitted
Suppressed: java.net.SocketException: Broken pipe (Write failed)
at java.base/java.net.SocketOutputStream.socketWrite0(Native Method)
at java.base/java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:110)
at java.base/java.net.SocketOutputStream.write(SocketOutputStream.java:150)
at java.base/sun.security.ssl.SSLSocketOutputRecord.encodeAlert(SSLSocketOutputRecord.java:81)
at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:352)
... 106 common frames omitted
Caused by: java.net.SocketException: Connection reset
at java.base/java.net.SocketInputStream.read(SocketInputStream.java:186)
at java.base/java.net.SocketInputStream.read(SocketInputStream.java:140)
at java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:448)
at java.base/sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(SSLSocketInputRecord.java:68)
at java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1104)
at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:823)
... 102 common frames omitted
自分を悩ませたもの。
- 特定のLB (GCP)だけ、このエラーが発生しやすくなった。(他は安定してエラーが出ない)
なので、
https://stackoverflow.com/questions/49624532/java-net-socketexception-connection-reset-on-gcp
を最初は疑いました。でも、java8だとエラーは出ない。やっぱJava11関係・・・? - Connection reset系は、安定してstackoverflowで回答がつかない。根本的には、ネットワークの問題だから・・・。
- Java11からSSL通信周りが色々変わっている。
- 発生確率的に、
javax.net.debug
システムプロパティのデバッグがしんどい。GKEで動かしているので・・・ディスク量的に辛い。 - nodeでtcpdumpをやろうにも発生確率的にやっぱり辛い。
たどりついた答え
机上でのチェックや、JDK、Spring Boot、HttpClientのバグ確認をしていて、ふと目についたものが、
です。
返されるエラーが変わっている・・・?
たしかに、SSLException。handshakeとかのエラーじゃないけど、SSLException。
SSLExceptionでは、 httpclientのDefaultHttpRequestRetryHandler#INSTANCEでは通信が再試行されない。
なるほど。再試行されていないせいで、エラーが余計に発生しているように見えていたのかな。と現在たどり着きました。
たしかにこのエラーが発生する際、接続を開始しようとしてからすぐにエラーが発生します。
バックポートされているみたいですが、どうもSSLExceptionが返されることには変わりなさそうです。
を見る限り、下記のようになっている・・・というか、Alert#createSSLException
を呼び出す以上、SSLException以外を返すことができないんですけどね。
+ if ((cause != null) && (cause instanceof IOException)) {
+ ssle = new SSLException(reason);
+ }
対応
下記のように、DefaultHttpRequestRetryHandlerのretryRequestをオーバーライドして、
public class HttpRequestRetryHandler extends DefaultHttpRequestRetryHandler {
@Override
public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
IOException cause = exception;
if (exception instanceof SSLException) {
if (exception.getCause() != null && exception.getCause() instanceof IOException) {
cause = (IOException) exception.getCause();
}
}
return super.retryRequest(cause, executionCount, context);
}
}
下記のようにセットしてやればいいのかなーと思ってます。
CloseableHttpClient httpclient = HttpClients.custom()
.setRetryHandler(new HttpRequestRetryHandler())
.build();
ただ、もっと良い方法がないのか?というのをstackoverflowで質問してみてます。
ただ、上にも書いた通り、Connection reset関係は回答をなかなか貰えない、そもそも英語が通じているかわからないので、何か意見貰えるかは怪しいですが・・・。
参考
- https://hc.apache.org/httpcomponents-client-ga/tutorial/html/fundamentals.html
- https://bugs.openjdk.java.net/browse/JDK-8214339
java8の場合のエラー
Caused by: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "https://xxxxxxxxx": Connection reset; nested exception is java.net.SocketException: Connection reset
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:674)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:636)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:598)
... 54 common frames omitted
Caused by: java.net.SocketException: Connection reset
at java.net.SocketInputStream.read(SocketInputStream.java:210)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.security.ssl.InputRecord.readFully(InputRecord.java:465)
at sun.security.ssl.InputRecord.read(InputRecord.java:503)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:975)
at sun.security.ssl.SSLSocketImpl.readDataRecord(SSLSocketImpl.java:933)
at sun.security.ssl.AppInputStream.read(AppInputStream.java:105)
at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:137)
at org.apache.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:153)
at org.apache.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:282)
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:138)
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56)
at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259)
at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163)
at org.apache.http.impl.conn.CPoolProxy.receiveResponseHeader(CPoolProxy.java:165)
at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:273)
at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:125)
at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:272)
at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:185)
at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:111)
at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56)
at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:89)
at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:660)
... 58 common frames omitted