问题出现:
项目中有一个业务:有三个定时任务,任务功能是第三方接口发送Http请求,定时任务设定为10分钟一次,随后发现这三个定时任务会在上午10点多停止运行一个多小时甚至更久。
解决过程:
第一次修复
考虑到之前是30分钟执行一次任务,现在是10分钟一次,可能会有并发问题。随后检查代码发现每次发送http请求都会new HttpClient(),这可能会导致tomcat的HttpClient资源被用完,而且还会占用大量内存。
于是,删掉部分HttpClient废弃的方法,新建NewSslHttpClient类,使用4.5版本新的实例方式。
将HttpClient设为成员变量:
private static CloseableHttpClient httpClient = HttpClientBuilder.create().build();
private static CloseableHttpClient sslHttpClient = NewSslHttpClient.create();
新增线程池类ThreadPoolExecutorConfig
,并把3个定时任务加上注解@Async(value = "asyncServiceExecutor")
于13日凌晨1点左右更新,当晚运行正常,13日全天运行正常。
14日凌晨异常,和第三方接口相关的定时任务都失效。
第二次修复
想尝试本地调试,后发现本地无法获取正式的token,错误日志没有相关内容,查看数据库没发现数据异常。于是将第三方相关接口重新作为一个单独项目运行,发现定时任务运行正常。
后检查代码,发现线上代码有以下问题:
-
httpclient的get请求失败时未回收httpclient,后面使用该httpclient是都会失败
-
@Async(value = "asyncServiceExecutor")
对于有返回值的方法,需使用CompletableFuture<>
修饰返回值,但是,虽然会报错但是方法能运行,只是没有返回值。
于是做了一下修改:
-
httpclient发送get和post请求后重置httpclient
-
从业务量和数据观察下来,并发造成定时任务失败的可能性不大,于是去掉
@Async(value = "asyncServiceExecutor")
,去掉 -
考虑到运行失败可能会是超时时间太短,导致未发送成功,于是设置了HttpClient连接超时时间,配置改为:
RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(8000).setConnectionRequestTimeout(8000) .setSocketTimeout(8000).build(); post.setConfig(requestConfig);
原配置:
RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000).setConnectionRequestTimeout(1000) .setSocketTimeout(5000).build(); post.setConfig(requestConfig);
第二次修复后,运行正常。
总结:
处理后查找相关资料时发现了2篇文章:
这2篇文章从实际案例出发,讲的很详细。
看完后,发现定时任务运行失效的核心原因是,定时任务已触发,请求已发送,由于网络抖动或者请求堵塞,导致请求没有发送到第三方方就已超时返回,导致本次任务失效。
同时根据这个文章,发现我的代码还有缺陷:没有配置HttpClient连接池,文章中写到:
这就是httpclient没有设置默认线程池的后果,赶快看看你们的代码是不是也有这个问题;
说到这边,有人说是因为连接池没有更改大小导致,其实是错误的,这个单独更改MaxTotal是不管用的,必须同时更改DefaultMaxPerRoute这个默认配置;
我们可以这样理解这两个参数,如果你访问的是一个域名,比如访问的是微信支付域名api.mch.weixin.qq.com,那么此时可以同时发起的请求受这两个参数影响。httpclient首先会从检查请求数是否超过DefaultMaxPerRoute,如果没有,则会再检查连接池中总连接数是否会超过MaxTotal大小。这两项都没有超过,才会新建立一个连接,反之则会等待连接池中其他线程释放。因此,同一时间向同一域名发起的总请求数<=DefaultMaxPerRoute<=MaxTotal;如果你使用httpclient不止向一个域名发起连接请求,那maxTotal会作为一个总的开关,来控制所有已经建立的网络连接数量;
还是上面的代码,如果想同时发起超过10个请求,就应该设置DefaultMaxPerRoute>10。代码(V5)如下:
public static void main(String argvs[]){ PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); // 总连接数 cm.setMaxTotal(200); // 这个至少要大于10 cm.setDefaultMaxPerRoute(20); CloseableHttpClient httpClient = HttpClientBuilder.create() .setConnectionManager(cm).build(); for(int i=0;i<10;i++) { new Thread(new Runnable() { @Override public void run() { GetRequest(httpClient); } }).start(); } }
由于目前业务运行稳定,没发现运行失效的情况,暂不更改配置。后续若出现问题再进行配置。
总结下来:
- 对HttpClient的详细配置和原理不够了解和多线程编程不熟悉导致出现bug
- 测试不够,缺少压测
- 缺少详细报警记录,对于异常情况只能根据有限的错误日志、数据库数据和现有代码结合推测问题出在哪,没有记录请求超时失败等细节。