你好呀,我是歪歪。
前兩天在一個技術羣裏看到有人拋出一張圖片,提出了這樣的一個問題:
請教一下,線程池可以做到根據任務的類型,來指定特定線程執行嗎?
瞭解了一下背景,是批量任務觸發,從訂單表中查詢出“處理中”狀態的訂單,訂單可能屬於不同的通道,所以需要調用不同通道的接口。
現在的方案是把訂單查出來之後,往線程池裏面扔,在異步任務裏面判斷當前訂單是屬於哪個通道,就調用哪個通道的查詢接口:
這是常規做法,看起來沒有毛病。
但是現在提問的這個哥們遇到了一個問題:有一個通道的查詢接口特別慢,會佔着線程池裏面的線程資源,影響了其他兩個通道的訂單查詢。
舉個極端的例子,比如你的線程池核心線程數就三個。
假設一共有 5 筆數據,前 3 筆是通道 A 的,後面兩筆分別是通道 B 和通道 C 的。
結果現在通道 A 出問題了:
直接把你的核心線程都佔滿了,剩下的兩筆對應 B 和 C 通道的數據就在隊列裏面排着隊,等着。
你說這個合不合理?
非常不合理,對不對?
但是這個問題確實也是很常規,常規到它甚至沒有資格作爲一個場景面試題出現在面試環節中。
問題在於不同的通道在共用同一個線程池,從而導致的相互影響。所以解決思路主要就是怎麼把資源隔離開來。
一般來說,大家能想到的第一個解決方案就是用 MQ 嘛:
利用不同的隊列,天然就把不同通道的訂單給區分開了,在監聽側各自處理各自通道的數據,這樣就達到了資源隔離的效果。
這個方案應該是很常規了,但是這個常規方案立馬就被斃了。
因爲:
需要注意的是,他這裏說的“系統內部”是指同一個微服務,也就是不允許一個微服務使用 MQ 來做“自產自銷”。
我個人認爲是“自產自銷”沒有任何問題的,在這個場景下我完全可以藉助它的特性幫我做數據分隔、異步處理數據,而且代碼簡單,邏輯清晰。
但是既然是公司規定,可能有一些因地制宜的考慮,我們也不好去做過多的批判。
反正就是 MQ 可以解決這個問題,但是老闆並不採取這個方案。
沒關係,小腦殼一轉,大多數同學就能立馬就掏出了另外一個解決方案。
你前面出問題的原因不是因爲不同的通道在共用同一個線程池嗎?
那很簡單,每個通道各自搞一個線程池。然後和 MQ 的方案類似,根據不同的通道扔到對應的線程池中去,自己玩自己的:
這樣即使某個通道出問題了,由於在線程池層面做了線程資源隔離,所以也不影響另外的通道進行數據處理。
這個就是線程池隔離的方案。
其實關於這個方案,我當時還想到了另外一種原理一致,實現形式不一樣,但是最終被認爲是比較 low 的一個回答。
因爲他拋出的這個圖片,我第一眼理解錯了,我以爲是按照通道分組,然後用單線程一個個的去調用查詢接口,避免併發調用:
所以我提到了一個叫做 KeyAffinityExecutor 的魔改線程池:
這個線程池,它有一個比較厲害的特性,可以確保投遞進來的任務按某個維度劃分出任務,然後按照任務提交的順序依次執行。這個線程池可以通過並行處理(多個線程)來提高吞吐量、又能保證一定範圍內的任務按照嚴格的先後順序來運行。
對比到當前的這個問題中。
可以按照通道維度進行任務劃分,然後把任務往線程池扔的時候,就會被分配到不同的線程中去。
關於這個線程池,我之前寫了這篇文章,有興趣的可以去了解一下,不贅述了:《看到一個魔改線程池,面試素材加一!》
本質上還是線程池隔離的思路,只不過一個是分多個不同的業務線程池,線程池和業務綁定。一個是一個大線程池裏麪包了多個線程池,線程池可以通過分配規則的方式指定。
同一個思路的不同實現方案而已。
但是爲什麼我說我提出的這個魔改線程池的方案 low 呢?
因爲人家只是需要分組的特性,而不需要“按照任務提交的順序依次執行”的特性。
反而會出現如果一個通道的訂單多,只有一個線程來處理,導致性能不夠,任務堆積的情況。
但是,話說回來,你也可以魔改一下這個魔改線程池,把裏面的小線程池的核心線程數搞多點,就行了。
總之,都是線程池隔離的思路。
好了,這個方案我又講完了,誰贊成,誰反對?
看着沒有任何問題,但是實際情況是:
臥槽,50 多個?
確實,如果是隻有三個通道,或者多說點,五個通道嘛,我覺得用上面這個方案做線程池維度的隔離,都是可以接受的。
但實際情況是 50 多個通道,一想起項目裏面有 50 多個線程池在跑,這個就有點難受了。
好了,現在 MQ 和線程池隔離的方案都被否決了,接下來的思路是什麼?
沒有思路沒有關係,我們再來讀讀題:批量任務觸發,從訂單表中查詢出“處理中”狀態的訂單,訂單可能屬於不同的通道,所以需要調用不同通道的接口。但是某個通道慢,導致影響了其他通道訂單的查詢。
問怎麼辦?
某個通道慢,該怎麼辦?
有的通道慢,有的通道快,我該怎麼辦?
等等...
前面我們按照通道維度分線程池被否了的原因是通道太多了。
但是其實針對響應快的通道,我們完全不需要做線程池隔離,他們完全可以使用同一個線程池嘛,反正都是唰唰唰的就查回來了。
所以,我們只需要搞兩個線程池,一個處理通道響應快的,比如把接口調用的超時時間設置爲 1s。另外一個處理通道響應慢的,超時時間直接拉滿到 30s,自己慢慢玩去:
至於怎麼去判斷通道到底是快是慢呢?
這裏又可以大致分爲三個不同的方案了。
第一個方案就是已知某幾個通道是慢的,那就代碼裏面寫硬編碼都行。雖然不優雅,但是這確實也是一個在實際生產中常常被提及的一個快速解決問題的方案。
第二個方案就是配置化,可以做個配置表,來配置通道的快慢標識。程序裏面根據當前訂單的通道,來表裏面獲取當前通道的快慢標識,從而把訂單扔到不同的線程池中去。
在這個方案中,用配置表代替了硬編碼,但是還是需要人工基於線下溝通或者數據監控的方式去調整通道的快慢標識。
你知道的,線上程序這玩意,一旦涉及到人工介入,就遭老罪了,很不爽。
所以這個方案,有一點優雅,但是不多。
第三個方案就是配置化加自動化這一套組合拳。
配置化還是指前面提到的配置表。
但是這個表中通道的快慢標識,就不需要人工來介入了,完全由程序自己收集信息,進行判斷。
比如,我們可以假設一開始的時候所有的通道都能快速響應。但是突然某個通道開始“扯拐”,響應時長出現波動,1s 內沒有響應成功,那麼這個任務就會超時,就可以把這個任務扔到慢通道線程池中去處理,同時對該通道的失敗次數進行記錄。
當某個時間段失敗次數超過某個閾值之後,則在配置表中標識該通道爲慢通道。
這樣當下一個屬於該通道的訂單過來時,就會直接被扔到慢通道線程池中去。
這樣,就由程序完成了通道由“快標識”到“慢標識”的處理。
那麼當這個通道的問題解決之後,它又變成一個快通道時,怎麼去修改它在配置表中的標識呢?
很簡單,同樣的邏輯,在慢通道線程池處理的過程中,記錄某個時間段某個通道的平均響應時長,如果低於指定閾值,比如 1s,則在配置表中重新標識該通道爲快通道。
整個過程,不管標識怎麼變化,都是基於程序自動的數據統計來的,完全不需要人工介入。
甚至你還可以加一個邏輯:當配置表中的通道都是快通道時,兩個線程池都可以用起來,實現資源利用的最大化。
優雅,非常優雅。
至於怎麼去統計線程池中的任務“某個時間段失敗次數”和“某個時間段某個通道的平均響應時長”這樣的統計信息,在線程池裏面,專門留了這兩個方法給你去在任務執行之前和之後搞事情,完全可以基於這兩個方法做一些統計工作:
java.util.concurrent.ThreadPoolExecutor#runWorker
就目前提出的方案來說,把通道分爲快慢通道,然後劃分爲線程池是最滿足提問者的需求的。
最後應該就拿着這個方案去彙報了。
彙報題目我都幫忙想好了:
《基於通道關鍵指標收集分析的全自適應、高敏感度、資源利用最大化的調度方案彙報》
剩下的,就看你怎麼去吹了。
除去前面的方案外,其實我還想到一個“比較奇葩”的解決方案。
因爲他的業務場景是定時任務嘛,所以我想起了之前寫過的這篇文章:《又被奪命連環問了!從一道關於定時任務的面試題說起。》
既然能區分出來通道的快慢,那麼在定時任務啓動之後,我們就可以把“快慢標識”傳遞到服務器中去,服務器就能把訂單分爲快慢兩大類,然後一臺機器處理通道慢的訂單數據,一臺處理快的:
這樣我就能從服務器這個物理層面就把數據區分開了。
所以只要能標識開區分數據,那麼理論上不僅可以在代碼中區分,也可以往上抽離一層,通過服務器維度區分。
但是好處是什麼呢?
呃...
看起來確實沒什麼好處,只是這個方案比較奇葩,一般沒人想到,我就是順便提一嘴,主要是顯擺一下。
不顯擺一下,裝裝逼,總感覺不得勁。
類似的場景
基於提問者的這個問題,歪師傅也想起了兩個類似的場景。
一個是我參與開發過的一個對客發送短信的消息系統,簡化一下整個流程大概是這樣的:
上面這個圖片會出現什麼問題呢?
就是消息堆積。
當某個業務系統調用短信發送接口,批量發送消息的時候,比如發送營銷活動時,大量的消息就在隊列裏面堆着,慢慢消費。
其實堆積也沒有關係,畢竟營銷活動的實時性要求不是那麼高,不要求立馬發送到客戶手機上去。
但是,如果在消息堆積起來之後,突然有用戶申請了驗證碼短信呢?
需要把前面堆積的消費完成後,纔會發送驗證碼短信,這個已經來不及了,甚至驗證碼已經過期很久了你才發過去。
客戶肯定會罵娘,因爲獲取不到驗證碼,他就不能進行後續業務。
如果大量客戶因爲收不到驗證碼不能進行後續業務,引起羣體性的客訴,甚至用戶恐慌,這個對於企業來說是一個非常嚴重的事件。
怎麼辦呢?
解決方案非常簡單,再搞一個“高速”隊列出來:
驗證碼消息直接扔到“高速”隊列中去,這個隊列專門用來處理驗證碼、動賬通知這一類時效性要求極高的消息,從業務場景上分析,也不會出現消息堆積。
不是特別複雜的方案,大道至簡,問題得到了解決。
類比到前面說的“快慢”線程池,其實是一樣的思想,都是從資源上進行隔離。
只不過我說的這個場景更加簡單,不需要去收集信息進行動態判斷。業務流程上天然的就能區分出來,哪些消息實時性比較高,應該走“高速”隊列;哪些消息慢慢 發沒關係,可以應該走“常規”隊列。
而這個所謂的“高速”和“常規”,只是開發人員給一個普通隊列賦予的一個屬性而已,站在 MQ 的角度,這兩個隊列沒有任何區別。
另外一個場景是我想起了之前寫過的這篇文章:《我試圖給你分享一種自適應的負載均衡。》
我們還是先看看前面出現的這個圖:
圖中的線程池,不管是快的還是慢的,本質上他們處理的請求都是一樣的,即拿着訂單去對應的通道查詢訂單結果。
那我們是不是可以把這兩個線程池抽象一下,理解爲部署了同一個服務的兩個不同的服務器,一個服務器的性能好,一個服務器的性能差。
現在有一個請求過來了,理論上這兩個服務器都能處理這個請求,所以我們通過某個邏輯選一個服務器出來,把請求發過去。
這個“某個邏輯”不就是我們常說的負載均衡算法嗎?
負載均衡算法的算法有很多:
其中這幾個都是需要統計服務端的相關數據,基於數據進行分析,最終覺得把當前請求發個哪個服務器:
這個邏輯,和我們前面提到的這句話,其實是一脈相承的,都是信息收集、指標分析、閾值設定:
去統計線程池中的任務“某個時間段失敗次數”和“某個時間段某個通道的平均響應時長”這樣的統計信息
你想想我們最開始的問題是“一個通道慢了,影響了其他通道的數據,怎麼辦?”
現在我帶着你扯到了“負載均衡策略”。
這兩個場景不能說八竿子打不着吧,但是它們確實在一定程度上有相似性,轉好幾個彎之後,也能聯繫到一起。
你要是再發散一點,你甚至能想到 Serverless 的彈性場景,通過收集 CPU、Mem 指標、QPS、RT、TCP 連接數等指標,進行綜合判斷,彈性擴容,也無需人工介入,手動擴容。
所以,朋友,這個事情告訴我們一個什麼道理?
向上抽象問題的能力,把看看似不一樣的場景抽離成類似的問題模型的能力很重要。
還有,“一個通道慢需要進行資源隔離”這個問題的關鍵不在於“一個通道”上,雖然可以在通道層面做隔離,但是這樣並沒有抓住問題的關鍵。問題的關鍵在於“通道慢”,所以可以在“快慢”的維度上做隔離,這纔是問題的關鍵。
關鍵問題,就是要找到問題的關鍵。
這也是我在這一次羣聊的討論中學習到的東西。
好啦,本文的技術部分就到這裏了。
下面這個環節叫做[荒腔走板],技術文章後面我偶爾會記錄、分享點生活相關的事情,和技術毫無關係。我知道看起來很突兀,但是我喜歡,因爲這是一個普通博主的生活氣息。
荒腔走板
我們家之前有一個花市,距離只有 3 km,基本上每個月我們都會騎自行車過去逛一圈,採購點鮮切花。由於花市規模比較大,所以整體上物美價廉,量大管夠。
自從花市從去年年初的時候統一遷移到稍遠一點的地方後,因爲交通不便,我們就從來沒有去過了,家裏也很久沒有買鮮花了。
2024 年的第一個週末,終於把車提了。從去年 10 月到現在,也算是等得望穿秋水了。
所以,提車後的第一天,就帶着 Max 同學,也就是我老婆,以練車的名義往花市跑了一趟。
新的花市開在高架橋下,一字排開,長長的一條街,分爲了好幾個區。我們直奔鮮切花區,逛了一小時,花了 50 元錢,買了一束鮮花,一把臘梅,兩個花瓶。
還有熟悉的物美價廉,還是熟悉的量大管夠。
就是過去的路上有一段小路,雖然我已經是拿了駕照有十年的“老司機”了,但是確實沒怎麼開過車,手上汗水都開出來了。Max 同學坐在副駕,神情嚴肅,目光如炬,從頭到尾也就一句話:注意注意注意,慢點慢點慢點,剎車剎車剎車...