什么是高并發(fā),從字面上理解,就是在某一時刻產(chǎn)生大量的請求,那么多少量稱為大量,業(yè)界并沒有標(biāo)準(zhǔn)的衡量范圍。原因非常簡單,不同的業(yè)務(wù)處理復(fù)雜度不一樣。
而我所理解的高并發(fā),它并不只是一個數(shù)字,而更是一種架構(gòu)思維模式,它讓你在面對不同的復(fù)雜情況下,從容地選擇不同的技術(shù)手段,來提升應(yīng)用系統(tǒng)的處理能力。
但是,并不意味應(yīng)用系統(tǒng)從誕生的那一刻,就需要具備強大的處理能力,這種做法并不提倡。要知道,脫離實際情況的技術(shù),會顯得毫無價值,甚至是一種浪費的表現(xiàn)。
言歸正傳,那高并發(fā)到底是一種怎樣的架構(gòu)思維模式,它對架構(gòu)設(shè)計又有什么影響,以及如何通過它來驅(qū)動架構(gòu)演進,讓我們接著往下讀,慢慢去體會這其中的精髓。
性能是一種基礎(chǔ)
在架構(gòu)設(shè)計的過程中,思考固然重要,但目標(biāo)更為關(guān)鍵。通過目標(biāo)的牽引力,可以始終確保推進方向,不會脫離成功的軌道。那高并發(fā)的目標(biāo)是什么,估計你的第一反應(yīng)就是性能。
沒錯,性能是高并發(fā)的目標(biāo)之一,它不可或缺,但并不代表所有。而我將它視為是高并發(fā)的一種基礎(chǔ)能力,它的能力高低將會直接影響到其他能力的取舍。例如:服務(wù)可用性,數(shù)據(jù)一致性等。
性能在軟件研發(fā)過程中無處不在,不管是在非功能性需求中,還是在性能測試報告中,都能見到它的身影。那么如何來衡量它的高低呢,先來看看常用的性能指標(biāo)。
每秒處理事務(wù)數(shù)(TPS)
每秒能夠處理的事務(wù)數(shù),其中T(Transactions)可以定義不同的含義,它可以是完整的一筆業(yè)務(wù),也可以是單個的接口請求。
每秒請求數(shù)(RPS)
每秒請求數(shù)量,也可以叫做QPS,但它與TPS有所不同,前者注重請求能力,后者注重處理能力。不過,若所有請求都在得到響應(yīng)后再次發(fā)起,那么RPS基本等于TPS。
響應(yīng)時長(RT)
從發(fā)出請求到得到響應(yīng)的耗時,一般可以采用毫秒單位來表示,而在一些對RT比較敏感的業(yè)務(wù)場景下,可以使用精度更高的微秒來表示。
并發(fā)用戶數(shù)(VU)
同時請求的用戶數(shù),很多人將它與并發(fā)數(shù)畫上等號,但兩者稍有不同,前者關(guān)注客戶端,后者關(guān)注服務(wù)端,除非每個用戶僅發(fā)送一筆請求,且請求從客戶端到服務(wù)端沒有延遲,同時服務(wù)端有足夠的處理線程。
以上都是些常用的性能指標(biāo),基本可以覆蓋80%以上的性能衡量要求。但千萬不要以單個指標(biāo)的高低來衡量性能。比如:訂單查詢TPS=100萬就認(rèn)為性能很高,但RT=10秒。
這顯然毫無意義。因此,建議同時觀察多個指標(biāo)的方式來衡量性能的高低,大多數(shù)情況下主要會關(guān)注TPS和RT,而你可以將TPS視為一種水平能力,注重并行處理能力,將RT視為一種垂直能力,注重單筆處理能力,兩者缺一不可。
接觸過性能測試的同學(xué),可能會見過如下這種性能測試結(jié)果圖,圖中包含了剛才提到過的三個性能指標(biāo),其中橫坐標(biāo)為VU,縱坐標(biāo)分別為TPS和RT。
注:表格中的數(shù)據(jù)都是理想情況下的,實際上會有上下抖動,而這里只是為了想用它來解釋一種現(xiàn)象而已。
圖中的兩條曲線,在不斷增加VU的情況下,TPS不斷上升,但RT保持穩(wěn)定,但當(dāng)VU增加到一定量級的時候,TPS開始趨于穩(wěn)定,而RT不斷上升。
如果你仔細觀察,還會發(fā)現(xiàn)一個奇妙的地方,當(dāng)RT=25ms時,它們?nèi)叽嬖谥撤N關(guān)系,即:TPS=VU/RT。但當(dāng)RT>25ms時,這種關(guān)系似乎被打破了,這里暫時先賣個關(guān)子,稍后再說。
根據(jù)表格中的數(shù)據(jù),性能測試報告結(jié)論:最大TPS=65000,當(dāng)RT=25ms(最短)時,最大可承受VU=1500。
感覺有點不對勁,用剛才的公式來驗證一下,1500/0.025s=60000,但最大卻是TPS=65000。那是因為,當(dāng)VU=1500時,應(yīng)用系統(tǒng)的使用資源還有空間。
再來觀察一下表格中的數(shù)據(jù),VU從1500增加到1750時,TPS繼續(xù)上升,且到了最大值65000。此時,你是不是會理解為當(dāng)VU增加到1750時,使用資源被耗盡了。話雖沒錯,但不嚴(yán)謹(jǐn)。
注:使用資源不一定是指硬件資源,也可能是其他方面,例如:應(yīng)用系統(tǒng)設(shè)置的最大處理線程。
其實在VU增加到1750前,使用資源就已飽和,那如何來測算VU的臨界值呢。你可以將最大TPS作為已知條件,即:VU=TPS*RT,65000*0.025s=1625。也就是說,當(dāng)VU=1625時,使用資源將出現(xiàn)瓶頸。
調(diào)整性能測試報告結(jié)論:最大TPS=65000,當(dāng)RT=25ms(最短)時,最大可承受VU=1625。
有人會問,表格中的RT是不是平均值,首先回答為是。不過,高并發(fā)場景對RT會特別敏感,所以除了要考慮RT的平均值外,建議還要考慮它的分位值,例如:P99。
舉例:假設(shè)1000筆請求,其中900筆RT=23ms,50筆RT=36ms,50筆RT=50ms
平均值 |
P99值 |
P95值 |
P90值 |
25ms |
50ms |
36ms |
23ms |
P99的計算方式,是將1000筆請求的RT從小到大進行排序,然后取排在第99%位的數(shù)值,基于以上舉例數(shù)據(jù)來進行計算,P99=50ms,其他分位值的計算方式類似。
再次調(diào)整性能測試報告結(jié)論:最大TPS=65000,當(dāng)RT(平均)=25ms(最短)時,最大可承受VU=1625,RT(P99)=50ms,RT(P95)=36ms,RT(P90)=23ms。
在非功能性需求中,你可能會看到這樣的需求,性能指標(biāo)要求:RT(平均)<=30。結(jié)合剛才的性能測試報告結(jié)論,當(dāng)RT(平均)=25ms(最短)時,最大可承受VU=1625。那就等于在RT上還有5ms的容忍時間。
既然是這樣的話,那我們不妨就繼續(xù)嘗試增加VU,不過RT(平均)會出現(xiàn)上升,但只要控制不要上升到30ms即可,這是一種通過犧牲耗時(RT)來換取并發(fā)用戶數(shù)(VU)的行為。但請不要把它理解為每筆請求耗時都會上升5ms,這將是一個嚴(yán)重的誤區(qū)。
RT(平均)的增加,完全可能是由于應(yīng)用系統(tǒng)當(dāng)前沒有足夠的使用資源來處理請求所造成的,例如:處理線程。如果沒有可用線程可以分配給請求時,就會將這請求先放入隊列,等前面的請求處理完成并釋放線程后,就可以繼續(xù)處理隊列中的請求了。
那也就是說,沒有進入隊列的請求并不會增加額外的耗時,而只有進入隊列的請求會增加。那么進入隊列的請求會增加多少耗時呢,在理想情況下(RT恒定),可能會是正常處理一筆請求耗時的倍數(shù),而倍數(shù)的大小又取決于并發(fā)請求的數(shù)量。
假設(shè)最大處理線程=1625,若每個用戶僅發(fā)送一筆請求,且請求從客戶端到服務(wù)端沒有延遲的條件下,當(dāng)并發(fā)用戶數(shù)=1625時,能夠保證RT=25ms,但當(dāng)并發(fā)用戶數(shù)>1625時,因為線程只能分配給1625筆請求,那多余的請求就無法保證RT=25ms。
超過1625筆的請求會先放入隊列,等前面1625筆請求處理完成后,再從隊列中拿出最多1625筆請求進行下一批處理,如果隊列中還有剩余請求,那就繼續(xù)按照這種方式循環(huán)處理。
進入隊列的請求,每等待一批就需要增加前一批的處理耗時。在理想情況下,每一批都是RT=25ms,如果這筆請求在隊列中等待了兩批,那就要額外增加50ms的耗時。
VU |
第一批 |
第二批 |
第三批 |
1000 |
1000筆請求 |
|
|
2000 |
1625筆請求 |
375筆請求 |
|
4000 |
1625筆請求 |
1625筆請求 |
750筆請求 |
因此,并不能簡單通過VU=TPS*RT=65000*0.03=1950來計算最大可承受VU。而是需要引入一種叫做科特爾法則(Little’s Law)的排隊模型來估算,不過由于這個法則比較復(fù)雜,這里暫時不做展開。
通過粗略估算后,VU大約在2032,我們再對這個值用上述表格中再反向驗算一下。
VU |
第一批 |
第二批 |
RT(平均) |
2032 |
1625筆請求 |
407筆請求 |
(1625*25+407*50)/(1625+407)≈30ms |
最終調(diào)整性能測試報告結(jié)論:最大TPS=65000,當(dāng)RT(平均)=25(最短)時,最大可承受VU=1625,RT(P99)=50,RT(P95)=36,RT(P90)=23;當(dāng)RT(平均)=30(容忍)時,(理想情況)最大可承受VU=2032,RT(P99)=RT(P95)=50,RT(P90)=25。
這就解釋了為什么當(dāng)RT>25ms時,VU=TPS*RT會不成立的原因。不過,這些都是在理想情況下推演出來的,實際情況會比這要復(fù)雜得多。
所以,還是盡量采用多輪性能測試來得到性能指標(biāo),這樣也更具備真實性。畢竟影響性能的因素實在太多且很難完全掌控,任何細微變化都將影響性能指標(biāo)的變化。
到這里,我們已經(jīng)了解了可以用哪些指標(biāo)來衡量性能的高低。不過,這里更想強調(diào)的是,性能是高并發(fā)的基礎(chǔ)能力,是實現(xiàn)高并發(fā)的基礎(chǔ)條件,并且你需要有側(cè)重性地提升不同維度的性能指標(biāo),而非僅關(guān)注某一項。
限制是一種設(shè)計
上文說到,性能是高并發(fā)的目標(biāo)之一。追求性能沒有錯,但并非永無止境。想要提升性能,勢必投入成本,不過它們并不是一直成正比,而是隨著成本不斷增加,性能提升幅度逐漸衰減,甚至可能不再提升。所以,有時間我們要懂得適可而止。
思考一下,追求性能是為了解決什么問題,至少有一點,是為了讓應(yīng)用系統(tǒng)能夠應(yīng)對突發(fā)請求。換言之,如果能解決這個問題,是不是也算實現(xiàn)了高并發(fā)的目標(biāo)。
而有時候,我們在解決問題時,不要總是習(xí)慣做加法,還可以嘗試做減法,架構(gòu)設(shè)計同樣如此。那么,如何通過做減法的方式,來解決應(yīng)對突發(fā)請求的問題呢。讓我們來講講限制。
限制,從狹義上可以理解為是一種約束或控制能力。在軟件領(lǐng)域中,它可以針對功能性或非功能性,而在高并發(fā)的場景中,它更偏向于非功能性。
限制應(yīng)用系統(tǒng)的處理能力,并不代表要降低應(yīng)用系統(tǒng)的處理能力,而是通過某些控制手段,讓突發(fā)請求能夠被平滑地處理,同時起到應(yīng)用系統(tǒng)的保護能力,避免癱瘓,還能將應(yīng)用系統(tǒng)的資源進行合理分配,避免浪費。
那么,到底有哪些控制手段,既能實現(xiàn)以上這些能力,又能減少對客戶體驗上的影響,下面就來介紹幾種常用的控制手段。
第一招:限流
限流,是在一個時間窗口內(nèi),對請求進行速率控制。若請求達到提前設(shè)定的閾值時,則對請求進行排隊或拒絕。常用的限流算法有兩種:漏桶算法和令牌桶算法。
漏桶算法,所有請求先進入漏桶,然后按照一個恒定的速率對漏桶里的請求進行處理,是一種控制處理速率的限流方式,用于平滑突發(fā)請求速率。
它的優(yōu)點是,能夠確保資源不會瞬間耗盡,避免請求處理發(fā)生阻塞現(xiàn)象,另外,還能夠保護被應(yīng)用系統(tǒng)所調(diào)用的外部服務(wù),也免受突發(fā)請求的沖擊。
它的缺點是,對于突發(fā)請求仍然會以一個恒定的速率來進行處理,其靈活性會較弱一點,容易發(fā)生突發(fā)請求超過漏桶的容量,導(dǎo)致后續(xù)請求直接被丟棄。
令牌桶算法,應(yīng)用系統(tǒng)會以一個恒定的速率往桶里放入令牌,請求處理前,會從桶里獲取令牌,當(dāng)桶里沒有令牌可取時,則拒絕服務(wù),是一種平均流入速率的限流方式。
它的優(yōu)點是,在限制平均流入速率的同時,還能在面對突發(fā)請求的情況下,確保資源被充分利用,不會被閑置或浪費。
它的缺點是,舍棄了處理速率的強控制能力,那么如果某些功能依賴外部服務(wù),可能將會讓外部服務(wù)無法承受壓力,導(dǎo)致無法正常返回,而且還浪費了這次獲取的令牌。
綜上,兩種算法并沒有絕對的好壞,而是需要根據(jù)實際的情況,選擇合適的方式,從而在發(fā)揮限流作用的同時不會引發(fā)其他問題。但在一些秒殺活動中,軟件黨的高頻請求,會很容易觸發(fā)限流,導(dǎo)致大量正常請求被誤殺的問題。
雖然在請求被限流后,會返回友好話術(shù),減輕對客戶體驗的影響,但也有可能他們的請求,會一直無法得到有效處理,這時候耐心再好的客戶也會離開及抱怨。
所以,我們除了使用限流這招外,還得搭配其他的招數(shù)組合一起使用,從而讓應(yīng)用系統(tǒng)能夠?qū)Y源進行合理分配,避免資源浪費,減少正常請求被誤殺的情況。
第二招:降頻
降頻,是在一個時間窗口內(nèi),對同一特征的請求進行速率控制。若請求達到提前設(shè)定的閾值時,則會對請求進行拒絕。
雖然和限流有點類似,但存在著細微的差別。對限流而言,它并不關(guān)心請求方,而只對服務(wù)端的速率進行控制,而對降頻而言,它會基于某種特征,對請求方的請求速率進行控制。
而降頻的目的,是為了減少應(yīng)用系統(tǒng)資源被不正常的請求所消耗,而導(dǎo)致正常的請求因限流被拒絕的情況發(fā)生。它的實現(xiàn)方式也有多種,而且在前端和后端都可以使用。
識別不正常的請求是降頻的第一步,也是最關(guān)鍵的一步。一般會制定某種特征+某段時間+請求數(shù)量這種三段式的識別規(guī)則。
特征可以是賬號、會話、IP地址、設(shè)備號等,時間一般會是1秒,也可以設(shè)置更長。賬號+1秒+5筆,意思就是同一個賬號在1秒內(nèi)可以發(fā)生5筆請求,但是這里請求數(shù)量與限流的設(shè)定參考依據(jù)不同。
限流大小主要依據(jù)性能來決定,而降頻中的請求數(shù)量,一般會以正常人的交互速率作為參考。所以,并不能因為性能好,就設(shè)定賬號+1秒+100筆這種識別規(guī)則,這不但不科學(xué)還會浪費資源。
接下來,有了識別規(guī)則還得搭配對應(yīng)的處置手段,常見的有兩種模式:挑戰(zhàn)和拒絕。
挑戰(zhàn) |
彈出驗證碼,輸入并驗證通過后,可以繼續(xù)請求 |
僅適用于前端 |
拒絕 |
彈出“請求頻繁”提示,且這筆請求將直接被拒絕 |
適用于前端及后端 |
限流會發(fā)生誤殺,難道降頻就不會嗎,其實也會發(fā)生,特別是用戶的網(wǎng)絡(luò)環(huán)境是一個出口IP地址時。所以,如果是基于IP地址特征的識別規(guī)則,請求數(shù)量建議適當(dāng)放大。
在降頻策略方面,建議配置多層+漸進式的方式,識別規(guī)則較為嚴(yán)格的采用挑戰(zhàn)模式,識別規(guī)則較為寬松的采用拒絕模式,減少因降頻而引發(fā)的誤殺情況,參考如下:
優(yōu)先級 |
識別規(guī)則 |
處置手段 |
1 |
賬號+1秒+5筆 |
挑戰(zhàn) |
2 |
賬號+1秒+10筆 |
拒絕 |
3 |
IP地址+1秒+20筆 |
挑戰(zhàn) |
4 |
IP地址+1秒+40筆 |
拒絕 |
降頻確實可以使應(yīng)用系統(tǒng)的資源,被合理地分配給請求方,但并不能保證萬無一失,特別對于那些技術(shù)高超的軟件黨們,他們?nèi)匀豢梢酝ㄟ^其他方式繞開這種控制手段。
不過,你可以將此視為一種攻防戰(zhàn),通過增強防守的方式,來提高攻擊者成本,而攻擊者一定會權(quán)衡成本和收益,當(dāng)成本大于收益時,可能就不會有攻擊,畢竟沒有人會這么無聊透頂。
雖然有了限流和降頻這兩招,但仍可能無法應(yīng)對高并發(fā)的場景,況且在初期,限流和降頻的策略,也無法設(shè)計得非常完美。所以,有些時候還得使出最后一招。
第三招:降級
降級,是當(dāng)應(yīng)用系統(tǒng)處理超載時,對其服務(wù)進行裁剪的一種機制。常見的是應(yīng)用系統(tǒng)處理阻塞時,會關(guān)閉非核心服務(wù),并將資源給到核心服務(wù),從而確保核心服務(wù)正常。
經(jīng)常有人將它與熔斷混為一談,但并非一回事。降級主要是針對應(yīng)用系統(tǒng)本身,若處理能力不足則可觸發(fā),而熔斷主要是針對應(yīng)用系統(tǒng)所調(diào)用的外部服務(wù),若外部服務(wù)不穩(wěn)定時則可觸發(fā)。
當(dāng)然,兩者也有一定的關(guān)系,因為當(dāng)發(fā)生熔斷時,也可以觸發(fā)降級機制,比如當(dāng)同步調(diào)用外部服務(wù)出現(xiàn)性能問題時,可以降級為異步調(diào)用,避免造成線程阻塞而癱瘓。
不過在降級前,必須得先梳理應(yīng)用系統(tǒng)中的核心服務(wù),可以采用經(jīng)典的二八原則,將服務(wù)劃分為20%核心服務(wù)+80%非核心服務(wù)。而這種分法的意圖,是希望讓你找到真正重要的核心服務(wù),不然,你會覺得都很重要。
在梳理過程中,建議通過多個維度來進行綜合評判,如下是我經(jīng)常采用的一種梳理方法,你可以將此作為一種參考,并結(jié)合自己的服務(wù)分類標(biāo)準(zhǔn)進行調(diào)整。
首先,可以設(shè)計一張類似如下的矩陣圖,請盡量地簡約它,將應(yīng)用系統(tǒng)中的各類服務(wù),按照矩陣所設(shè)定的不同屬性進行分門別類。
|
操作類 |
查詢類 |
業(yè)務(wù)類 |
訂單下單 |
商品查詢 |
基礎(chǔ)類 |
用戶登錄 |
用戶查詢 |
然后,將業(yè)務(wù)類+操作類的挑選出來作為核心服務(wù),你會不會認(rèn)為這就結(jié)束了。不好意思,游戲才剛剛開始。不過你可以試想一下,假設(shè)僅保留這些核心服務(wù),會出現(xiàn)什么問題。
用戶登錄不了無法訂單支付,訂單查詢不了無法訂單退貨。所以,我們還需引入服務(wù)關(guān)鍵路徑的概念,可以理解為在使用某個服務(wù)前,還必須要使用的其他服務(wù)。
分別對挑選出來的核心服務(wù),進行服務(wù)關(guān)鍵路徑的梳理。
路徑1:用戶登錄——>商品查詢——>訂單下單
路徑2:用戶登錄——>商品查詢——>訂單下單——>訂單支付
路徑3:用戶登錄——>訂單查詢——>訂單退貨
待服務(wù)關(guān)鍵路徑梳理完成后,再對路徑上的所有服務(wù)進行合并及去重,將會得到一組新的核心服務(wù):用戶登錄/商品查詢/訂單下單/訂單支付/訂單查詢/訂單退貨。
來計算下核心服務(wù)的占比,所有服務(wù)14個/核心服務(wù)6個,占42.86%,遠遠超過了20%。所以,建議繼續(xù)從這些核心服務(wù)中,識別更核心的部分,但仍然以服務(wù)關(guān)鍵路徑為整體。
相比訂單下單/訂單支付/訂單退貨這三條服務(wù)關(guān)鍵路徑,我想訂單支付可能會更有價值。最后,我們可以僅將訂單支付這條服務(wù)關(guān)鍵路徑上的服務(wù)作為核心服務(wù)。
重新再來計算下核心服務(wù)的占比,所有服務(wù)14個/核心服務(wù)4個,占28.57%,雖然還是超過了20%,但這并不是重點,重點是我們已經(jīng)找到了最核心的服務(wù)。
其余的核心服務(wù),可以降級為準(zhǔn)核心服務(wù),重組后得到如下這份服務(wù)重要程度清單。
核心服務(wù) |
準(zhǔn)核心服務(wù) |
非核心服務(wù) |
用戶登錄 |
訂單查詢 |
退貨進度 |
當(dāng)擁有這份清單后,若應(yīng)用系統(tǒng)處理阻塞時,就可以按照非核心服務(wù)>準(zhǔn)核心服務(wù)>核心服務(wù)這個順序依次進行降級。不過,降級不一定要拒絕請求,也可以是限流請求,這樣可以減少對服務(wù)能力的裁剪力度。
以上只是一種相對較粗的降級策略,如果你想要制定更精細化的降級策略,還需要對每個服務(wù)進行優(yōu)先級的設(shè)定,高低依據(jù)可以結(jié)合自身需要來制定,例如:歷史服務(wù)使用情況。
當(dāng)有了限流、降頻、降級這三招,基本就能夠在資源有限的情況下,讓突發(fā)請求能夠被平滑地處理,將應(yīng)用系統(tǒng)的資源能夠被合理地分配,以及當(dāng)應(yīng)用系統(tǒng)處理堵塞時,確保核心服務(wù)正常。
取舍是一種權(quán)衡
現(xiàn)在,我們已經(jīng)基本了解了如何衡量性能的指標(biāo),以及大致掌握了如何保護應(yīng)用系統(tǒng)的招數(shù)。但這些就是高并發(fā)全部了嗎,我想說這僅僅只是入門級別。
上述內(nèi)容,主要是為了讓你能夠清晰地看到應(yīng)用系統(tǒng)的性能水位在哪里,以及在資源有限下,當(dāng)面對突發(fā)請求時可以采取哪些招數(shù),能讓應(yīng)用系統(tǒng)安全地存活下來。
存活,即代表著可用性,它也是高并發(fā)的一個特性,而且是我認(rèn)為相對比較重要的特性。設(shè)想一下,如果你的應(yīng)用系統(tǒng)連可用性都無法保證,那再高的性能又有何意義。
對于大部分應(yīng)用系統(tǒng)而言,大家都會比較關(guān)注應(yīng)用系統(tǒng)的可用性,99.9%不夠那就99.99%,甚至還有想做到99.999%的,畢竟可用性的不足會直接影響到業(yè)務(wù)運作。
但對于一個想成為高并發(fā)的應(yīng)用系統(tǒng)而言,僅單方面關(guān)注高可用,肯定無法稱得上這個頭銜,它仍然還需要在其他特性上具備極佳的表現(xiàn)。
讓我們拉回到最初的目標(biāo),性能是高并發(fā)的目標(biāo)之一,不過,這里我們不再談?wù)撔阅苤笜?biāo),而是來研究如何來提升性能。因為,高性能是高并發(fā)的另外一個重要特性。
想要提升性能,勢必投入成本。隨著成本不斷增加,性能提升幅度逐漸衰減。這兩句話,是不是覺得有點耳熟,但不管你是否還記得,先讓我在這里打個問號再說。
在架構(gòu)設(shè)計的過程中,你是否經(jīng)常會聽到“取舍”的這個詞,它是通過犧牲一種能力來換取另外一種能力的方式,這些能力可以是性能、可用性、數(shù)據(jù)一致性或是其他能力。
等等,你是不是突然有意識到,提升性能并不只有投入成本這種方式,至少是在硬件資源方面,我們還可以通過犧牲一種能力去換取。那到底選擇犧牲哪種能力呢,犧牲可用性,一般不會第一時間考慮,那是不是可以考慮犧牲數(shù)據(jù)一致性。
但在考慮前得先聲明一下,所謂犧牲數(shù)據(jù)一致性,并不是完全不要,而是將數(shù)據(jù)強一致性降級為最終一致性。而對于數(shù)據(jù)最終一致性的理解,就是在數(shù)據(jù)更新后,要過一段時間后才能看到,而時間的長短就代表著犧牲了多少。
但并不是說,所有情況都必須犧牲數(shù)據(jù)一致性來提升性能,有些時候也可以考慮犧牲其他能力。但在取舍前,得先弄清楚當(dāng)前要什么,但更需要弄清楚當(dāng)前可以失去什么,不合理的取舍,不但無法換取收益,反而還會引來更多的問題。
情況1:數(shù)據(jù)緩存
緩存,是高并發(fā)架構(gòu)設(shè)計中一種不可或缺的能力,一般是指那些經(jīng)常被訪問的熱點數(shù)據(jù),可以將它放入緩存中,從而提升數(shù)據(jù)被讀取的效率。
但是否所有的數(shù)據(jù)都適合放入緩存中,如果是靜態(tài)數(shù)據(jù),那么你可以很安心地放入。原因很簡單,靜態(tài)數(shù)據(jù)不會更新,那么緩存和數(shù)據(jù)源始終保持一致,而且就算緩存中的數(shù)據(jù)丟失了,至少還有一份在數(shù)據(jù)源。
通過將靜態(tài)數(shù)據(jù)緩存,可以很輕易地提升靜態(tài)數(shù)據(jù)的訪問性能,甚至可能是幾十倍的效果。但應(yīng)用系統(tǒng)中還有大量的動態(tài)數(shù)據(jù),僅提升靜態(tài)數(shù)據(jù)可能對總體的提升并不一定顯著。
你是不是想說,那就把動態(tài)數(shù)據(jù)也放入緩存中不就行了。在下這個決定前,建議你先想一下,動態(tài)數(shù)據(jù)是會更新的,這就意味著動態(tài)數(shù)據(jù)在放入緩存后,當(dāng)數(shù)據(jù)源中的數(shù)據(jù)被更新后,再次訪問返回的都是更新前的數(shù)據(jù),這種效果你是否可以接受。
我想應(yīng)該沒有人會接受吧,而你是不是又想說,設(shè)置下緩存會過期不就能解決了。沒錯,但得等過期后才能解決,那還沒過期前呢,這種方式只能緩解,但并不能根治,而且還會引入一個新的問題,請問過期設(shè)置多久才合適。
設(shè)置緩存5秒鐘過期,可能永遠無法命中緩存,而且不但沒有提升性能,還增加了代碼復(fù)雜度,有點畫蛇添足的感覺。設(shè)置緩存5分鐘過期,命中緩存的幾率可能會提高,但緩存后在5分鐘內(nèi)的如果數(shù)據(jù)更新,要從緩存開始往后推5分鐘才能看到。
即:第1分鐘緩存,第1-6分鐘內(nèi)的任何數(shù)據(jù)更新,要第6分鐘后才能看到。
所以,如果你無法容忍這種情況,請你不要濫用緩存,雖然性能提高了,但問題可能也出現(xiàn)了。反之,如果你可以容忍這種情況,那就可以這么操作,而至于過期設(shè)置多久,可以結(jié)合業(yè)務(wù)場景及使用頻率綜合來評估,畢竟不同的業(yè)務(wù)容忍度是不同的。
對于是否要將動態(tài)數(shù)據(jù)進行緩存,本質(zhì)上,其實就是一種取舍,是一種性能與數(shù)據(jù)一致性的權(quán)衡,而緩存的過期時長,就像是保持這種平衡的支點,從而讓這種犧牲變得更有意義。
情況2:單機限流
限流,前面已經(jīng)有介紹過,它有兩種常用的限流算法,漏桶算法和令牌桶算法。不過,這兩種算法都僅支持單機限流,不支持全局限流。
單機限流,就是對單節(jié)點設(shè)定一個限流閾值,如果單節(jié)點上的請求到達閾值,則會拒絕請求。例如:限流閾值=每秒100次請求,如果在1秒內(nèi)單節(jié)點上,有第101次的請求則拒絕。
全局限流,就是對一組節(jié)點設(shè)定一個限流總閾值,如果這組節(jié)點上的匯總請求到達閾值,則會拒絕請求。例如:10個節(jié)點的限流總閾值=1000次請求,如果在1秒內(nèi)這組節(jié)點上,匯總有第1001次請求則拒絕,不過單節(jié)點上有超過第100次的請求也會接受。
這么看下來,感覺單機限流控制能力更厲害一點,它能保證單節(jié)點的請求不會超過100次。而全局限流在極端情況下,單節(jié)點都有可能在1秒內(nèi)會接受1000次請求。當(dāng)然,這種情況的可能性比較低,比如在突發(fā)請求時,9個節(jié)點同時宕機。
既然如此,那全局限流有存在的意義嗎,難道這就是漏桶算法和令牌桶算法都不支持全局限流的原因。全局限流就真的沒有存在的意義嗎。存在即合理,既然存在,那就一定有它存在的道理。
換個情況,還是將10個節(jié)點為一組,不過這次換成采用單機限流。問題來了,每個節(jié)點的限流閾值該如何設(shè)定,如果采用平均分配,則限流閾值=每秒100次請求,讓我們來測試一下,在1秒內(nèi)依次發(fā)出1000次請求,會發(fā)生什么現(xiàn)象。
結(jié)果是在第100次請求后,從第101次到第1000次的請求中,可能有些請求會發(fā)生被拒絕的情況,而且請求一會兒成功一會兒拒絕,沒有任何規(guī)律。原因可能是10個節(jié)點請求負(fù)載不均所引發(fā)的,導(dǎo)致某個節(jié)點提前超過了100次請求。
基于以上情況,最終1000次請求沒有全部成功,這種情況等同于降低了應(yīng)用系統(tǒng)的吞吐能力。而在實際情況中,就算采用輪詢的負(fù)載算法,請求數(shù)不均的可能性仍然還是會存在的。這么一看,單機限流好像也有缺陷。
估計你已經(jīng)被我說暈了吧,讓我們整體再重新梳理一遍,并對兩種不同限流模式的影響進行對比。不過,這次還加上每秒不同的請求數(shù)量。
每秒請求 |
單機限流 |
全局限流 |
100筆 |
無影響 |
無影響 |
1000筆 |
少量請求拒絕 |
耗時小幅波動 |
10000筆 |
請求拒絕>9000 |
請求拒絕=9000 |
注:每個節(jié)點的處理能力為100筆/秒
兩種限流對比下來,單機限流更強調(diào)單機的控制范圍,但可能會造成額外的請求拒絕,但對單節(jié)點不會造成性能壓力,而全局限流更強調(diào)整體的控制范圍,雖不會造成額外的請求拒絕,但可能會對單節(jié)點造成性能壓力,引發(fā)性能過載。
除此之外,全局限流還是一種采用中心化的設(shè)計思路,因此在網(wǎng)絡(luò)開銷方面,還會產(chǎn)生額外的性能損耗,這種損耗在請求量少的時候估計還可以容忍,但在高并發(fā)的情況下可能是場災(zāi)難,因為在每次限流判斷前,還會產(chǎn)生一次網(wǎng)絡(luò)開銷。
所以,不能為了想要實現(xiàn)更精準(zhǔn)的限流,就盲目地采用全局限流,它將在高并發(fā)的情況下?lián)p耗更多的性能。而單機限流所額外造成的少量請求拒絕,在某些情況下,可以考慮采用某些技術(shù)手段進行補償。
不過,不管是單機限流還是全局限流,似乎都和數(shù)據(jù)一致性沒有關(guān)系。但事實上,全局限流這種精準(zhǔn)限流的方式,也可以視為另一種一致性的表現(xiàn),而單機限流就是通過對這種一致性的犧牲,來減少性能損耗,何嘗不是提升性能的另一種方式。
以上,只是簡單列舉了兩種不同情況下的取舍,而在高并發(fā)架構(gòu)上,可取舍的地方遠不止這些。你得知道,高并發(fā)的每一處設(shè)計或每一份設(shè)計方案的背后,都曾是通過不斷地取舍所獲得的,而沒有取舍的高并發(fā)架構(gòu)決策,將會顯得毫無說服力。
取舍不但可以作為高并發(fā)架構(gòu)決策的有力武器,也將是驅(qū)動架構(gòu)演進最合理的一種方式。但要切記,取舍的方向并不是一成不變的,而是會隨著外界環(huán)境的變化而變化,它將是一種獨特的藝術(shù)。
寫在最后
高并發(fā)的魅力之處,就在于它沒有唯一的答案,而答案是需要我們以不同的業(yè)務(wù)場景作為線索去不斷地尋找,這種尋找的過程也是一種不斷思考的過程,這就是我為什么說高并發(fā)是一種架構(gòu)思維模式。
本文從淺到深依次講述了性能是實現(xiàn)高并發(fā)的基礎(chǔ)條件,控制是實現(xiàn)資源最大化利用的方式,以及如何通過取舍來換取當(dāng)前應(yīng)用系統(tǒng)更所需的能力,但這些僅僅只是高并發(fā)世界里的一個角落。因篇幅有限,今天就暫告一段落。
最后想說,高并發(fā)其實并不可怕,可怕的是你知其然而不知其所以然。對于追求技術(shù)的你,需要不斷地拓寬你的技術(shù)深度與廣度,才能更好地掌握高并發(fā),以及運用高并發(fā)的思維模式來提升應(yīng)用系統(tǒng)處理能力。