記一次因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. 缺少詳細報警記錄,對於異常情況只能根據有限的錯誤日誌、數據庫數據和現有代碼結合推測問題出在哪,沒有記錄請求超時失敗等細節。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章