盤一盤這個沒資格出現在面試環節的場景題
你好呀,我是歪歪。
前兩天在一個技術群里看到有人拋出一張圖片,提出了這樣的一個問題:
請教一下,線程池可以做到根據任務的類型,來指定特定線程執行嗎?
圖片
了解了一下背景,是批量任務觸發,從訂單表中查詢出“處理中”狀態的訂單,訂單可能屬于不同的通道,所以需要調用不同通道的接口。
現在的方案是把訂單查出來之后,往線程池里面扔,在異步任務里面判斷當前訂單是屬于哪個通道,就調用哪個通道的查詢接口:
圖片
這是常規做法,看起來沒有毛病。
但是現在提問的這個哥們遇到了一個問題:有一個通道的查詢接口特別慢,會占著線程池里面的線程資源,影響了其他兩個通道的訂單查詢。
舉個極端的例子,比如你的線程池核心線程數就三個。
假設一共有 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 連接數等指標,進行綜合判斷,彈性擴容,也無需人工介入,手動擴容。
所以,朋友,這個事情告訴我們一個什么道理?
向上抽象問題的能力,把看看似不一樣的場景抽離成類似的問題模型的能力很重要。
還有,“一個通道慢需要進行資源隔離”這個問題的關鍵不在于“一個通道”上,雖然可以在通道層面做隔離,但是這樣并沒有抓住問題的關鍵。問題的關鍵在于“通道慢”,所以可以在“快慢”的維度上做隔離,這才是問題的關鍵。
關鍵問題,就是要找到問題的關鍵。
這也是我在這一次群聊的討論中學習到的東西。