背景
公司大促活動流量上升,突然一線用戶反饋發消息特別慢,運維已經初步通過監控發現B服務接口大量超時,調用鏈如下圖。
發消息接口以前只經過A服務,后面為了防止客服罵人(我們是客服系統),接入了風控。因為產品下面好幾個模塊都要對接風控(外部門的),因此搞了一個公共服務(B服務)去對接風控,B服務本身沒有業務邏輯,基本上是純透傳。
當時業務明確,風控的邏輯耗時不能超過500ms,如果超過500ms則放通,當時這個DFX特性做在了B服務,代碼如下
@RequestMapping(xx)
public ServiceResponse<CheckRspVO> checkTextMsg(request) {Future<Result> aicFuture = hossTaskExecutor.submit(new Callable<Result>() {@Overridepublic Result call() throws Exception {return getAICResult(request);}});return aicFuture.get(500, TimeUnit.MILLISECONDS);
}private Result getAICResult(request) {log.info("rcService: taskId:[{}]",);HttpEntity<String> formEntity = buildHttpEntity<String>(request);try {ResponseEntity<AICRspVO> response = restTemplate.exchange(aicUrl, HttpMethod.POST, formEntity);log.info("rcService: From AIC [{}]");return response.getBody();} catch (RestClientException rtException) {log.error("rcService: Call RiskControl Center Failed: []");}
}
如上所述,B服務接口沒有業務邏輯,就是純透傳請求到風控。只不過為了保證接口500ms能夠返回,使用了異步線程池,并通過Future.get的方法實現500ms超時返回。
上述代碼去掉了一些參數轉換的代碼,整體架子就這些。
定位過程
首先通過監控能明確是風控邏輯導致的接口變慢,只不過不確定是我們自己(B服務),還是風控系統本身導致的。不過自己本能的還是認為是風控系統導致,因為我們本身服務器監控指標沒什么異常,而且B服務也只是透傳,自己沒啥計算量,不至于過載,另外正好出問題的當天有大促活動,業務量增多,很可能是風控系統本身過載了。
為了能把鍋甩出去,還是得找到實質證據。
從日志入手,確認風控有沒有報錯
從日志確實發現調用風控有報錯:“rcService: Call RiskControl Center Failed”
但是由于異常沒有打印出來,并不確認到底是什么問題。因此在仔細查閱一段時間的日志,確認報錯到底是少量報錯,還是大量報錯,試圖從其中找到一些有用信息。(這里要批評一下開發者,異常沒打印出來,導致問題不能快速的確認)。
分析日志的過程中,敏銳的發現一個異常點:getAICResult函數中第一行日志和異常日志,中間隔了5秒鐘,并且所有報錯2者都是隔了5秒鐘。5秒基本就是接口請求的耗時,因為從上面代碼可以看到,并無其他操作(參數組裝轉換的代碼沒貼在上面,但是這個基本不耗時時間)。
接口請求固定花費5秒,這個很像是設置請求超時時間導致的,因為尋找配置,確認到底是什么超時。
public class SpringRestTemplateConfig {// tcp三次握手完成時間@Value("${xx:5000}")private int connectTimeout;// 數據傳輸過程中數據包之間間隔的最大時間@Value("${xx:60000}")private int socketTimeout;
從配置項確認是連接超時,很可能是網絡不通,通過登錄使用telnet命令得以確認。
至此問題得以初步定位(后面找平臺確認是N天前,對網絡做了變更,部分區域恢復了防火墻)。
疑問
通過翻閱日志,發現報錯一致存在, 因此該問題一直有,為什么今天才爆發。
通過回顧代碼,發現已經通過Future.get保證了SLA,就算風控超時,也不會影響B服務及時返回,難道線程池設置的有問題?
解惑
查閱線程池配置如下
corePoolSize為4,workqueue大小4*corePoolSize,即線程池的核心線程,最大線程數量為4,任務隊列大小為16,線程拒絕策略為CallerRunsPolicy。
@Bean(name = xx)public ThreadPoolExecutor xx(TaskConfig taskConfig, ThreadFactory namedThreadFactory) {return new ThreadPoolExecutor(corePoolSize, corePoolSize, 0, TimeUnit.SECONDS, taskConfig.getWorkQueue(),namedThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy());}
從配置上來看,線程數設置的過小,大概率是線程池過載,然后拒絕策略又是在本線程執行(CallerRunsPolicy),導致過載的請求響應時間由0.5m變成了5秒(請求5秒超時)。
證明:
從上述代碼可以看到,只要能夠提交到異步線程處理的都能保證SLA(0.5s)
系統能夠保證SLA的QPS為 12個實例*0.8=9.6 一分鐘就是576
對于0.8的解釋:
因為異步線程的最大線程數為4,因此并發量最大為4,單個請求5秒才完成,因此每5秒能完成4個請求,那單個實例的QPS為4/5=0.8,單個實例的請求頻率一旦超過這個數,那么線程池就無法及時處理完,并放入任務隊列,最終任務隊列滿了后,在本線程執行,導致SLA無法得到滿足。
平時調用量統計
出問題當天調用量統計
當天超時接口統計
從上述可以看到,平時QPS也就400到500間,沒有超過576,線程池不會過載,0.5S的SLA能夠保證
出問題時(下午15:50分后)QPS逐漸超過576,線程池過載,問題開始出現。
因為請求的分布并不是絕對的平均(單個容器的QPS超過0.8了),所以15:50分開始,雖然中QPS沒有超過576,但是有少量請求還是變慢了。
總結
線程池的幾個參數意義我這里就不詳細介紹了,網上一大堆。
將請求第三方的邏輯,放到異步線程里面處理,還是比較常見的做法,比如某個接口要調用好幾個第三方,而且各自沒有依賴,會使用異步線程,同時提交對第三方系統的請求,以減小本接口的響應時間。請求第三方屬于IO密集型,本身并不會怎么消耗CPU,因此線程池的線程數可以設置大一點(比如200)。另外一個比較重要的參數就是線程池拒絕策略,一定要想好極端場景的兜底措施,對于本系統,風控邏輯并不重要,線程池超載后可以丟棄,因此可以自定義一個處理策略,丟棄配置打印日志就可以了。