排查与解决`java.io.EOFException: \n not found: limit=0 content=…

排查与解决java.io.EOFException: \n not found: limit=0 content=…

问题

起因是项目向华为云 OBS 请求视频, 偶现题目中的异常;如果等一段时间(30s),就可以正常读取到视频。

原因是 OBS SDK 底层用的 Okhttp 连接池,而 Okhttp 的 GitHub 上也有相关 issue:

  • 服务器在返回数据后就直接关闭 TCP 连接,而客户端(项目应用)却将连接放入连接池重复使用;
  • 当下次请求复用时,连接已经被关闭,就读取不到返回值,报上述异常。
  • 如果等待一段时间,等连接池杀掉闲置连接(闲置存活时长 30s),那下次请求就会重新建立一个连接,就可以正常拉到视频。

解决的方法很多,有生硬的,有灵活的;可以改上层代码,可以动二方库的包。

但是因为我们明明所有环境都连一个 OBS 服务器,但只有一个环境出现上述问题。

最后发现根本原因是:出问题的环境连的是代理服务器,代理服务器没有开启 keepalive。

后面讲一下排查过程和可以解决的手段。

急需解决方案的,直接看解决手段吧。

排查

如果查看日志的话,标题的异常会被 Okhttp 用 IOException 封装一下连接信息抛出来,最后被华为云 OBS SDK 再包一层 ObsException: OBS servcie Error Message

所以第一步,找到根本原因是 Caused by: java.io.EOFException: \n not found: limit=0 content=…

先去GitHub - huaweicloud/huaweicloud-sdk-java-obs: The OBS SDK for Java, which is used for accessing Object Storage Service 找,没有发现相关 issue。

遇事不决就 Google, 发现 Okhttp 有相关的:java.io.EOFException: \n not found: limit=0 content=… · Issue #5390 · square/okhttp · GitHub。然后看了一下 OBS SDK 浓眉大眼的,还真用的是 Okhttp。

Okhttp

大致原因和开头说的一样:

  • 服务器在返回数据后就直接关闭 TCP 连接,而客户端(项目应用)却将连接放入连接池重复使用;
  • 当下次请求复用时,连接已经被关闭,就读取不到返回值,报上述异常。
  • 如果等待一段时间,等连接池杀掉闲置连接,那下次请求就会重新建立一个连接,就可以正常拉到视频。

当然,大家可能都是希望可以提前发现已经被关闭的连接,去主动使用打开的 TCP 连接使用。如果仔细去翻类似 issue 的话,官方的回复都是:As far as I can tell, there’s no efficient way to detect that the connection has been closed. If you can find one, we’d love to use it!

所以 Okhttp 是提供了一个重试参数,如果发现连接失败了,会重试。默认是开启的。

OkHttpClient.retryOnConnectionFailure
public Builder() {
 ...
 retryOnConnectionFailure = true;
 ...
}

但是 OBS SDK 却在构建时设为 false,不明白为啥。builder.....retryOnConnectionFailure(false).....

如果看到这,相信也发现了 SDK 写死设置为 false 了,根本没有在外部配置的可能性。兜兜转转又回到了起点。

huaweicloud-sdk-java-obs

SDK 基本只靠 ObsConfiguration这个类对自己和 Okhttp Client 进行配置,可以说这就是一个唯一的入口:

@Bean
public ObsClient obsClient(HwCloudObsPropreties hwCloudObsPropreties) {
    ObsConfiguration obsConfiguration = new ObsConfiguration();
    obsConfiguration.setEndPoint(hwCloudObsPropreties.getEndPoint());
    obsConfiguration.setEndpointHttpPort(80);
    obsConfiguration.setEndpointHttpsPort(443);
    obsConfiguration.setConnectionTimeout(60000);
    obsConfiguration.setSocketTimeout(60000);
    obsConfiguration.setValidateCertificate(false);
    obsConfiguration.setUploadStreamRetryBufferSize(512);
    obsConfiguration.disableNio();
    return new ObsClient(hwCloudObsPropreties.getAccessKey(),hwCloudObsPropreties.getSecretKey(),obsConfiguration);
}

public class ObsConfiguration implements Cloneable {
    private int connectionTimeout = 60000;
    private int idleConnectionTime = 30000;
    private int maxIdleConnections = 1000;
    private int maxConnections = 1000;
    private int maxErrorRetry = 3;
    ......
}

很遗憾,它不支持 retryOnConnectionFailure,至少是目前使用的版本:3.1.3。

于是这里又剩了几条路

  • 下源码,手动改,再重新打包,稍微麻烦一点。
  • 在外层捕获异常,硬编码重试,不够优雅。
  • 偏门方式:把连接池闲置连接存活时间idleConnectionTime 设置的小一些(因为不能为 0),可以尽量小一些,但是担心太小,连接池的就没有意义了。频繁建立与销毁会带来性能损耗。
  • 升级到新版,至少是 3.20.1 以后,但是整体兼容性未知。

关于第三点升级的方案,在 3.20.1 以后,扩展了huaweicloud-sdk-java-obs/ExtObsConfiguration.java 来继承 ObsConfiguration,增加了对retryOnConnectionFailure的支持。

public class ExtObsConfiguration extends ObsConfiguration {
    // 是否重试
    private boolean retryOnConnectionFailureInOkhttp;
    // times for retryOnRetryOnUnexpectedEndException;
    private int maxRetryOnUnexpectedEndException;
    public ExtObsConfiguration() {
        super();
        this.retryOnConnectionFailureInOkhttp = ExtObsConstraint.DEFAULT_RETRY_ON_CONNECTION_FAILURE_IN_OKHTTP;
        this.maxRetryOnUnexpectedEndException = ExtObsConstraint.DEFAULT_MAX_RETRY_ON_UNEXPECTED_END_EXCEPTION;
    }
}

构建 client 时, 根据实际配置填入:

if (config instanceof ExtObsConfiguration) {
            // retry in okhttp
            obsProperties.setProperty(ExtObsConstraint.IS_RETRY_ON_CONNECTION_FAILURE_IN_OKHTTP,
                    String.valueOf(((ExtObsConfiguration) config).isRetryOnConnectionFailureInOkhttp()));

            // retry on unexpected end exception
            obsProperties.setProperty(ExtObsConstraint.HTTP_MAX_RETRY_ON_UNEXPECTED_END_EXCEPTION,
                    String.valueOf(((ExtObsConfiguration) config).getMaxRetryOnUnexpectedEndException()));
        }


builder..retryOnConnectionFailure( obsProperties.getBoolProperty(ExtObsConstraint.IS_RETRY_ON_CONNECTION_FAILURE_IN_OKHTTP, false))

上述方法的前提都是,只有测试环境出现了问题,如果仅仅是一个环境就通过偏二方库的方式修改,影响太大;如果在应用层判断环境来处理,太麻烦,容易留坑。

服务器,柳暗花明又一村

因为只是一个环境出错,本地无法复现,那就只能在测试环境验证。但是任何代码修改都要经过各种流程才能上去,过于麻烦。

只好把视角转到环境与服务器上,期望能发现是服务器问题,仅通过修复特定服务器的配置来解决。

应用服务器

在开发环境和测试环境的服务器上,持续执行:netstat -an |grep 服务器IP

发现在请求 OBS 时,TCP 连接都是打开状态:

tcp6 0 0 172.160.0.132:56970 xxx.xxx.xxx.xxx:443 ESTABLISHED

但是请求完毕之后,开发环境会保持一段时间的打开状态,持续 30s 左右。

而测试环境的连接会直接进入 CLOSE_WAIT。如果这个时间触发调用,就会出现上述情况。

在问询了 DBA 之后,被告知 OBS 服务器只有一台,那我以为是应用服务器的配置不同,可能测试环境没开启 TCP Keepalive。

于是去找配置,然后啥也没有。

cat /proc/sys/net/ipv4/tcp_keepalive_time
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
cat /proc/sys/net/ipv4/tcp_keepalive_probes

不论是哪个环境都没有找到相关配置,也没有找到其他可能性配置,可能和用的是容器有关。

那只能猜测应该都是开启的,把思路转向 OBS 服务器。

OBS 服务器

因为之前拉 TCP 连接状态,发现都是 OBS 服务器主动关闭。这里可能问题在 OBS 服务器。

对比了一下几个环境的配置,发现其他环境都是公网域名,如果在服务器上 ping 的话,都是局域网 IP。

而测试环境的直接就是一个局域网域名,猜测可能是代理服务器。

找了 DBA 确定并处理了这个问题,上述问题就解决了。

不用改代码是极好的。

解决方案

所以总结一下上述流程发现的解决方案:

Okhttp

第二第三的方法有些人说不好使,具体看个人了。

  • 设置 retryOnConnectionFailure(true)
  • set header("Connection","close")
  • set header("Accept-Encoding", "identity")

方案来源:

  • android - java.io.EOFException: \n not found, while reading from BufferedReader - Stack Overflow
  • java.io.EOFException: \n not found: limit=0 content=… · Issue #5390 · square/okhttp · GitHub
SDK 封装

如果是 SDK 封装了 Okhttp,没有更改入口的话:

  • 自己拉源码改
  • 看是否有可以升级的版本做支持:像文章中的 hwcloud SDK OBS 可以升级到 3.20.1 以后
  • 上层捕获异常, 可能需要判断最深 cause 才能定位到异常. 之后进行重试(最不好用)

huaweicloud-sdk-java-obs/ExtObsConfiguration.java at c6bd01c1a57d4549d33c2d2396cd9ad33d17691c · huaweicloud/huaweicloud-sdk-java-obs · GitHub

服务器 or 应用

如果是自己的服务器或应用的话,可以支持一下 keepalive, 对齐一下连接池的存活时长和服务器或应用 keepalive 的时长。

当然,因为我没有直接接触到服务器端,所以可能思路给出来。具体解决方案可以自行研究,或者自行运维哦。

参考文档

  • huaweicloud-sdk-java-obs/ExtObsConfiguration.java at c6bd01c1a57d4549d33c2d2396cd9ad33d17691c · huaweicloud/huaweicloud-sdk-java-obs · GitHub
  • android - java.io.EOFException: \n not found, while reading from BufferedReader - Stack Overflow
  • java.io.EOFException: \n not found: limit=0 content=… · Issue #5390 · square/okhttp · GitHub
  • TCP Keepalive HOWTO (zzyongx.github.io)

你可能感兴趣的:(java,java,开发语言,后端)