java.io.EOFException: \n not found: limit=0 content=…
起因是项目向华为云 OBS 请求视频, 偶现题目中的异常;如果等一段时间(30s),就可以正常读取到视频。
原因是 OBS SDK 底层用的 Okhttp 连接池,而 Okhttp 的 GitHub 上也有相关 issue:
解决的方法很多,有生硬的,有灵活的;可以改上层代码,可以动二方库的包。
但是因为我们明明所有环境都连一个 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。
大致原因和开头说的一样:
当然,大家可能都是希望可以提前发现已经被关闭的连接,去主动使用打开的 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 了,根本没有在外部配置的可能性。兜兜转转又回到了起点。
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 以后,扩展了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 服务器。
因为之前拉 TCP 连接状态,发现都是 OBS 服务器主动关闭。这里可能问题在 OBS 服务器。
对比了一下几个环境的配置,发现其他环境都是公网域名,如果在服务器上 ping 的话,都是局域网 IP。
而测试环境的直接就是一个局域网域名,猜测可能是代理服务器。
找了 DBA 确定并处理了这个问题,上述问题就解决了。
不用改代码是极好的。
所以总结一下上述流程发现的解决方案:
第二第三的方法有些人说不好使,具体看个人了。
retryOnConnectionFailure(true)
header("Connection","close")
header("Accept-Encoding", "identity")
方案来源:
如果是 SDK 封装了 Okhttp,没有更改入口的话:
huaweicloud-sdk-java-obs/ExtObsConfiguration.java at c6bd01c1a57d4549d33c2d2396cd9ad33d17691c · huaweicloud/huaweicloud-sdk-java-obs · GitHub
如果是自己的服务器或应用的话,可以支持一下 keepalive, 对齐一下连接池的存活时长和服务器或应用 keepalive 的时长。
当然,因为我没有直接接触到服务器端,所以可能思路给出来。具体解决方案可以自行研究,或者自行运维哦。