LoginSignup
13
12

More than 3 years have passed since last update.

Java11からSSL通信のエラーが発生しやすくなった事への考察と対処

Last updated at Posted at 2019-05-25

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では、 httpclientDefaultHttpRequestRetryHandler#INSTANCEでは通信が再試行されない。

なるほど。再試行されていないせいで、エラーが余計に発生しているように見えていたのかな。と現在たどり着きました。
たしかにこのエラーが発生する際、接続を開始しようとしてからすぐにエラーが発生します。

バックポートされているみたいですが、どうもSSLExceptionが返されることには変わりなさそうです。

を見る限り、下記のようになっている・・・というか、Alert#createSSLException を呼び出す以上、SSLException以外を返すことができないんですけどね。

+        if ((cause != null) && (cause instanceof IOException)) {
+            ssle = new SSLException(reason);
+        } 

対応

下記のように、DefaultHttpRequestRetryHandlerのretryRequestをオーバーライドして、

HttpRequestRetryHandler.java
  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関係は回答をなかなか貰えない、そもそも英語が通じているかわからないので、何か意見貰えるかは怪しいですが・・・。

参考

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
13
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12