记一个 httpclient 神坑

缘起

昨天下午看服务器,出现了一堆 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 个常用设置。

  1. new PoolingHttpClientConnectionManager(1, TimeUnit.MINUTES),在构造器中设置单个连接最大可用时长。
  2. cm.setMaxTotal(200);,设置整个连接池最大连接数。
  3. 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。

image.png

我看懂了,但也大受震撼。

缘灭。

速速找到运维,查看服务端域名的缓存配置。

lALPDgtYx9d1IurNApzNAjE_561_668.png

速查各配置意思。www.cnblogs.com/wshenjin/p/…

这下我是真的看不太懂了,但139121/1024 约等于 128,感觉 128k 那个字段特可疑,尝试着改了一下,重新 nginx 后,问题解决。

复杂的bug往往只需要简单的配置。

总结下,这种问题解决思路:

  1. 检查客户端是否提前关闭连接
  2. 检查服务端是否执行时长过长导致超时等
  3. 检查 nginx 相关配置
分类:
后端
标签: