負載大逃亡:四十二路怪獸聯軍及七條逃生法則
盡管你很精心的“烹制”你的應用程序,但是隨著負載的增加,所有災難都將降臨。當然你可以使用橫向擴展或縱向擴展,但是你同樣可以更好的進行編程,讓你的系統可以支撐更大的負載。這會給你節約成本,因為可以減少所添加的服務器數量,同樣還可以提高整個應用程序的可靠性和響應速度。同時,這也應該是優秀工程師的分內之事。
下面一睹Tod Hoff在 High Scalabilty上帶來的42怪獸軍團崢嶸:
大量的對象
一旦對象數量太多,我們都會面臨擴展問題。顯然隨著對象數量的劇增,可以為各種類型對象使用的資源將愈加捉襟見肘。
故障得不到恢復會導致無限的事件流
在大型網絡故障的情景下,不會存在任何時間做系統恢復,系統將一直處于重負之下。
大量的高優先級工作
舉個例子,路由的重定向就是個高優先級活動。如果存在大量既不可以被卸載又不可以被降級的路由重定向,資源將不斷的被消耗,用于支撐這些高優先級工作。
數據流增大
隨著數據體積的增大,系統負載將加重。隨著請求源的增多,系統負載將加重。
功能蔓延(Feature Creep)
隨著更多超過預期的特性添加,系統中的漏洞將會出現。
客戶端的劇增
更多客戶端意味著更多資源的占用。更多的線程被創建用于驅動事件。更多的內存在客戶端請求隊列上被占用,更多網絡帶寬被用于通信,每個客戶端數據更需要專門的維護。#p#
不完善的設計決策
不完善的設計可能會導致擴展隱患。
·設計不足以處理大量對象
·缺乏終端到終端的應用級流控制(flow control)
·應用級重試導致消息的再分配和發送
·常用內存的占用比例
·不是真正可靠的發布
·特殊數據結構的過度內存消耗
·消息協議不能覆蓋所有錯誤場景
·使用硬盤作為存儲
·磁盤同步中不使用塊復制
·應用級協議還可以做的更好
·低功率CPU應對更多功能帶來的負載增加
·操作系統不支持過程體系結構(process architecture)
·硬件缺少對操作的支持,甚至是簡單的單消息推送
·常見網絡問題,比如:當負載增加時,ARP故障
假設是無效的
大量無效的假設,比如:預期中的內存使用、某個操作/處理會持續多長時間、什么地方會產生響應超時、某個時間點會消耗多少資源、會發生什么類型的失敗、系統中不同點的延時、請求隊列的長度等等。
內存不足
隨著負載的增加:系統的基本使用內存和峰值使用內存都會增加,這可能會導致OOM。
CPU饑餓(CPU Starvation)
更多的對象需要更長的時間去處理,因為它們必須要對更多的對象進行操作。這將降低CPU的有效性,同時其它的操作也可能會餓死。一個地方的饑餓會導致另一個地方也發生類似的情況。也可能會出現沒有足夠的CPU去處理需要立刻執行的工作,這可能是因為工作數量太多或者是特定情況下需要做大量優先級高的工作。
原始的資源使用增加
更多的對象占用更多的資源。如果有人希望支持1000萬個對象,你可能就無法實現,因為你根本沒有足夠的內存。
隱式的資源使用量增加
大部分特性都需要耗費原始資源之外更多的資源,比如:對比之前你只將對象儲存在一個表中,你現在將對象存入了兩個表,那么耗費的資源將是之前的兩倍。同樣還可能會在以下方面耗費更多的資源:隊列的長度、磁盤空間、二次數據拷貝增加的時間、將數據加載到應用程序中的時間、用于處理這些操作的CPU使用率、引導時間等等。#p#
只知道瘋狂的壓榨CPU,對系統無真正的認知
無限工作流在許多新型系統中都有實現。Web服務器和應用程序服務器支撐著非常大的用戶群體,不斷的新工作會帶來無限的事件流?;?×24小時永無止境的新工作,CPU使用率很容易的就會被推倒100%。
通常情況下100%的CPU使用率會被當作一個不好的標志。為了應對,我們建立了復雜的基礎設施、機器集群、冗余用于負載的平衡。
因為CPU不會說它很累,所以你可能一直讓其處于滿載狀態。在服務器領域中,我們為了得到需要的響應時間通常會盡可能的壓榨CPU;想法就是:如果我們不讓CPU***效率的運轉,新的工作可能就會得不到理想的延時,舊的工作也不可能盡快的完成。
然而一直將CPU壓榨到100%真的沒問題嗎?真正的問題就在于我們使用CPU有效性和任務優先級的方法:在系統架構中我們只對系統做簡單的認知,而不是弄懂系統的低等級工作流,然后使用這個信息制定合適的調度決策。
除了基于負載平衡服務器做笨拙的架構決策,以及猜測會使用到的線程數量及這些線程的優先次序,我們并沒有使用工具做任何對架構有益的事。
擴展一個系統首先要對其做深刻的認知,而當前的框架很少有說明應用程序的運行機制。
延時增加
隨著負載增加(規模變大),延時可能和你想象的完全不同。CPU饑餓是導致這個問題的主要原因。
錯誤的任務優先級
低負載下制定的優先級方案可能完全不適用于高負載情況。這種情況在差的流程機制下尤為明顯:給高優先級的任務指定一個低的優先級,會導致速度下降以及增加內存使用,因為低優先級任務得到運行的機會很小。
隊列長度不夠
大量的對象意味著更多的同步操作,從而隊列的長度將遠過于前。
引導時間變長
更多的對象將需要更長的時間從磁盤加載到內存中,因此引導時間必然變長。
同步時間變長
對象越多,應用程序越需要更長的時間去完成相互之間的同步。
大場景下的測試不夠
隨著結構變大,測試步驟所需要的開銷將越來越大,所以在大型結構下測試的時間很少。在開發過程中你不可能使用到大型系統,就如同開始時你的系統并沒有擴展。
操作需要更長的時間
如果某個操作是針對所有對象的,那么隨著對象增多其必然會花費更長的時間。表格同樣會變大,所以在之前查詢很快速的表格,對象變多后也會變慢。
更多的隨機錯誤
可能會出現正常操作中不會遇見的某些錯誤。ARP請求、文件系統,消息、響應等都會出現你預期不到的故障。
為錯誤打開了更大的窗戶
規模的變大意味著錯誤有更多的發生機會,因為所有操作都要花費更長的時間。小數據集的數據交換可能會很快結束,這就意味著超時或重啟只有很小的概率發生;然而在你對系統進行擴展的同時,你還擴寬了錯誤進入的大門,所以一些錯誤將***進入你的眼簾。
超時適應不了擴展
任何設定在小數據集上的超時隨著數據集變大都將失去作用。加上CPU饑餓問題,你的代碼甚至得不到任何機會運行,初始的超時設定就更加無效了。
重試適應不了擴展
在錯誤發生之前沒有任何途徑給應用程序選擇一個合適的重試次數,因為沒有足夠的信息讓你做這項決策。1秒4次的重試真的有用?為什么不是20次重試?
優先級繼承
長時間使用大范圍鎖將出現優先級繼承問題。
內存消耗模式的改變會耗盡節點資源
在一種規模下,你可以取得發生器中的所有數據;而在另一種規模下,你可能就會耗盡隊列空間或者是內存。舉個例子:在輪詢下一個隊列之前,使用輪詢器輪詢一個遠程隊列中所有數據源;在隊列中排隊項很少時,這會工作的很好;但是一個特性的改變就可以增加遠程隊列中的排隊項數量,而輪詢器就會耗盡一個節點上的所有資源。
監控器超時
100% CPU使用可能會導致監控器超時。在低負載系統上可能會很少出現,然而高負載系統上則會經常出現。
內存泄露加速
隨著系統擴展度增加,你不曾重視的緩慢內存泄露可能會增加到你不會相信的速度。
被遺忘的鎖再次出現
放錯位置的鎖,在低擴展系統下可能不會引起注意,然而在接到指令前這個可能永遠占用著CPU的線程在高擴展情況下就可能產生問題。在高擴展系統中可能會出現更多的搶占,這就意味著不同線程同時訪問數據的機會增多。
死鎖的可能性增加
不同的調度模式通過不同的路徑使用代碼,這就給死鎖的出現創造了更多的機會。舉個例子:某個文件系統會因為CPU的使用率過高而得不到運行,恰好的是這個文件系統在運行過程中莫名其妙的中斷并且占用了100%的CPU使用率,這樣的話它永遠都不會得到運行。
被破壞的時鐘同步
時鐘同步并不具備很高的優先級,所以當CPU和網絡資源變得緊張時,不同點上的時鐘將不再保持一致。
記錄器(Logger)丟失數據
如果隊列的長度不足以支撐負載或者是CPU沒有時間分給記錄器去發送記錄數據,那么將會出現記錄器丟失數據的情況。取決于隊列的類型和長度,這可能導致OOM。
無法完成在預期的時間啟動計時器(timer)
一個繁忙的系統可能無法在預期的時間啟動計時器,這可能在系統的其它部分造成一連串的延時。
ARP失效
在高CPU使用率或者高網絡負載情況下,ARP可能會在機器之間失去作用。這意味著表格在修改之前(可能永遠都不會),數據包可能發給了錯誤的機器。
文件描述(File Descriptor)符限制
通常情況下,每臺主機上的描述符數量都存在一個固定的限制,而設計的限制數量必須低于主機默認的限制數量。如果發生描述符池泄露,一個擁有大量連接(ftp、booting、clients等)的設計將會出現問題。隨著負載增加,描述符的數量有可能會達到一個峰值,描述符池泄露可能耗盡描述符池的可用性。
Socket緩沖限制
每個Socket都擁有預分配大小的緩沖空間,大量的Socket會減少可用內存的數量。隨著負載的增加,可能會出現空間不足的情況。這同樣與優先級有關,因為一個任務可能沒有足夠的優先級去讀取Socket內的數據;同樣在發送方,一個高優先級的任務可能會淹沒在一個低優先級任務的不斷消息請求中。
啟動鏡像服務限制
每個節點同時服務的鏡像節點數量是有限制的,FTP服務器基礎設施必須限制服務的節點數量,否則將出現CPU饑餓。
無序的信息
重壓下的消息系統可能會發送無序的消息,這可能會促成非冪等性操作,從而造成大量的問題。
協議相關
必須謹慎的定制你的應用程序協議,否則隨著程序的擴展,將會帶來大把問題。
連接限制
將某種服務器作為中心服務器:在連接10臺客戶端時,可能會有優異的表現;然而連接的客戶端數量變成100時,可能就會適應不了對響應的需求。這種情況下,響應時間可能會隨客戶端數量的增加呈線性增高,我們稱之為O(N)復雜度,同樣也可能會出現其它復雜性的問題。比如,我們需要一個網絡中的N個節點都可以相互通信:如果我們將它們都連接到一個通信樞紐的話,需要O(N)條電纜;但是如果我們把它們做相互連接的話,則需要O(N^2)條電纜。
分層架構
這里有一個很好的總結,所以此處只做簡單的敘述:基于分層的架構永遠都與低延時、高吞吐量的程序絕緣,問題在于分層架構本身就是用于處理歷史數據。在客戶端的時代到互聯網時代的過渡中,分層架構確實是解決可擴展性的不二選擇。
先前問題的關鍵是如何對應用程序進行擴展,讓其可以支撐數十萬的用戶。當然現在我們已經知道這個問題的答案就是N層架構,擴展性通過表示層上的負載均衡器實現。事實上,它確實解決了這個問題;然而隨著問題的衍變,當下許多行業需要考慮的不僅是擴展用戶體驗,還必須考慮數據的體積問題。
多重處理器的性能問題
當處理器被要求在巨量不相干任務中切換時,硬件緩存加速可能會失效。#p#

負載加重的籠罩下的“怪獸軍團”已現,開發者又該如何完成程序的設計,才能讓程序既易于擴展又具備高可靠性?下面來看一下High Scalability上的一篇姐妹文—— “7條法則以應對負載怪獸的襲擊”,當然這都是在程序低擴展等級編寫下需要注意的事項:
1. 限制資源使用的比例
這在實現擴展的應用程序中可能是最重要的規則,可以這么認為:
將資源限制到你認為可以支撐應用程序的等級,比如:保證可以在內存中處理一定數量的對象。所以如果我們一直按比例添加資源,那么就可以防止資源枯竭情況的出現。
針對個體資源試用不同的設計方式
一些例子:
我們需要保存一個訂單列表,訂單中商品的金額大于20美元(隨便多少)。這種情況下內存中保存的絕不能是這些商品,因為商品的數量可能遠大于訂單的數量。你可以對比列表中訂單的數量和資源的使用率,這樣你就清楚了你的系統可以支撐多少個訂單。
使用Merge Aggregation:同一個對象說的所有操作應該整合到一個請求中,而不是為每個操作分別做請求。比如,一個create和一個update就可以整合進一個請求。
2. Merge Aggregation
在Merge Aggregation中,獨立的數據和/或命令聚合到一處,用的是規則1的思想。
舉個例子,如果一個對象中包含了以下幾個命令序列:
Create
Update
Update
這三個分開的請求,可以融合到一處。如果有個循環運行了這個請求100次,那么我們的隊列中始終只有一個請求。
另一個例子是屬性修改事件,獨立的改變也可以融合到一處。想象一下這樣做的益處有多大,隊列的長度永遠都不會超過對象的數量,不管觸發了多少事件。完成這一點需要通信子系統的配合,所以必須確保相對智能。
3. Delete Aggregation
在Delete Aggregation中,數據和/或請求在允許的情況下將會被刪除。比如,做一下兩個操作:
Create
Delete
在這個聚合中,許多create和delete操作將會被刪除,大量的資源將被釋放。
4. Batch Aggregation
定時分析的批處理將會把大量的數據整合在一起,從而大幅度的提高性能。逐個的進行操作永遠比批量的處理來的慢。理念上應用程序不需要手動的做批處理業務,比如:框架會幫助你完成這個聚合。
5. Change Aggregation
在Change Aggregation中,所有的改變都將會被聚合到一處。當然這與Merge Aggregation不同,在Change Aggregation中我們注意到的是對象/數據改變的狀態,而不是它被改變了多少次;取代為每次改變做記錄,我們將把它的值發送給一個客戶端,然后告訴它事情已經改變了。因為在大型系統中,我們不可能為事物的每次改變做記錄。
6. Integration Aggregation
在Integration Aggregation中,事件只有在它存在過一個周期被關閉后才會被建立。
最常見的例子。一個警報只有在聚合周期結束后才被設置。當硬件發生問題等其它情況,我們可以建立一個alarm storm。
7. 卸載
卸載同樣是個擴展方案,在這里工作將會被拒絕直到有足夠的資源去運行它。
舉個例子,在一個呼叫處理系統(call processing system)中,呼叫的數量將會被限制。任何在限制之后的出現的呼叫都會被拒絕,這將會給現有的呼叫足夠的處理時間。
一些其它的卸載例子:
限制單節點FTP上的Session數量
在忙碌時將改服務器上的請求轉發到另一個服務器
公共電話系統在提示所有的呼叫正忙去阻止新呼叫的接入
寫在***
當然一篇文章不可能包括負載加重后出現的所有問題,也不可能包括所有的應對方案。但是可以肯定是對自己系統做足夠的認知,基于充足的信息做好設計決策,才能減少系統在擴展后所帶來的問題。