当前位置 博文首页 > 少年时未觉悟 ,觉悟时不再年少,还危机四伏!:HttpClient下载
CVE 的英文全称是“Common Vulnerabilities & Exposures” 通用漏洞披露,官网会定期纰漏最新漏洞信息,有一个 Java 定时任务会定期下载最新漏洞文件 allitems.csv.Z
。
偶发的问题是:压缩文件不大,但是国外网站加网络不稳定,任务运行时偶尔能够下载成功,其他时候就卡在读取响应流方法那里。
这个技术债一直拖着,昨天下定决心跟踪,抓到了堆栈日志,果然是数据读取的问题。定时任务卡在文件下载的地方,文件不是很大,是一个压缩文件,三十多兆。
首先,打印堆栈日志。 Java 提供了很多查看线程堆栈信息的工具,这里使用 jcmd
,操作步骤如下:
第一步,定位 Tomcat 进程编号 15617 :jps grep Bootstrap
第二步,打印堆栈信息:jcmd 15617 Thread.print > /opt/jcmdstack.log
查看 jcmdstack.log
文件内容,抓到了文件下载操作所处的线程信息是这段:
"quartzScheduler_Worker-2" #19 prio=5 os_prio=0 tid=0x00007f45f9200800 nid=0x3d33 runnable [0x00007f46212bb000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:170)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:139)
at org.apache.http.impl.io.SessionInputBufferImpl.read(SessionInputBufferImpl.java:200)
at org.apache.http.impl.io.ContentLengthInputStream.read(ContentLengthInputStream.java:178)
at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:137)
at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:150)
at xxx.download(XXXUtil.java:xxx)
其次,定位到 download
所在的方法。逐行分析下载代码,大致流程没问题,就是缺少了超时配置。
文件下载就是将 HttpEntity
的响应流输出到本地文件,为 HttpGet
实例设置各类超时配置,代码如下:
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
/**
* 根据url下载文件,保存到filepath中
*/
public static void download(String url, String filepath) {
if(url == null || filepath == null) {
return ;
}
int cache = 200 * 1024;
try {
HttpClient client = HttpClients.createDefault();
HttpGet httpget = new HttpGet(url);
// 设置超时时间,解决读取响应数据卡死的问题,时间足够长,如一小时
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(3600000)
.setConnectTimeout(3600000)
.setSocketTimeout(3600000).build();
httpget.setConfig(requestConfig);
// 执行 http 请求,获取响应流
HttpResponse response = client.execute(httpget);
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
File file = new File(filepath);
file.getParentFile().mkdirs();
FileOutputStream fileout = new FileOutputStream(file);
byte[] buffer = new byte[cache];
int ch = 0;
// 读取响应内容到本地文件流
while ((ch = is.read(buffer)) != -1) {
fileout.write(buffer, 0, ch);
}
is.close();
fileout.flush();
fileout.close();
EntityUtils.consume(entity);
logger.info("Finished download successfully!");
} catch (Exception e) {
logger.error("Download file error!",e);
}
}
温故下三个超时的含义,根据 API 可知:
setConnectionRequestTimeout
:从连接管理池中获取连接的超时时间,这个是 HttpClient
连接池管理的参数,未设置且没有可用连接时将导致无限等待;setConnectTimeout
:与服务器建立链接的超时时间,网站不挂的话,基本OK;setSocketTimeout
:从服务器获取响应数据的超时时间,a maximum period inactivity between two consecutive data packets
。这个是关键指标,指在等待读取数据时两次连续数据包之间等待的最大时间间隔。为何 HttpClient
工具下载文件操作不设置超时时间,就容易出现程序假死的问题呢?堆栈显示下载线程虽处于 RUNNABLE
运行状态的,但是下载流程就没有继续了。怎么逻辑自洽地解释这个超时未设置时任务卡住的现象呢?
可以确定的是 setSocketTimeout
数据读取超时时间未设置时,国外网站,公司网络又很慢的情况下,两次接收数据包之间,如果服务器端没有数据返回,socketRead0
这个底层方法它肯定去傻傻等待了,至于这个方法有没有阻塞逻辑,navtive 代码也不好定论。
为什么后面服务器端一直都没有最新数据传输过来呢?推断可能是服务器断掉了这个连接请求,不再继续发送数据了,而客户端还卡在读取的地方。设置超时时间,一个小时后,如果依然无数据,该轮任务就能退出,不会影响下一轮的定时调度逻辑。
由于目标网站位于国外,总出现 Readtimeout
异常,运气好能成功下载的次数就一两次。添加超时配置后,定时任务重新跑了十几次,但没有再出现任务卡住的问题。