聊一次線程池使用不當(dāng)導(dǎo)致的生產(chǎn)故障
一、搶救
2023 年 10月 27 日,是一個(gè)風(fēng)和日麗的周五,我正在開(kāi)車(chē)上班的路上。難得不怎么堵車(chē),原本心情還是很不錯(cuò)的。可時(shí)間來(lái)到 08:50 左右,飛書(shū)突然猛烈的彈出消息、告警電話響起,輕松的氛圍瞬間被打破。我們的一個(gè)核心應(yīng)用 bfe-customer-application-query-svc 的 RT 飆升,沒(méi)過(guò)一會(huì)兒,整個(gè) zone-2[1]陷入不可用的狀態(tài)。隨后便是緊張的應(yīng)急處置,限流、回退配置、擴(kuò)容…宛如搶救突然倒地不醒的病患,CPR、AED 除顫、腎上腺素靜推… 最終經(jīng)過(guò)十幾分鐘的努力,應(yīng)用重新上線,zone-2 完全恢復(fù)。
二、診斷病因
“病人” 是暫時(shí)救回來(lái)了,但病因還未找到,隨時(shí)都有可能再次陷入危急。到公司后,我便馬不停蹄的開(kāi)始定位本次故障的根因。
好消息是,在前面應(yīng)急的過(guò)程中,已經(jīng)掌握了不少有用的信息:
1.bfe-customer-application-query-svc 是近期上線的新應(yīng)用,故障發(fā)生時(shí)每個(gè) zone 僅有 2 個(gè) Pod 節(jié)點(diǎn);
2.故障發(fā)生的前一天晚上,剛剛進(jìn)行了確認(rèn)訂單頁(yè)面二次估價(jià)接口(以下稱(chēng)作 confirmEvaluate)的切流(50% → 100%),故障發(fā)生時(shí),該接口的 QPS 是前一天同時(shí)段的約 2 倍,如圖1;
3.初步判斷,故障可能是 confirmEvaluate 依賴(lài)的一個(gè)下游接口(以下稱(chēng)作 getTagInfo)超時(shí)抖動(dòng)引起的(RT 上漲到了 500ms,正常情況下 P95 在 10ms 左右,持續(xù)約 1s);
圖 1:confirmEvaluate 故障當(dāng)日 QPS(zone-1 和 zone-2 流量之和)
綜合這些信息來(lái)看,Pod 節(jié)點(diǎn)數(shù)過(guò)少 + 切流導(dǎo)致流量增加 + 依賴(lài)耗時(shí)突發(fā)抖動(dòng),且最終通過(guò)擴(kuò)容得以恢復(fù),這疊滿的 buff, 將故障原因指向了 “容量不足”。
但這只能算是一種定性的判斷,就好比說(shuō)一個(gè)人突然倒地不醒是因?yàn)樾呐K的毛病,但心臟(心血管)的疾病很多,具體是哪一種呢?在計(jì)算機(jī)科學(xué)的語(yǔ)境中提到 “容量” 這個(gè)詞,可能泛指各種資源,例如算力(CPU)、存儲(chǔ)(Mem) 等硬件資源,也可能是工作線程、網(wǎng)絡(luò)連接等軟件資源,那么,這次故障究竟是哪個(gè)或哪些資源不足了呢?還需要更細(xì)致的分析,才能解答。
2.1 初步定位異常指征:tomcat 線程池處理能力飽和,任務(wù)排隊(duì)
通過(guò) Pod 監(jiān)控,輕易就能排除算力、存儲(chǔ)等硬件資源耗盡的可能性[2]。該應(yīng)用不依賴(lài)數(shù)據(jù)庫(kù),并未使用連接池來(lái)處理同步 I/O也不太可能是連接池耗盡[3]。因此,最有可能還是工作線程出了問(wèn)題。
我們使用 tomcat 線程池來(lái)處理請(qǐng)求,工作線程的使用情況可以通過(guò) “tomcat 線程池監(jiān)控” 獲得。如圖2、圖3,可以看到,Pod-1(..186.8)在 08:54:00 線程池的可用線程數(shù)(Available Threads)已經(jīng)到了最大值(Max Threads)[4],Pod-2(..188.173)則是在更早的 08:53:30 達(dá)到最大值。
圖 2:tomcat 線程池使用情況(*.*.186.8)
圖 3:tomcat 線程池使用情況(*.*.188.173)
為了更好的理解曲線變化的含義,我們需要認(rèn)真分析一下 tomcat 線程池的擴(kuò)容邏輯。
不過(guò)在這之前,先明確一下兩個(gè)監(jiān)控指標(biāo)和下文將要討論的代碼方法名的映射關(guān)系:
圖片
實(shí)際上, tomcat線程池(org.apache.tomcat.util.threads.ThreadPoolExecutor)繼承了java.util.concurrent.ThreadPoolExecutor,而且并沒(méi)有重寫(xiě)線程池?cái)U(kuò)容的核心代碼,而是復(fù)用了java.util.concurrent.ThreadPoolExecutor#execute方法中的實(shí)現(xiàn),如圖4。其中,第4行~第23行的代碼注釋?zhuān)呀?jīng)將這段擴(kuò)容邏輯解釋的非常清晰。即每次執(zhí)行新任務(wù)(Runnable command)時(shí):
- Line 25 - 26:首先判斷當(dāng)前池中工作線程數(shù)是否小于 corePoolSize,如果小于 corePoolSize 則直接新增工作線程執(zhí)行該任務(wù);
- Line 30:否則,嘗試將當(dāng)前任務(wù)放入 workQueue;
- Line 37:如果第 30 行未能成功將任務(wù)放入 workQueue,即workerQueue.offer(command)返回 false,則繼續(xù)嘗試新增工作線程執(zhí)行該任務(wù)(第 37 行);圖 4:tomcat 線程池?cái)U(kuò)容實(shí)現(xiàn)
比較 Tricky 的地方在于 tomcat 定制了第 30 行 workQueue 的實(shí)現(xiàn),代碼位于org.apache.tomcat.util.threads.TaskQueue類(lèi)中。TaskQueue 繼承自 LinkedBlockingQueue,并重寫(xiě)了offer方法,如圖5??梢钥吹剑?/p>
- Line 4:當(dāng)線程池中的工作線程數(shù)已經(jīng)達(dá)到最大線程數(shù)時(shí),則直接將任務(wù)放入隊(duì)列;
- Line 6:否則,如果線程池中的工作線程數(shù)還未達(dá)到最大線程數(shù),當(dāng)提交的任務(wù)數(shù)(parent.getSubmittedCount())小于池中工作線程數(shù),即存在空閑的工作線程時(shí),將任務(wù)放入隊(duì)列。這種情況下,放入隊(duì)列的任務(wù),理論上將立刻被空閑的工作線程取出并執(zhí)行;
- Line 8:否則,只要當(dāng)前池中工作線程數(shù)沒(méi)有達(dá)到最大值,直接返回false。此時(shí)圖 4第30行workQueue.offer(command) 就將返回false,這會(huì)導(dǎo)致execute方法執(zhí)行第37行的addWorker(command, false),對(duì)線程池進(jìn)行擴(kuò)容;
圖 5:tomcat TaskQueue offer() 方法實(shí)現(xiàn)
通過(guò)分析這兩段代碼,得到如圖6的 tomcat 線程池?cái)U(kuò)容流程圖。
圖 6:tomcat 線程池?cái)U(kuò)容邏輯流程圖
歸納一下,可以將 tomcat 線程池的擴(kuò)容過(guò)程拆分為兩個(gè)階段。
圖片
可見(jiàn),一旦進(jìn)入到 “階段 2”,線程池的處理能力就無(wú)法及時(shí)消費(fèi)新到達(dá)的任務(wù),于是這些任務(wù)就會(huì)開(kāi)始在線程池隊(duì)列(workQueue)中排隊(duì),甚至積壓。而結(jié)合 圖2、圖3,我們可以得出結(jié)論,兩個(gè) Pod 先后都進(jìn)入了 “階段 2” 飽和狀態(tài),此時(shí)必然已經(jīng)出現(xiàn)了任務(wù)的排隊(duì)。對(duì) tomcat 線程池來(lái)說(shuō),“任務(wù)” 這個(gè)詞指的就是接收到的外部請(qǐng)求。
2.2 線程池處理能力飽和的后果:任務(wù)排隊(duì)導(dǎo)致探活失敗,引發(fā) Pod 重啟
當(dāng)前,該應(yīng)用基于默認(rèn)配置,使用 SpringBoot 的健康檢查端口(Endpoint),即actuator/health,作為容器的存活探針。而問(wèn)題就有可能出在這個(gè)存活探針上,k8s 會(huì)請(qǐng)求應(yīng)用的actuator/health端口,而這個(gè)請(qǐng)求也需要提交給 tomcat 線程池執(zhí)行。
設(shè)想如圖7中的場(chǎng)景。Thread-1 ~ Thread-200 因處理業(yè)務(wù)請(qǐng)求而飽和,工作隊(duì)列已經(jīng)積壓了一些待處理的業(yè)務(wù)請(qǐng)求(待處理任務(wù) R1 ~ R4),此時(shí) k8s 發(fā)出了探活請(qǐng)求(R5),但只能在隊(duì)列中等待。對(duì)于任務(wù) R5 來(lái)說(shuō),最好的情況是剛剛放入工作隊(duì)列就立刻有5個(gè)工作線程從之前的任務(wù)中釋放出來(lái),R1~R5 被并行取出執(zhí)行,探活請(qǐng)求被立即處理。但線程池因業(yè)務(wù)請(qǐng)求飽和時(shí),并不存在這種理想情況。通過(guò)前文已經(jīng)知道,confirmEvaluate 的耗時(shí)飆升到了 5s,排在 R5 前面的業(yè)務(wù)請(qǐng)求處理都很慢。因此比較壞的情況是,R5 需要等待 R1、R2、R3、R4 依次執(zhí)行完成才能獲得工作線程,將在任務(wù)隊(duì)列中積壓 20s(), 甚至更久。
圖 7:在任務(wù)隊(duì)列中等待的探活請(qǐng)求
而 bfe-customer-application-query-svc 的探活超時(shí)時(shí)間設(shè)置的是 1s,一旦發(fā)生積壓的情況,則大概率會(huì)超時(shí)。此時(shí) k8s 會(huì)接收到超時(shí)異常,因此判定探活失敗。我在 《解決因主庫(kù)故障遷移導(dǎo)致的核心服務(wù)不可用問(wèn)題》一文中討論過(guò)探活失敗導(dǎo)致 Pod 重啟引發(fā)雪崩的問(wèn)題,看來(lái)這次故障,又是如出一轍。
果然,如圖8、圖9,通過(guò)查閱 Pod 事件日志,我發(fā)現(xiàn) Pod-1(..186.8)在 08:53:59 記錄了探活失敗,隨后觸發(fā)了重啟,Pod-2(..188.173)則是在 08:53:33 記錄了探活失敗,隨后也觸發(fā)了重啟。而這兩個(gè)時(shí)間正是在上文提到的 “線程池達(dá)到飽和" 的兩個(gè)時(shí)間點(diǎn)附近(Pod-1 08:54:00 和 Pod-2 00:53:30)。
由于 zone-2 僅有 2 個(gè) Pod,當(dāng) Pod-1 和 Pod-2 陸續(xù)重啟后,整個(gè) zone-2便沒(méi)有能夠處理請(qǐng)求的節(jié)點(diǎn)了,自然就表現(xiàn)出完全不可用的狀態(tài)。
圖 8:探活失敗
圖 9:探活失敗
但為什么只有 zone-2 會(huì)整個(gè)不可用呢?于是,我又專(zhuān)門(mén)對(duì)比了 zone-1 的兩個(gè) Pod,如 圖10到圖13。從圖10、圖11可以看到,zone-1 的兩個(gè) Pod 在下游依賴(lài)抖動(dòng)時(shí)也發(fā)生了類(lèi)似 zone-2 的 tomcat 線程池?cái)U(kuò)容,不同之處在于,zone-1 兩個(gè) Pod 的線程池都沒(méi)有達(dá)到飽和。從圖12、圖13也可以看到,zone-1 的兩個(gè) Pod 在 八點(diǎn)五十分前后這段時(shí)間內(nèi),沒(méi)有任何探活失敗導(dǎo)致重啟的記錄。
圖 10:tomcat 線程池使用情況
圖 11:tomcat 線程池使用情況
圖 12:探活成功
圖 13:探活成功
顯然, zone-1 并沒(méi)有置身事外,同樣受到了耗時(shí)抖動(dòng)的影響,同樣進(jìn)行了線程池?cái)U(kuò)容。但可用線程仍有余量,因此并沒(méi)有遇到 zone-2 探活失敗,進(jìn)而觸發(fā) Pod 重啟的問(wèn)題。
2.3 深度檢查:尋找線程池處理能力惡化的根因
現(xiàn)在,已經(jīng)明確了 “tomcat 線程池飽和” 是導(dǎo)致這次容量問(wèn)題的關(guān)鍵,但線程池為什么會(huì)飽和,還需要繼續(xù)尋找原因。結(jié)合應(yīng)急處置過(guò)程中獲得的 #3 線索和過(guò)往經(jīng)驗(yàn),首先浮現(xiàn)在我腦中的推論是:SOA 調(diào)用下游 getTagInfo 耗時(shí)增加,導(dǎo)致 tomcat 線程陸續(xù)陷入等待,最終耗盡了所有可用線程。
2.3.1 與推論相矛盾的關(guān)鍵證據(jù):`WAITING`狀態(tài)線程數(shù)飆升
自從開(kāi)始寫(xiě)這個(gè)系列文章,線程就是 “常駐嘉賓”??催^(guò)該系列前面幾篇文章的讀者,此時(shí)應(yīng)該想到,既然懷疑是線程等待的問(wèn)題,高低是要調(diào)查下線程狀態(tài)機(jī)的。
以下圖14、圖15是 zone-2 兩個(gè) Pod 在故障時(shí)段的線程狀態(tài)監(jiān)控曲線(X 軸為時(shí)間,Y 軸為線程的數(shù)量)。
圖 14:線程狀態(tài)
圖 15:線程狀態(tài)
可以看到,不論 Pod-1(..186.8)還是 Pod-2(..188.173)在故障開(kāi)始之初(08:52:0030s),都是WAITING狀態(tài)的線程顯著飆升。這有點(diǎn)出乎意料,因?yàn)槿绻_(kāi)頭的推測(cè)成立,下游 getTagInfo 耗時(shí)增加,線程的波動(dòng)應(yīng)該體現(xiàn)為T(mén)IMED_WAITING 狀態(tài)的線程數(shù)飆升。
2.3.2 是推論站不住腳,還是錯(cuò)誤理解了證據(jù)?深度溯源框架代碼,撥開(kāi)迷霧
感到意外也不是空穴來(lái)風(fēng),下方圖16是通過(guò) jstack 捕獲的基于公司自研 SOA 框架(以下稱(chēng)作 hermes)的一個(gè)典型 tomcat 線程調(diào)用棧。其中,第 9 行 ~ 第 11 行棧信息清晰提示了一次 SOA 調(diào)用的執(zhí)行過(guò)程。從圖中第 2 行可以看到,此時(shí)線程此時(shí)處于TIMED_WAITING狀態(tài)。
圖 16:一個(gè)典型的 Tomcat 線程調(diào)用棧
但為了防止先入為主,代入錯(cuò)誤的假設(shè)進(jìn)行調(diào)查,我還是決定重新確認(rèn)一遍 hermes 框架中執(zhí)行 SOA 調(diào)用的代碼實(shí)現(xiàn)。圖16中的第11行,cn.huolala.arch.hermes.compatible.protocol.invoker.ConcreteInvocationInvoker#invoke是 SOA 調(diào)用的入口方法。該方法的核心代碼如圖17,其中最重要的是第8行和第12行。
- Line 8:調(diào)用ConcreteInvocationInvoker類(lèi)的另一個(gè)doInvoke方法獲得response。真正發(fā)起調(diào)用的邏輯其實(shí)都是在這個(gè)doInvoke方法里實(shí)現(xiàn)的;
- Line 12:調(diào)用 response的recreate()方法,并將結(jié)果包裝為result。由于省略了非核心代碼,Line 12 實(shí)際上就是線程調(diào)用棧中提示的的ConcreteInvocationInvoker.java 71行的代碼;
圖 17:ConcreteInvocationInvoker#invoke 方法定義
而 doInvoke方法做的最核心的事情,就是將請(qǐng)求交給 asyncClient 執(zhí)行,這里asyncClient是org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient的實(shí)例(在 hermes 框架中,SOA 請(qǐng)求使用的低級(jí)通信協(xié)議是 HTTP)。代碼如圖18中第4行~第24行。
- Line 4:執(zhí)行 asyncClient.execute 方法,發(fā)起 HTTP 請(qǐng)求。execute方法有三個(gè)參數(shù),此處只需要重點(diǎn)關(guān)注第三個(gè)參數(shù)。由于是異步客戶(hù)端,調(diào)用 execute方法將立刻返回,待請(qǐng)求執(zhí)行完成后,asyncClient 將通過(guò)第三個(gè)參數(shù)注冊(cè)的回調(diào)(callback)邏輯,通知調(diào)用方執(zhí)行結(jié)果?;卣{(diào)邏輯必須實(shí)現(xiàn) org.apache.hc.core5.concurrent.FutureCallback接口,實(shí)際上就是自定義 3 個(gè)方法:a. void completed(T result):HTTP 請(qǐng)求執(zhí)行成功,如何處理結(jié)果(response);b .void failed(Exception ex):請(qǐng)求執(zhí)行失敗,如何處理異常;c. void cancelled():如何處理請(qǐng)求取消;
- Line 11:我們重點(diǎn)關(guān)注正向流程,即 completed方法的實(shí)現(xiàn)。實(shí)際上就是解析(unpack) HTTP 的響應(yīng)結(jié)果,然后調(diào)用 response.succeeded(result);
圖 18:ConcreteInvocationInvoker#doInvoke 方法定義
兩段代碼中的 response都是 cn.huolala.arch.hermes.compatible.protocol.handler.JsonRpcResponse的實(shí)例,它的實(shí)現(xiàn)如圖19,重點(diǎn)關(guān)注圖17中用到的 recreate 方法和圖18中用到的 succeeded 方法在 JsonRpcResponse 中是如何定義的。我們會(huì)發(fā)現(xiàn),JsonRpcResponse本質(zhì)上就是對(duì) CompletableFuture的一層包裝:
- Line 5:一上來(lái)就綁定了一個(gè) future 對(duì)象;
- Line 11:recreate 方法的核心,就是在 future 對(duì)象的 get 方法上等待;
- Line 22:succeeded方法的核心,就是將值(從 HTTP 響應(yīng)中解析出的調(diào)用結(jié)果)寫(xiě)入到 future;
圖 19:JsonRpcResponse 定義
為了更方便理解,我將圖17~圖19 的三段代碼整理為了如圖20的序列圖。通過(guò)序列圖可以更清晰的觀察到,調(diào)用過(guò)程分為 “發(fā)送請(qǐng)求” 和 “等待回調(diào)” 兩個(gè)階段。在發(fā)送請(qǐng)求階段,asyncClient 將 I/O 任務(wù)注冊(cè)到內(nèi)部事件循環(huán)后就立刻返回[8],tomcat 線程在這一階段不存在任何等待。唯一會(huì)導(dǎo)致線程等待的,是在回調(diào)階段的第 11 步 future.get(Integer.MAX_VALUE, TimeUnit.MILLISECONDS)調(diào)用。但調(diào)用 get 方法時(shí)指定了超時(shí)時(shí)間(Integer.MAX_VALUE 毫秒),線程將進(jìn)入 TIMED_WAITING 狀態(tài)。
圖 20:客戶(hù)端發(fā)起 SOA 調(diào)用序列圖
更進(jìn)一步,將 asyncClient 的內(nèi)部實(shí)現(xiàn)展開(kāi),完整的 “線程池 + 連接池” 模型如圖21:
圖 21:SOA 調(diào)用客戶(hù)端線程池、連接池模型
- 在 asyncClient 內(nèi)部,專(zhuān)門(mén)定義了用于處理網(wǎng)絡(luò) I/O 的工作線程,即 IOReactorWorker Thread。注意它并不是一個(gè)線程池,僅僅是多個(gè)獨(dú)立的 IOReactorWorker 線程,因此 IOReactorWorker 并不會(huì)動(dòng)態(tài)擴(kuò)容,線程數(shù)量是在asyncClient 初始化時(shí)就固定下來(lái)的。當(dāng)請(qǐng)求被提交給 asyncClient 時(shí),會(huì)根據(jù)一個(gè)特定的算法選擇其中一個(gè) IOReactorWorker 線程進(jìn)行接收;
- 每個(gè) IOReactorWorker 線程內(nèi)部都綁定了一個(gè)請(qǐng)求隊(duì)列(requestQueue) 和 一個(gè)該線程獨(dú)享的 Selector,線程啟動(dòng)后,即開(kāi)啟 Selector 事件循環(huán),代碼如圖22;
- 在 tomcat 線程中,SOA 調(diào)用最終會(huì)被包裝成一個(gè) HTTP 請(qǐng)求并投遞到某個(gè) IOReactorWorker 線程的請(qǐng)求隊(duì)列里。隨后 IOReactorWorker 在 Selector 事件循環(huán)中會(huì)不斷的取出(poll)隊(duì)列中的請(qǐng)求,包裝為 Channel 注冊(cè)到 Selector 循環(huán)中,即圖22中代碼的第19行。請(qǐng)求提交給 asyncClient 后,tomcat 線程就將在 future.get(Integer.MAX_VALUE, TimeUnit.MILLISECONDS)上等待回調(diào);
- IOReactorWorker 會(huì)從連接池中取出(lease)一個(gè)連接并執(zhí)行請(qǐng)求。注意 asyncClient 的連接池管理方式比較特別,是根據(jù) route(即不同的 host+port/domain 組合)進(jìn)行分組的,每個(gè) route 有一個(gè)獨(dú)立的連接池;
- 請(qǐng)求執(zhí)行完成后,IOReactorWorker 會(huì)通過(guò) future.complete(value)將請(qǐng)求結(jié)果回寫(xiě)給 tomcat 線程,此時(shí)第 3 步中的get方法調(diào)用將返回;
圖 22:IOReactorWorker 線程內(nèi)部的事件循環(huán)核心邏輯
通過(guò)上面的分析可以得出結(jié)論:一次標(biāo)準(zhǔn)的 SOA 調(diào)用,并不會(huì)導(dǎo)致 tomcat 線程進(jìn)入 WAITING 狀態(tài)。
- 雖然 tomcat 線程確實(shí)會(huì)在 HTTP 請(qǐng)求執(zhí)行完成前,一直等待 future.get(Integer.MAX_VALUE, TimeUnit.MILLISECONDS) 返回,隨著 SOA 調(diào)用耗時(shí)增加,等待的時(shí)間也會(huì)更久,從而導(dǎo)致可用線程逐漸耗盡。但正如最初的設(shè)想一樣,此時(shí)應(yīng)該 TIMED_WAITING狀態(tài)的線程數(shù)飆升,與實(shí)際監(jiān)控曲線的特征不符;
- 此外,IOReactorWorker 線程的嫌疑也可以排除。一是因?yàn)镮OReactorWorker線程的數(shù)量在初始化 asyncClient 的時(shí)候就已經(jīng)固定了下來(lái),不會(huì) “飆升”。二是不論this.selector.select(this.selectTimeoutMillis)(如圖22,第5行)還是讀寫(xiě) I/O,線程狀態(tài)都是 RUNNABLE,而非 WAITING。
2.3.3 弄巧成拙的業(yè)務(wù)代碼
既然已經(jīng)意識(shí)到 WAITING 狀態(tài)線程數(shù)飆升的事實(shí),不如基于這個(gè)事實(shí)給問(wèn)題代碼做個(gè)畫(huà)像。那么,什么樣的代碼可能會(huì)導(dǎo)致線程進(jìn)入WAITING狀態(tài)呢?
還是在 《解決因主庫(kù)故障遷移導(dǎo)致的核心服務(wù)不可用問(wèn)題》 一文中(3.1 節(jié)),我曾詳細(xì)分析了WAITING和 TIMED_WAITING 狀態(tài)的區(qū)別。線程進(jìn)入WAITING 或 TIMED_WAITING狀態(tài),通常是因?yàn)榇a中執(zhí)行了如下表格列出的方法。
圖片
或者,也有可能是執(zhí)行了將這些方法進(jìn)行過(guò)上層包裝后的方法。例如,
- 當(dāng)執(zhí)行 CompletableFuture 實(shí)例的 future.get(timeout, unit)方法時(shí),在 CompletableFuture 的底層實(shí)現(xiàn)中,最終會(huì)調(diào)用`LockSupport.parkNanos(o, timeout)方法;
- 而如果執(zhí)行 CompletableFuture 實(shí)例的 future.get()方法(不指定超時(shí)時(shí)間),則最終會(huì)調(diào)用 LockSupport.park(o) 方法;
因此,執(zhí)行 future.get(timeout, unit)方法將導(dǎo)致線程進(jìn)入 TIMED_WAITING狀態(tài)(正如前文討論的框架代碼一樣),而執(zhí)行 future.get()方法則會(huì)導(dǎo)致線程進(jìn)入WAITING 狀態(tài)。
Sherlock Holmes 有句名言:
“當(dāng)你排除一切不可能的情況,剩下的,不管多難以置信,那都是事實(shí)?!?/p>
盡管 bfe-customer-application-query-svc 的定位是業(yè)務(wù)聚合層的應(yīng)用,主要是串行調(diào)用下游領(lǐng)域服務(wù),編排產(chǎn)品功能,通常不太可能主動(dòng)使用上面提到的這些方法來(lái)管理線程。但畢竟已經(jīng)確定了框架代碼中并無(wú)任何導(dǎo)致WAITING狀態(tài)線程數(shù)飆升的證據(jù),因此我還是決定到業(yè)務(wù)代碼中一探究竟。
沒(méi)想到很快就發(fā)現(xiàn)了蛛絲馬跡。幾乎沒(méi)廢什么功夫,就從 confirmEvaluate 方法中找到了一段高度符合畫(huà)像的代碼,經(jīng)過(guò)精簡(jiǎn)后,如圖23。當(dāng)看到第 15 行和第 16 行的 future.get()調(diào)用時(shí),我不禁眉頭一緊。
圖 23:confirmEvaluate 方法實(shí)現(xiàn)
和當(dāng)時(shí)開(kāi)發(fā)這段代碼的同學(xué)了解了一下, 創(chuàng)作背景大致是這樣的:
由于種種歷史原因,在確認(rèn)頁(yè)估價(jià)時(shí),需要根據(jù) “是否到付”,分兩次調(diào)用下游進(jìn)行估價(jià)(即圖23中第6行g(shù)etArrivalPayConfirmEvaluateResult 和 第 10 行 getCommonConfirmEvaluateResult)。confirmEvaluate 的耗時(shí)主要受調(diào)用下游計(jì)價(jià)服務(wù)的耗時(shí)影響,原先日常 P95 約為 750ms,串行調(diào)用兩次會(huì)導(dǎo)致 confirmEvaluate 方法的耗時(shí) double,達(dá)到 1.5s 左右??紤]到用戶(hù)體驗(yàn),于是決定通過(guò)多線程并發(fā)執(zhí)行來(lái)減少耗時(shí)。
雖然想法無(wú)可厚非,但具體到代碼實(shí)現(xiàn)卻有幾個(gè)明顯的問(wèn)題:
- 在調(diào)用 future.get() 等待線程返回結(jié)果時(shí),沒(méi)有設(shè)置超時(shí)時(shí)間,這是非常不可取的錯(cuò)誤實(shí)踐;
- getArrivalPayConfirmEvaluateResult 和 getCommonConfirmEvaluateResult 中包裝了繁重的業(yè)務(wù)邏輯(且重復(fù)性極高),不僅會(huì)調(diào)用下游的計(jì)價(jià)服務(wù),還會(huì)調(diào)用包括 getTagInfo 在內(nèi)的多個(gè)其他下游服務(wù)的接口。而最初的目的僅僅是解決 “調(diào)用兩次計(jì)價(jià)服務(wù)” 的問(wèn)題;
- 使用了自定義線程池 BizExecutorsUtils.EXECUTOR 而不是框架推薦的動(dòng)態(tài)線程池;
尤其是看到使用了自定義線程池 BizExecutorsUtils.EXECUTOR 的時(shí)候,我第二次眉頭一緊,心中預(yù)感問(wèn)題找到了。BizExecutorsUtils.EXECUTOR 的定義如圖24。
- Line 16:可以看到線程池的最大線程數(shù)只有 20;
- Line 19:工作隊(duì)列卻很大,可以允許 1K+ 個(gè)任務(wù)排隊(duì);
此外,一個(gè)平??雌饋?lái)合情合理,但用在這里卻雪上加霜的設(shè)計(jì)是,EXECUTOR 是靜態(tài)初始化的,在同一個(gè) JVM 進(jìn)程中全局唯一。這里的線程池定義,很難不讓我想到只有 3 個(gè)診室,卻排了 500 號(hào)病人的呼吸內(nèi)科。
圖 24:自建線程池定義
正是因?yàn)檫@個(gè)線程池的引入,導(dǎo)致了 “瓶頸效應(yīng)”,如圖25,顯然 confirmEvaluate 的執(zhí)行過(guò)程要比圖21描繪的模型更加復(fù)雜。作為 SOA 請(qǐng)求的入口,方法本身確實(shí)是通過(guò) tomcat 線程執(zhí)行的。故障發(fā)生時(shí),tomcat 線程池配置的最大線程數(shù)為 200(單個(gè) Pod 最多能夠并發(fā)處理 200 個(gè)請(qǐng)求)。但在方法內(nèi)部,估價(jià)的核心邏輯被包裝成了兩個(gè)子任務(wù),提交到了最大僅有 20 個(gè)工作線程的 EXECUTOR 自定義線程池執(zhí)行。也就是說(shuō),EXECUTOR 自定義線程池的處理能力僅有 tomcat 線程池的 1/10,卻需要處理 2 倍于 QPS 的任務(wù)量。
圖 25:引入自定義線程池導(dǎo)致的 “瓶口效應(yīng)”
我們不妨結(jié)合前文的信息做個(gè)粗略的估算:
- 首先,在 getTagInfo 耗時(shí)抖動(dòng)前,請(qǐng)求量約為 75+ requests/s,負(fù)載均衡到 2 個(gè) zone 的共 4 個(gè) Pod 上,單個(gè) Pod 的請(qǐng)求量約為 18 ~ 20 requests/s;
- 在 getTagInfo 耗時(shí)抖動(dòng)前,一次估價(jià)流程的耗時(shí)約為 750ms,getTagInfo 耗時(shí)飆升后(10ms → 500ms),一次估價(jià)流程的耗時(shí)理論上增加到 1.3s 左右。由于 confirmEvaluate 內(nèi)部拆分的子任務(wù),幾乎就是完整的估價(jià)流程,因此,兩個(gè)子任務(wù)的執(zhí)行耗時(shí)也需要 1.3s 左右;
- 在 getTagInfo 耗時(shí)抖動(dòng)的 1s 內(nèi),雖然請(qǐng)求量無(wú)顯著變化,但根據(jù) 2 倍關(guān)系,這 1s 內(nèi)將會(huì)產(chǎn)生 36 ~ 40 個(gè)任務(wù)(單個(gè) Pod),提交給 EXECUTOR 線程池執(zhí)行;
- 由于單個(gè)子任務(wù)執(zhí)行耗時(shí) 1s,也就是說(shuō),在這 1s 內(nèi)提交到 EXECUTOR 線程池的任務(wù)都無(wú)法執(zhí)行完,EXECUTOR 的 20 個(gè)工作線程,將全部飽和,大約有一半的任務(wù)正占用工作線程,另一半的任務(wù)在工作隊(duì)列中等待;
顯然,這是一個(gè)十分簡(jiǎn)單的計(jì)算模型,并不能反映現(xiàn)實(shí)中更加復(fù)雜的情況[12],但并不妨礙說(shuō)明問(wèn)題(和 2.2 節(jié)中分析的案例一模一樣的問(wèn)題)。子任務(wù)可能在 EXECUTOR 線程池工作隊(duì)列中排隊(duì)很久, 從而導(dǎo)致 confirmEvaluate 方法在 future.get() 上等待(圖23第15、16行),一直占用 tomcat 工作線程,新到達(dá)的請(qǐng)求只能不斷創(chuàng)建新的 tomcat 線程來(lái)執(zhí)行。同時(shí),confirmEvaluate 響應(yīng)變慢或超時(shí)錯(cuò)誤,又會(huì)誘發(fā)用戶(hù)重試,進(jìn)一步帶來(lái)請(qǐng)求量激增(如圖1,抖動(dòng)發(fā)生后,請(qǐng)求量飆升到了約 100 requests/s),導(dǎo)致 EXECUTOR 線程池的處理能力進(jìn)一步惡化。如此惡性循環(huán),最終徹底耗盡 tomcat 線程池的可用工作線程。
通過(guò) Trace 跟蹤,我們可以輕松找出支持 “瓶頸效應(yīng)” 的證據(jù),如圖26。confirmEvaluate 方法首先會(huì)調(diào)用下游的 get_user_info 接口獲取用戶(hù)信息[13]。然后,該方法將構(gòu)建兩個(gè)子任務(wù)并將其提交至 EXECUTOR 線程池(圖23 第 5~7 行、 第 9~11 行)。值得注意的是,get_user_info 調(diào)用完成后,到子任務(wù)被提交到 EXECUTOR 線程池的這段時(shí)間里,代碼只做了一些參數(shù)包裝和驗(yàn)證的簡(jiǎn)單邏輯輯[13],因此這部分代碼的執(zhí)行時(shí)間基本可以忽略不計(jì)。我們可以近似的理解為,在 get_user_info 的調(diào)用完成后,子任務(wù)就立即提交給了 EXECUTOR 線程池。
圖 26:子任務(wù)在 EXECUTOR 線程池工作隊(duì)列中排隊(duì)
當(dāng) EXECUTOR 線程池處理能力足夠時(shí),子任務(wù)被提交到 EXECUTOR 線程池后也應(yīng)該迅速被執(zhí)行,因此從 get_user_info 接口調(diào)用完成到子任務(wù)開(kāi)始執(zhí)行中間的間隔時(shí)間應(yīng)該很短。但從 圖26中給出的 Trace 鏈路可以看到[14],從 get_user_info 接口調(diào)用,到子任務(wù) 1 和子任務(wù) 2 開(kāi)始執(zhí)行,間隔了約 6s 的時(shí)間[15]。而這 6s 反映的正是子任務(wù)在 EXECUTOR 線程池中等待執(zhí)行的耗時(shí)。
2.4 診斷結(jié)論
至此,終于可以給出診斷結(jié)論:
圖片
三、醫(yī)囑和處方
本文的最后,來(lái)談?wù)剰倪@次故障中能獲得怎樣的經(jīng)驗(yàn)教訓(xùn)?
3.1 穩(wěn)定性預(yù)算要有保障,評(píng)估 I/O 密集型應(yīng)用的容量時(shí)不能只看 CPU 水位
不知大家在閱讀前文時(shí)是否好奇過(guò),為什么堂堂一個(gè)核心應(yīng)用,每個(gè) zone 只部署 2 個(gè) Pod?
一個(gè)最基本的共識(shí)是,部署兩個(gè) Pod 是故障冗余的最低限度[16]。但對(duì)于核心應(yīng)用而言,僅用兩個(gè) Pod 互為備份有點(diǎn)過(guò)于極限了。這是因?yàn)?,核心?yīng)用通常需要處理更復(fù)雜的業(yè)務(wù)邏輯并承載更多的流量,當(dāng)其中一個(gè) Pod 故障時(shí),剩下唯一的 Pod 將瞬間承載全部的正常流量和陡增的異常流量(往往會(huì)伴有用戶(hù)重試的流量),很容易引發(fā)性能問(wèn)題。
事實(shí)上,就在故障發(fā)生前不久,該應(yīng)用在每個(gè) zone 還部署有 4 個(gè) Pod。按照 2.3.3 節(jié)的計(jì)算方式算一下就會(huì)發(fā)現(xiàn),如果故障發(fā)生時(shí)的流量均勻分配到 8 個(gè) Pod 上,單個(gè) Pod 上 EXECUTOR 線程池每秒需要處理的子任務(wù)數(shù)量約為 18 個(gè),工作線程還有余量,這次的故障有極大的可能就不會(huì)發(fā)生[17]。
那為什么 Pod 數(shù)量縮水了呢?因?yàn)樵诳刂瞥杀镜拇蟊尘跋?,CI 團(tuán)隊(duì)多次找到應(yīng)用的 Owner 并以 CPU 水位為唯一標(biāo)準(zhǔn)溝通縮容計(jì)劃,最終兩邊達(dá)成一致,將每個(gè) zone 的 Pod 數(shù)量縮容到了 2 個(gè)。回過(guò)頭來(lái)看,這是一個(gè)非常錯(cuò)誤的決策,原因至少有以下兩點(diǎn):
- 正如剛才所說(shuō),這是一個(gè)核心應(yīng)用。每個(gè) zone 僅部署 2 個(gè) Pod 過(guò)于極限;
- 這是一個(gè) I/O 密集型應(yīng)用,不應(yīng)該將 CPU 水位作為評(píng)估容量的唯一參考指標(biāo);
前者本質(zhì)上是一個(gè)“成本(或預(yù)算)規(guī)劃”的問(wèn)題。通過(guò)系統(tǒng)縮容以減少整體運(yùn)營(yíng)成本,的確行之有效。但如果在執(zhí)行過(guò)程中,各方過(guò)于追求 “節(jié)省成本”,便可能在不自知的情況下,削減了原本必要的開(kāi)支。比如在本文討論的場(chǎng)景中,部署額外的若干個(gè) Pod 以增強(qiáng)應(yīng)對(duì)異常情況(依賴(lài)耗時(shí)、流量等波動(dòng))的能力就是必要的投入,即提供基本業(yè)務(wù)功能以外的穩(wěn)定性預(yù)算。
后者則反映出一個(gè)更為艱巨的問(wèn)題:我們?nèi)狈茖W(xué)的方法(或統(tǒng)一的標(biāo)準(zhǔn))來(lái)評(píng)估容量應(yīng)該設(shè)定為多少。對(duì)于一個(gè) I/O 密集型應(yīng)用,許多有限資源均可能在 CPU 算力達(dá)到瓶頸之前先達(dá)到瓶頸,比如本文討論的線程池,以及連接池、帶寬等。因此,評(píng)估 I/O 密集型應(yīng)用的容量并不是一件容易的事情。往往需要根據(jù)流量的預(yù)測(cè)值進(jìn)行壓測(cè),同時(shí)根據(jù)線程池、連接池、帶寬等有限資源的飽和度指標(biāo),結(jié)合接口響應(yīng)時(shí)間(延遲),異常數(shù)量(錯(cuò)誤)等健康度指標(biāo)來(lái)綜合分析判斷。然而我們?nèi)匀狈χ笇?dǎo)這種評(píng)估的最佳實(shí)踐,下一節(jié)我們將繼續(xù)討論。
3.2 謹(jǐn)慎的對(duì)系統(tǒng)性能做出假設(shè),壓測(cè)結(jié)果才是檢驗(yàn)性能的唯一方
實(shí)際上,bfe-customer-application-query-svc 及其依賴(lài)的下游 bfe-commodity-core-svc 均為同一項(xiàng)目申請(qǐng)、同一批次上線的新應(yīng)用。一般情況下,我們會(huì)對(duì)新上線的應(yīng)用進(jìn)行專(zhuān)門(mén)的壓力測(cè)試。而正如上文討論的,自定義線程池瓶頸問(wèn)題在壓力測(cè)試中應(yīng)該很容易被觸發(fā)。然而后來(lái)經(jīng)過(guò)證實(shí),只針對(duì) bfe-commodity-core-svc 進(jìn)行了壓測(cè),卻并未對(duì) bfe-customer-application-query-svc 進(jìn)行壓測(cè),這讓我感到難以理解。
進(jìn)一步深入了解當(dāng)時(shí)的上下文后,我發(fā)現(xiàn)在決定不對(duì) bfe-customer-application-query-svc 進(jìn)行壓力測(cè)試的過(guò)程中,主要有兩種觀點(diǎn):
- 壓測(cè)也難以發(fā)現(xiàn)導(dǎo)致本次故障的問(wèn)題;
- confirmEvaluate 是由舊應(yīng)用遷移過(guò)來(lái)的。舊應(yīng)用運(yùn)行穩(wěn)定且沒(méi)有任何問(wèn)題,新應(yīng)用也會(huì)同樣運(yùn)行良好;
首先,且不論第一種觀點(diǎn)是否正確,它都不能作為不進(jìn)行壓測(cè)的充分理由。因?yàn)樵诋?dāng)時(shí)我們并未具有現(xiàn)在的全知全覽視野,壓測(cè)的目的并不僅僅在于尋找本次發(fā)生的這一具體問(wèn)題,而是揭示所有可能存在的、尚未明確的性能風(fēng)險(xiǎn)。我更傾向于將這一觀點(diǎn)定性為事后為之前的過(guò)失找補(bǔ)的話術(shù),而不是一個(gè)專(zhuān)業(yè)的技術(shù)性判斷。
第二種觀點(diǎn)則揭示了我們?cè)谌粘9ぷ髦谐S械钠?jiàn)。相較于編碼和功能測(cè)試,由于工期緊張、時(shí)間壓力等因素,我們常常將那些非日常的任務(wù),如壓測(cè),視為可有可無(wú)的附加項(xiàng),主觀上傾向于省略這項(xiàng)工作。然而,壓測(cè)真的那么復(fù)雜嗎?答案并非如此。舉個(gè)例子,后期我們?cè)陬A(yù)發(fā)環(huán)境通過(guò)壓測(cè)復(fù)現(xiàn)此次故障的演練,包括前期準(zhǔn)備工作和制定演練方案,幾個(gè)小時(shí)內(nèi)就可完成。
往根兒上說(shuō),這反映了我們?cè)?“測(cè)試方案設(shè)計(jì)(或測(cè)試分析)” 工作上的不足,本質(zhì)上還是在 “如何開(kāi)展容量/性能評(píng)估“ 的問(wèn)題上,缺少最佳實(shí)踐、缺少科學(xué)的方法論指導(dǎo)。
另一個(gè)致命的假設(shè),是在將流量比例從 50% 提升到 100% 的窗口前期做出的:
“50% 流量運(yùn)行了很多天都沒(méi)問(wèn)題,放量到 100% 應(yīng)該穩(wěn)了?!?/p>
只論業(yè)務(wù)邏輯的正確性,這個(gè)假設(shè)或許沒(méi)有太大的問(wèn)題。但如果把容量因素納入考慮,這個(gè)假設(shè)就過(guò)于想當(dāng)然了,缺少了一次至關(guān)重要的壓測(cè)論證。
其實(shí),實(shí)踐檢驗(yàn)真理的粗淺道理每個(gè)人都懂。難的是在工作中不心存僥幸,踏踏實(shí)實(shí)的執(zhí)行到位。
3.3 將有限資源納入監(jiān)控
在穩(wěn)定性工程中,我們常會(huì)談到 “可監(jiān)控” 這個(gè)詞,指的是通過(guò)具有統(tǒng)計(jì)意義和顯著性的可觀測(cè)指標(biāo)來(lái)反映系統(tǒng)運(yùn)行狀況。
對(duì)系統(tǒng)中的有限資源進(jìn)行針對(duì)性監(jiān)控往往大有裨益。主要有以下三點(diǎn)理由:
- 有限資源的耗竭往往是許多性能問(wèn)題的根本原因。因此,針對(duì)性的監(jiān)控將大大加速性能問(wèn)題的排查;
- 有限資源經(jīng)常也是跨越多個(gè)業(yè)務(wù)場(chǎng)景的“共享資源”。因此,一旦這些資源引發(fā)性能問(wèn)題,可能迅速導(dǎo)致整個(gè)系統(tǒng)的崩潰;
- 有限資源的耗竭過(guò)程通常伴隨著流量的增加。但在達(dá)到瓶頸之前,常規(guī)的業(yè)務(wù)指標(biāo)很難反映出這一趨勢(shì);
實(shí)際上,在大家熟知的那些被廣泛使用的開(kāi)源項(xiàng)目中,就有很多值得借鑒的例子。例如,通過(guò)圖2、圖3所示的監(jiān)控指標(biāo),能夠很好的幫助我們觀察運(yùn)行時(shí) tomcat 線程池的飽和度。再比如 HikariCP,提供了十分豐富的監(jiān)控指標(biāo),以便于觀察連接池運(yùn)行時(shí)的各種狀態(tài)。
如果從一開(kāi)始,能夠效仿 tomcat 的做法,在引入 EXECUTOR 自定義線程池的時(shí)候,將繁忙線程數(shù)、最大線程數(shù)指標(biāo)監(jiān)控起來(lái),是有機(jī)會(huì)在切流 50% 的時(shí)候就發(fā)現(xiàn)危機(jī)的。按 2.3.3 節(jié)討論的方式粗略換算,切流 50% 時(shí) EXECUTOR 線程池的繁忙線程占比將超過(guò) 80%[19]。任何素質(zhì)過(guò)關(guān)的研發(fā),此時(shí)都會(huì)審慎的評(píng)估容量風(fēng)險(xiǎn)。
腳注
[1] 基于現(xiàn)有架構(gòu),我們的應(yīng)用部署拆分成了兩個(gè) zone,zone 的概念在這里不做展開(kāi),可以簡(jiǎn)單理解為兩個(gè)異地機(jī)房,流量按照接近 1:1 的比例均分到兩個(gè) zone 中;
[2] zone-2 兩個(gè) Pod 的 CPU 利用率、內(nèi)存利用率等指標(biāo)都處于低水位。注意文中將 CPU、內(nèi)存 稱(chēng)作硬件資源只是為了敘述方便,實(shí)際上它們都是基于真實(shí)硬件資源 “虛擬化“ 之后的 ”配額”;
[3] 我在《解決因主庫(kù)故障遷移導(dǎo)致的核心服務(wù)不可用問(wèn)題》一文中討論過(guò)連接池耗盡的問(wèn)題,如果你感興趣,可以看一看這篇文章;
[4] 圖3、圖4中的曲線含義:① Max Threads,即圖中的藍(lán)色曲線,表示由人為配置的 tomcat 線程池的最大線程數(shù),該應(yīng)用的最大線程數(shù)被設(shè)置為 200。② Available Threads,即圖中的橙色曲線,表示當(dāng)前線程池中已經(jīng)創(chuàng)建的線程數(shù)。③Busy Threads,即圖中的綠色曲線,表示當(dāng)前線程池中正在處理任務(wù)的繁忙線程數(shù);
[5] 這里實(shí)際上還判斷了線程池是否仍在運(yùn)行(isRunning(c)),為了表述重點(diǎn),省略了對(duì)這段代碼的解釋?zhuān)?/p>
[6] 也有可能是線程池已經(jīng)被 shutdown,即 isRunning(c) 返回了 false。同[4],為了表述重點(diǎn),省略了對(duì)這種可能性的解釋?zhuān)?/p>
[7] 這個(gè)過(guò)程主要是讀取 HTTP 響應(yīng)的 body(JSON String),反序列化為 Java POJO。這部分代碼不在這里展開(kāi);
[8] 這個(gè)表達(dá)為了能更簡(jiǎn)單的說(shuō)明問(wèn)題,其實(shí)并不準(zhǔn)確。更準(zhǔn)確的是請(qǐng)求被放入 IOReactorWorker 線程的請(qǐng)求隊(duì)列后立即返回。下文會(huì)通過(guò)更清晰的模型來(lái)解釋這個(gè)邏輯;
[9] 通過(guò) IOReactorConfig 類(lèi)的 ioThreadCount屬性設(shè)置,通常是 CPU 核數(shù)的 2 倍;
[10] 實(shí)現(xiàn)代碼在org.apache.hc.core5.reactor.IOWorkers#newSelector 方法中,有兩種實(shí)現(xiàn) PowerOfTwoSelector 和 GenericSelector,在這里不做詳細(xì)展開(kāi);
[11] 實(shí)際上,除了直接處理請(qǐng)求的 tomcat 線程,框架代碼還創(chuàng)建并管理著一些其他的工作線程,但這類(lèi)線程的飽和度、擴(kuò)縮容通常并不會(huì)受到業(yè)務(wù)流量變化的影響。例如 consulCacheScheduledCallback 線程,主要和依賴(lài)的下游 SOA 應(yīng)用的數(shù)量有關(guān)。因此,這類(lèi)線程的嫌疑也可以排除;
[12] 例如,在這個(gè)計(jì)算模型中,getTagInfo 抖動(dòng)前后,一次估價(jià)流程耗時(shí)分別為 750ms 和 1.3s,增加的耗時(shí)是由 getTagInfo 接口的響應(yīng)時(shí)長(zhǎng)唯一決定的,這是一種理想狀態(tài)。實(shí)際上,一旦線程池處理能力衰退,會(huì)有相當(dāng)一部分請(qǐng)求的耗時(shí),遠(yuǎn)遠(yuǎn)大于 1.3s 這個(gè)值,耗時(shí)并不僅僅只受 getTagInfo 接口的影響;
[13] 這部分的代碼并未在圖23中展示,包含在了第 3 行 [Ellipis Java code] 省略的代碼中;
[14] 需要注意的是,盡管圖26中我標(biāo)注了 子任務(wù) 1(getArrivalPayConfirmEvaluateResult)在前、子任務(wù) 2(getCommonConfirmEvaluateResult)在后,但在并發(fā)狀態(tài)下,實(shí)際的執(zhí)行順序并不一定是這樣。這里僅僅是為了敘述方便;
[15] 嚴(yán)謹(jǐn)?shù)恼f(shuō),最后一列時(shí)間戳(Timestamp)表示的是 “開(kāi)始執(zhí)行” 的時(shí)間,因此 6s 實(shí)際上是 get_user_info 調(diào)用開(kāi)始執(zhí)行到子任務(wù)開(kāi)始執(zhí)行的間隔時(shí)間,包含了 get_user_info 調(diào)用的耗時(shí)。但這里 get_user_info 調(diào)用耗時(shí)只有 10ms,相對(duì)于 6s 而言,完全可以忽略不計(jì);
[16] 單一 Pod 發(fā)生故障的概率是非常高的,而兩個(gè) Pod 同時(shí)發(fā)生故障的概率則會(huì)大大降低,因此,至少部署兩個(gè) Pod 以防止在單一 Pod 故障時(shí)整個(gè)應(yīng)用都不可用。同時(shí),兩個(gè) Pod 也是為了更好的實(shí)現(xiàn)平滑發(fā)布;
[17] 注意,這里并不是說(shuō)一定不會(huì)發(fā)生。實(shí)際上每秒 18 個(gè)任務(wù)對(duì)于 EXECUTOR 線程池來(lái)說(shuō)也已經(jīng)比較極限,即便躲過(guò)了這一次故障,也未必能躲過(guò)下一次;
[18] 這就好比在急性心梗發(fā)病前,患者的心電圖通常不會(huì)有太顯著的異常。但如果進(jìn)行冠狀動(dòng)脈造影就能夠更早的發(fā)現(xiàn)心梗風(fēng)險(xiǎn)。更進(jìn)一步,通過(guò)定期體檢檢測(cè)血脂、膽固醇等指標(biāo),也能夠更早體現(xiàn)疾病發(fā)生的趨勢(shì);
[19] 注意,這只是一個(gè)參考:34 reqeusts/s ÷ 4 pods × 2 ÷ 20(max pool size) × 100% = 85%;