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 可知:

  1. setConnectionRequestTimeout:从连接管理池中获取连接的超时时间,这个是 HttpClient 连接池管理的参数,未设置且没有可用连接时将导致无限等待
  2. setConnectTimeout:与服务器建立链接的超时时间,网站不挂的话,基本OK;
  3. setSocketTimeout :从服务器获取响应数据的超时时间,a maximum period inactivity between two consecutive data packets 。这个是关键指标,指在等待读取数据时两次连续数据包之间等待的最大时间间隔。

启示录

为何 HttpClient 工具下载文件操作不设置超时时间,就容易出现程序假死的问题呢?堆栈显示下载线程虽处于 RUNNABLE 运行状态的,但是下载流程就没有继续了。怎么逻辑自洽地解释这个超时未设置时任务卡住的现象呢?

可以确定的是 setSocketTimeout 数据读取超时时间未设置时,国外网站,公司网络又很慢的情况下,两次接收数据包之间,如果服务器端没有数据返回,socketRead0 这个底层方法它肯定去傻傻等待了,至于这个方法有没有阻塞逻辑,navtive 代码也不好定论。

为什么后面服务器端一直都没有最新数据传输过来呢?推断可能是服务器断掉了这个连接请求,不再继续发送数据了,而客户端还卡在读取的地方。设置超时时间,一个小时后,如果依然无数据,该轮任务就能退出,不会影响下一轮的定时调度逻辑。

由于目标网站位于国外,总出现 Readtimeout 异常,运气好能成功下载的次数就一两次。添加超时配置后,定时任务重新跑了十几次,但没有再出现任务卡住的问题。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值