记一次因HttpClient引起的定时任务运行失效的异常

记一次因HttpClient引起的定时任务运行失效的异常

    • 问题出现:
    • 解决过程:
      • 第一次修复
      • 第二次修复
  • 总结:

问题出现:

项目中有一个业务:有三个定时任务,任务功能是第三方接口发送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,错误日志没有相关内容,查看数据库没发现数据异常。于是将第三方相关接口重新作为一个单独项目运行,发现定时任务运行正常。

后检查代码,发现线上代码有以下问题:

  1. httpclient的get请求失败时未回收httpclient,后面使用该httpclient是都会失败

  2. @Async(value = "asyncServiceExecutor") 对于有返回值的方法,需使用 CompletableFuture<> 修饰返回值,但是,虽然会报错但是方法能运行,只是没有返回值。

于是做了一下修改:

  1. httpclient发送get和post请求后重置httpclient

  2. 从业务量和数据观察下来,并发造成定时任务失败的可能性不大,于是去掉@Async(value = "asyncServiceExecutor"),去掉

  3. 考虑到运行失败可能会是超时时间太短,导致未发送成功,于是设置了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篇文章:

  1. 一个隐藏在支付系统很长时间的雷
  2. 做支付遇到的HttpClient大坑

这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();
        }
    }

由于目前业务运行稳定,没发现运行失效的情况,暂不更改配置。后续若出现问题再进行配置。

总结下来:

  1. 对HttpClient的详细配置和原理不够了解和多线程编程不熟悉导致出现bug
  2. 测试不够,缺少压测
  3. 缺少详细报警记录,对于异常情况只能根据有限的错误日志、数据库数据和现有代码结合推测问题出在哪,没有记录请求超时失败等细节。

你可能感兴趣的:(性能优化,网络传输,HttpClient)