带有JDK11的Kubernetes中的Spring Boot随机“SSLException:Connection reset”

Uro*_* T. 7 java ssl spring resttemplate

语境:

  • 我们有一个 Spring Boot (2.3.1.RELEASE) 网络应用程序
  • 它是用 Java 8 编写的,但在带有 Java 11 ( openjdk:11.0.6-jre-stretch)的容器内运行。
  • 它有一个数据库连接和一个通过 https 调用的上游服务(简单的 RestTemplate#exchange 方法)(这很重要!)
  • 它部署在 Kubernetes 集群内部(不确定这是否重要)

问题:

  • 每天,我都会看到一小部分对上游服务的请求因以下错误而失败: I/O error on GET request for "https://upstream.xyz/path": Connection reset; nested exception is javax.net.ssl.SSLException: Connection reset
  • 错误是完全随机的,并且间歇性地发生。
  • 我们有一个与javax.net.ssl.SSLProtocolException: Connection resetJRE11 相关的类似错误 ( ),它是 TLS 1.3 协商问题。我们已将 Docker 映像更新为上述内容并修复了它。
  • 这是错误的堆栈跟踪:
java.net.SocketException: Connection reset
    at java.base/java.net.SocketInputStream.read(Unknown Source)
    at java.base/java.net.SocketInputStream.read(Unknown Source)
    at java.base/sun.security.ssl.SSLSocketInputRecord.read(Unknown Source)
    at java.base/sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(Unknown Source)
    at java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(Unknown Source)
    at java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(Unknown Source)
    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:739)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:674)
    at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:583)
....
Run Code Online (Sandbox Code Playgroud)

配置:

public static RestTemplate create(final int maxTotal, final int defaultMaxPerRoute,
                                  final int connectTimeout, final int readTimeout,
                                  final String userAgent) {
    final Registry<ConnectionSocketFactory> schemeRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
            .register("http", PlainConnectionSocketFactory.getSocketFactory())
            .register("https", SSLConnectionSocketFactory.getSocketFactory())
            .build();

    final PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(schemeRegistry);
    connManager.setMaxTotal(maxTotal);
    connManager.setDefaultMaxPerRoute(defaultMaxPerRoute);

    final CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(connManager)
            .setUserAgent(userAgent)
            .setDefaultRequestConfig(RequestConfig.custom()
                                             .setConnectTimeout(connectTimeout)
                                             .setSocketTimeout(readTimeout)
                                             .setExpectContinueEnabled(false).build())
            .build();

    return new RestTemplateBuilder()
            .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(httpClient))
            .build();
}
Run Code Online (Sandbox Code Playgroud)

有没有人遇到过这个问题?当我打开 http 客户端上的调试日志时,它充满了噪音,我无法辨别任何有用的东西......

van*_*den 20

我们在迁移到 AWS/Kubernetes 时遇到了类似的问题。我已经找到原因了

您正在使用连接池。PoolingHttpClientConnectionManager 的默认行为是重用连接。因此,当您的请求完成时,连接不会立即关闭。这样就不必一直重新连接,从而节省资源。

Kubernetes 集群使用 NAT(网络地址转换)进行传出连接。当某个连接在一定时间内没有使用时,该连接将从 NAT 表中删除,并且该连接将被断开。这会导致看似随机的 SSLException。

在 AWS 上,当连接空闲 350 秒时,将从 NAT 表中删除连接。其他 Kubernetes 实例可能有其他设置。

请参阅https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-troubleshooting.html

解决方案:

禁用连接重用:

final CloseableHttpClient closeableHttpClient = HttpClients.custom()
    .setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE)
    .setConnectionManager(poolingHttpClientConnectionManager)
    .build();
Run Code Online (Sandbox Code Playgroud)

或者,让 httpClient 驱逐空闲时间过长的连接:

return HttpClients.custom()
            .evictIdleConnections(300, TimeUnit.SECONDS)  //Read the javadocs, may not be used when the instance of HttpClient is created inside an EJB container.
            .setConnectionManager(poolingHttpClientConnectionManager)
            .build();
        
Run Code Online (Sandbox Code Playgroud)

或者使用自定义的 KeepAliveStrategy进行调用setConnectionKeepAliveStrategy(....),该策略永远不会返回 -1 或超时值超过 300 秒。


Jud*_*han 5

我将分享我对此错误的经验,可能这与您面临的问题相同。比较我的堆栈跟踪。

由于这是随机发生的,所以我怀疑这是同一个问题。

HTTP 连接是通过 HTTP 客户端库(Apache HTTP Client)建立的。

HTTP 客户端通常管理可重用的连接池。这个池子是有限制的。在我们的例子中,连接池有时(随机地)被完全占用。不再有可以使用的免费连接。

  1. 您可以增加池大小
  2. 实现退避重试机制,当成功执行 HTTP 请求失败时,该机制将尝试从 HTTP 连接池中获取连接。

如果您想知道如何调整 sprint 启动中使用的底层 HTTP 客户端,请查看这篇文章。


jac*_*neo 0

我猜这个问题与k8s有关。

  1. 如果您使用flannel作为k8s网络,请检查flannel状态并查看是否重启多次。使用下面的命令
kubectl get pod -n kube-system | grep flannel
Run Code Online (Sandbox Code Playgroud)
  1. 你的linux kennel是什么版本的?如果不是 4.x 版本或更高版本,请升级到 4.x。
# to check linux kennel version
uname -sr 

# upgrade step
1)
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-4.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel -y install kernel-lt
2) open and edit /etc/default/grub, and set "GRUB_DEFAULT=0"
3) grub2-mkconfig -o /boot/grub2/grub.cfg
4) reboot
Run Code Online (Sandbox Code Playgroud)

希望它对解决问题有用。