缘起
昨天下午看服务器,出现了一堆 500 的响应码。细看之。
throwable:org.apache.http.ConnectionClosedException: Premature end of Content-Length delimited message body (expected: 146,256; received: 139,121)
at org.apache.http.impl.io.ContentLengthInputStream.read(ContentLengthInputStream.java:178)
at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:135)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.Reader.read(Reader.java:140)
at org.apache.http.util.EntityUtils.toString(EntityUtils.java:227)
at org.apache.http.util.EntityUtils.toString(EntityUtils.java:308)
at com.zuhao.uhaozutool.base.CommonHttpClient.getHttpClientResult(CommonHttpClient.java:524)
at com.zuhao.uhaozutool.base.CommonHttpClient.doPost(CommonHttpClient.java:390)
at com.zuhao.uhaozutool.base.CommonHttpClient.doJsonPost(CommonHttpClient.java:216)
复制代码
错误比较常见,简单来说就是 Httpclient 在读取响应的时候,客户端/服务端 断开连接了。导致数据只读到了一般报错(expected: 146,256; received: 139,121)
。
解缘
于是检查代码中有没有提前关闭连接的代码,既要检查客户端也要检查服务端。
发现代码中压根就没有显性关闭连接的,而是用 PoolingHttpClientConnectionManager
统一管理。
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(1, TimeUnit.MINUTES);
cm.setMaxTotal(200);
cm.setDefaultMaxPerRoute(20);
defaultHttpClient = HttpClients.custom()
.setConnectionManager(cm).build();
复制代码
于是查找PoolingHttpClientConnectionManager
相关配置。只发现了 3 个常用设置。
new PoolingHttpClientConnectionManager(1, TimeUnit.MINUTES)
,在构造器中设置单个连接最大可用时长。cm.setMaxTotal(200);
,设置整个连接池最大连接数。cm.setDefaultMaxPerRoute(20);
,设置单个 host/域名 最大连接数。
不对呀我giao,压根没有设置,然后查看服务端代码,只是最基础的 springboot 项目,更不可能主动断开连接了。然后尝试了一系列改参数的设置,都无效。
于是重新观察日志,发现一个可疑的地方。
特喵的每次(expected: 146,256; received: 139,121)
,received 都是 139,121。这就离谱了,怎么每次接受的数据大小都一样??
于是转向思考,是不是那里设置到响应最大接受 size?找了一圈,也没有找到相关配置。
转念一想,不对呀,httpclient.excute
成功的,只是在EntityUtils.toString
的时候失败了。请求都执行成功了,为啥会在读取的时候失败呢?查看源码才发现,原来真正读取响应内容在EntityUtils.toString
中。当然了,这只是个小插曲。
private static String toString(
final HttpEntity entity,
final ContentType contentType) throws IOException {
final InputStream inStream = entity.getContent();
if (inStream == null) {
return null;
}
try {
Args.check(entity.getContentLength() <= Integer.MAX_VALUE,
"HTTP entity too large to be buffered in memory");
int capacity = (int)entity.getContentLength();
if (capacity < 0) {
capacity = DEFAULT_BUFFER_SIZE;
}
Charset charset = null;
if (contentType != null) {
charset = contentType.getCharset();
if (charset == null) {
final ContentType defaultContentType = ContentType.getByMimeType(contentType.getMimeType());
charset = defaultContentType != null ? defaultContentType.getCharset() : null;
}
}
if (charset == null) {
charset = HTTP.DEF_CONTENT_CHARSET;
}
final Reader reader = new InputStreamReader(inStream, charset);
final CharArrayBuffer buffer = new CharArrayBuffer(capacity);
final char[] tmp = new char[1024];
int l;
while((l = reader.read(tmp)) != -1) {
buffer.append(tmp, 0, l);
}
return buffer.toString();
} finally {
inStream.close();
}
}
复制代码
此时在 Google 上看到个兄弟表示,他遇到个类似的问题,是因为 nginx 配置的缓存区放不下整个响应 body,nginx 启用的用户又没有在目录的写入权限,导致每次超过的 body,都只返回缓存区大小的 body。
我看懂了,但也大受震撼。
缘灭。
速速找到运维,查看服务端域名的缓存配置。
速查各配置意思。www.cnblogs.com/wshenjin/p/…
这下我是真的看不太懂了,但139121/1024 约等于 128,感觉 128k 那个字段特可疑,尝试着改了一下,重新 nginx 后,问题解决。
复杂的bug往往只需要简单的配置。
总结下,这种问题解决思路:
- 检查客户端是否提前关闭连接
- 检查服务端是否执行时长过长导致超时等
- 检查 nginx 相关配置