成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

聊一次線程池使用不當(dāng)導(dǎo)致的生產(chǎn)故障

開(kāi)發(fā) 前端
通過(guò) Pod 監(jiān)控,輕易就能排除算力、存儲(chǔ)等硬件資源耗盡的可能性[2]。該應(yīng)用不依賴(lài)數(shù)據(jù)庫(kù),并未使用連接池來(lái)處理同步 I/O也不太可能是連接池耗盡[3]。因此,最有可能還是工作線程出了問(wè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 流量之和)圖 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)圖 2:tomcat 線程池使用情況(*.*.186.8)

圖 3:tomcat 線程池使用情況(*.*.188.173)圖 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í):

  1. Line 25 - 26:首先判斷當(dāng)前池中工作線程數(shù)是否小于 corePoolSize,如果小于 corePoolSize 則直接新增工作線程執(zhí)行該任務(wù);
  2. Line 30:否則,嘗試將當(dāng)前任務(wù)放入 workQueue;
  3. 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>

  1. Line 4:當(dāng)線程池中的工作線程數(shù)已經(jīng)達(dá)到最大線程數(shù)時(shí),則直接將任務(wù)放入隊(duì)列;
  2. Line 6:否則,如果線程池中的工作線程數(shù)還未達(dá)到最大線程數(shù),當(dāng)提交的任務(wù)數(shù)(parent.getSubmittedCount())小于池中工作線程數(shù),即存在空閑的工作線程時(shí),將任務(wù)放入隊(duì)列。這種情況下,放入隊(duì)列的任務(wù),理論上將立刻被空閑的工作線程取出并執(zhí)行;
  3. 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)圖 5:tomcat TaskQueue offer() 方法實(shí)現(xiàn)

通過(guò)分析這兩段代碼,得到如圖6的 tomcat 線程池?cái)U(kuò)容流程圖。

圖 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)求圖 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:探活失敗圖 8:探活失敗

圖 9:探活失敗圖 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 線程池使用情況圖 10:tomcat 線程池使用情況

圖 11:tomcat 線程池使用情況圖 11:tomcat 線程池使用情況

圖 12:探活成功圖 12:探活成功

圖 13:探活成功圖 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)圖 14:線程狀態(tài)

圖 15:線程狀態(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)用棧圖 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行。

  1. Line 8:調(diào)用ConcreteInvocationInvoker類(lèi)的另一個(gè)doInvoke方法獲得response。真正發(fā)起調(diào)用的邏輯其實(shí)都是在這個(gè)doInvoke方法里實(shí)現(xiàn)的;
  2. Line 12:調(diào)用 response的recreate()方法,并將結(jié)果包裝為result。由于省略了非核心代碼,Line 12 實(shí)際上就是線程調(diào)用棧中提示的的ConcreteInvocationInvoker.java 71行的代碼;

圖 17:ConcreteInvocationInvoker#invoke 方法定義圖 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行。

  1. 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)求取消;
  2. Line 11:我們重點(diǎn)關(guān)注正向流程,即 completed方法的實(shí)現(xiàn)。實(shí)際上就是解析(unpack) HTTP 的響應(yīng)結(jié)果,然后調(diào)用 response.succeeded(result);

圖 18:ConcreteInvocationInvoker#doInvoke 方法定義圖 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的一層包裝:

  1. Line 5:一上來(lái)就綁定了一個(gè) future 對(duì)象;
  2. Line 11:recreate 方法的核心,就是在 future 對(duì)象的 get 方法上等待;
  3. Line 22:succeeded方法的核心,就是將值(從 HTTP 響應(yīng)中解析出的調(diào)用結(jié)果)寫(xiě)入到 future;

圖 19:JsonRpcResponse 定義圖 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)用序列圖圖 20:客戶(hù)端發(fā)起 SOA 調(diào)用序列圖

更進(jìn)一步,將 asyncClient 的內(nèi)部實(shí)現(xiàn)展開(kāi),完整的 “線程池 + 連接池” 模型如圖21:

圖 21:SOA 調(diào)用客戶(hù)端線程池、連接池模型圖 21:SOA 調(diào)用客戶(hù)端線程池、連接池模型

  1. 在 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)行接收;
  2. 每個(gè) IOReactorWorker 線程內(nèi)部都綁定了一個(gè)請(qǐng)求隊(duì)列(requestQueue) 和 一個(gè)該線程獨(dú)享的 Selector,線程啟動(dòng)后,即開(kāi)啟 Selector 事件循環(huán),代碼如圖22;
  3. 在 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);
  4. IOReactorWorker 會(huì)從連接池中取出(lease)一個(gè)連接并執(zhí)行請(qǐng)求。注意 asyncClient 的連接池管理方式比較特別,是根據(jù) route(即不同的 host+port/domain 組合)進(jìn)行分組的,每個(gè) route 有一個(gè)獨(dú)立的連接池;
  5. 請(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)核心邏輯圖 22:IOReactorWorker 線程內(nèi)部的事件循環(huán)核心邏輯

通過(guò)上面的分析可以得出結(jié)論:一次標(biāo)準(zhǔn)的 SOA 調(diào)用,并不會(huì)導(dǎo)致 tomcat 線程進(jìn)入 WAITING 狀態(tài)。

  1. 雖然 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)控曲線的特征不符;
  2. 此外,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)圖 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)題:

  1. 在調(diào)用 future.get() 等待線程返回結(jié)果時(shí),沒(méi)有設(shè)置超時(shí)時(shí)間,這是非常不可取的錯(cuò)誤實(shí)踐;
  2. 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)題;
  3. 使用了自定義線程池 BizExecutorsUtils.EXECUTOR 而不是框架推薦的動(dòng)態(tài)線程池;

尤其是看到使用了自定義線程池 BizExecutorsUtils.EXECUTOR 的時(shí)候,我第二次眉頭一緊,心中預(yù)感問(wèn)題找到了。BizExecutorsUtils.EXECUTOR 的定義如圖24。

  1. Line 16:可以看到線程池的最大線程數(shù)只有 20;
  2. 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:自建線程池定義圖 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)”圖 25:引入自定義線程池導(dǎo)致的 “瓶口效應(yīng)”

我們不妨結(jié)合前文的信息做個(gè)粗略的估算:

  1. 首先,在 getTagInfo 耗時(shí)抖動(dòng)前,請(qǐng)求量約為 75+ requests/s,負(fù)載均衡到 2 個(gè) zone 的共 4 個(gè) Pod 上,單個(gè) Pod 的請(qǐng)求量約為 18 ~ 20 requests/s;
  2. 在 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 左右;
  3. 在 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í)行;
  4. 由于單個(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ì)圖 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):

  1. 正如剛才所說(shuō),這是一個(gè)核心應(yīng)用。每個(gè) zone 僅部署 2 個(gè) Pod 過(guò)于極限;
  2. 這是一個(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):

  1. 壓測(cè)也難以發(fā)現(xiàn)導(dǎo)致本次故障的問(wèn)題;
  2. 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)理由:

  1. 有限資源的耗竭往往是許多性能問(wèn)題的根本原因。因此,針對(duì)性的監(jiān)控將大大加速性能問(wèn)題的排查;
  2. 有限資源經(jīng)常也是跨越多個(gè)業(yè)務(wù)場(chǎng)景的“共享資源”。因此,一旦這些資源引發(fā)性能問(wèn)題,可能迅速導(dǎo)致整個(gè)系統(tǒng)的崩潰;
  3. 有限資源的耗竭過(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%;

責(zé)任編輯:武曉燕 來(lái)源: JAVA日知錄
相關(guān)推薦

2024-09-05 08:07:55

2022-10-25 18:00:00

Redis事務(wù)生產(chǎn)事故

2020-11-16 12:35:25

線程池Java代碼

2019-10-10 15:40:17

redisbug數(shù)據(jù)庫(kù)

2021-09-11 19:00:54

Intro元素MemoryCache

2024-02-04 08:26:38

線程池參數(shù)內(nèi)存

2022-06-21 11:24:05

多線程運(yùn)維

2021-06-10 06:59:34

Redis應(yīng)用API

2020-10-22 07:09:19

TCP網(wǎng)絡(luò)協(xié)議

2009-12-17 14:53:52

VS2008程序

2021-05-20 10:02:50

系統(tǒng)Redis技巧

2021-08-26 14:26:25

Java代碼集合

2010-01-06 10:56:47

華為交換機(jī)使用

2021-07-11 09:34:45

ArrayListLinkedList

2020-06-12 13:26:03

線程池故障日志

2011-08-18 13:49:32

筆記本技巧

2022-08-16 08:27:20

線程毀線程異步

2022-06-01 06:17:42

微服務(wù)Kafka

2016-12-06 09:34:33

線程框架經(jīng)歷

2025-04-10 08:05:00

Netty線程池代碼
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 成人久久一区 | 国产精品美女久久久久久免费 | 91精品国产日韩91久久久久久 | 欧美日韩国产中文 | 日日摸日日碰夜夜爽亚洲精品蜜乳 | 91国内精精品久久久久久婷婷 | 五月花丁香婷婷 | 一区二区三区四区av | 婷婷久久五月天 | 久久一 | 9191在线播放| 成人免费在线播放视频 | 一区二区精品在线 | 一区二区三区精品在线 | 午夜影院在线观看免费 | 成人av鲁丝片一区二区小说 | 日韩精品一区二区不卡 | 国产激情视频在线观看 | 超碰97在线免费 | 久久久久久久久国产成人免费 | 日韩欧美三级在线 | 日韩国产精品一区二区三区 | 欧美成人视屏 | 亚洲第一在线 | 激情五月婷婷综合 | 夜夜干夜夜操 | 久久大陆| 99精品一区二区 | 亚洲欧美日韩在线不卡 | 在线观看中文视频 | 九色 在线 | 国产精彩视频 | 亚洲国产精品一区二区第一页 | 一区二区三区四区电影 | 成人h视频在线 | 夜夜摸天天操 | 美女天堂| 久久久久久久久久久91 | 亚洲精品国产偷自在线观看 | 日韩国产欧美在线观看 | 青草青草久热精品视频在线观看 |