問題出現:
項目中有一個業務:有三個定時任務,任務功能是第三方接口發送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
- 測試不夠,缺少壓測
- 缺少詳細報警記錄,對於異常情況只能根據有限的錯誤日誌、數據庫數據和現有代碼結合推測問題出在哪,沒有記錄請求超時失敗等細節。