可伸縮性原則
從最簡單的水平來看,可伸縮性就是做更多的事情。更多的事情可以是響應更多的用戶請求,執行更多的工作,或處理更多的數據。設計軟件這件事本身是復雜的,而讓軟件做更多的工作也有其特有的問題。這篇文章針對構建可伸縮軟件系統提出了一些原則和方針。
1. 減少處理時間
增加應用所做工作數量的一個方法就是減少完成單項工作所花費的時間。舉例來說,減少處理一個用戶請求所需的時間意味著你能在同樣長的時間內處理更多的用戶請求。這里有一些本原則適用的例子和一些可能的實現策略。
- 并置(Collocation):通過并置數據和代碼,減少因獲取所需數據而產生的必要開銷。
- 緩存:如果數據和代碼不能并置,就緩存數據,以減少反復取數據的開銷。
- 池化:通過池化昂貴的資源,減少與其使用相關的開銷。
- 并行化:通過分解問題、并行化獨立的步驟,減少完成一個工作單元所需的時間。
- 分區處理:通過分割處理代碼、并置相關的分區,盡可能將相關的處理過程集中在一起。
- 遠程處理:減少訪問遠程服務所花費的時間,比如可以通過更粗粒度地劃分接口。遠程還是本地是明確的設計決策,不能隨意來回更動,這一點應當牢記。還要考慮分布式計算的第一準則——不要分布你的對象。
軟件開發人員總愛在不需要的地方引入抽象和層。是的,這些概念對軟件組件之間的解耦來說是很好的工具,但它們可能會增加復雜性、影響性能,尤其是在每層的數據表示之間都需要轉換的情況下。因此,減少處理時間還要注意保證抽象不要過于抽象化,并且沒有過多的分層。另外,對于我們視為理所當然的運行時服務,有必要理解其成本,因為除非它們提供了特定的服務水平協議,否則很有可能最終會成為應用中的瓶頸。
2. 分區
減少單個工作單元的處理時間能達到不錯的效果,但當你達到單進程方案的極限,最終還是需要對系統作水平伸縮。在典型的Web應用中,水平伸縮可能很簡單,只要加入更多的Web服務器來處理用戶請求,再給它們加上負載均衡就行了。但是,你可能會發現總體架構的某些部分會成為資源爭用的焦點,因為一切東西都會在同一時間變得忙碌起來。一個很好的例子就是所有Web服務器后端的單一數據庫服務器。當這個單一的數據庫服務器變成瓶頸時,你必須改變方法,其中一種方式就是采用分區策略。簡而言之,這涉及到將架構的單個部分分解成更小、更容易管理的部分。將單個的元素分割成更小的部分能實現水平伸縮,這恰恰也是 eBay這樣的大型網站采用、以此來確保它們的架構可伸縮的技術。分區是一個很好的解決辦法,盡管你可能會發現犧牲了一致性。
至于如何分割你的系統,那要看情形而定。真正無狀態的組件能簡單地作水平伸縮,將工作負載分散到所有實例上,讓組件的所有實例都能有效地運行。另一方面,如果需要維護某狀態,你需要找到一種工作量分割策略能允許有狀態組件的多重實例,讓每個實例負責工作和/或數據的一個獨特的子集。
3. 可伸縮性在于并發
可伸縮性天生就和并發聯系在一起;畢竟,它就是要在同樣的時間內做更多的工作。像EJB早期版本這樣的技術試圖提供一種簡化的編程模型,鼓勵我們編寫單線程的組件。遺憾的是,組件往往要依賴于其它組件,還是導致了并發問題。如果沒有考慮并發,系統中的數據會很容易被損壞。另一方面,圍繞并發做了太多的保護會導致系統實質上變成串行的,限制了伸縮的能力。并發編程不是很難做到,在構建可伸縮系統的時候,有一些簡單的原則會有所幫助。
- 如果你確實需要持有鎖(比如本地對象、數據庫對象等),試著盡可能短地持有它們。
- 設法減少對共享資源的爭用,并盡可能是爭用避開關鍵處理路徑(比如通過異步調度工作)。
- 任何針對并發的設計都需要預先完成,以便能被充分地理解哪些資源可以被安全共享、哪里可能會是潛在的可伸縮性瓶頸。
4. 必須知道需求
為了構建一個成功的軟件系統,你需要知道你的目標是什么、你針對什么去做。盡管功能性需求往往是明確的,但常常會缺少非功能性需求(或系統質量需求)。如果你真是需要構建一套高可伸縮的軟件,那你首先需要調查清楚關鍵組件/工作流的以下特質:
- 目標平均性能和峰值性能(即響應時間、延遲等)。
- 目標平均負荷和峰值負荷(即并發用戶、信息量等)。
- 性能和可伸縮性可接受的極限。
性能也許不是最緊要的方面,但你必須盡早知道這個信息,因為處理可伸縮性的方法會由性能需求決定。
5. 持續測試
理解了需求就可以開始設計和構建解決方案。我們提出的設計、編寫的代碼實際上都是靜態的,所以你在執行之前不能完全斷定它會怎樣運轉。此外,所有關于性能和可伸縮性的決策應該由證據支持的原因也在于此,而且應當從項目一開始就收集和審核這些證據,此后也要一直繼續。換句話說,就是設立貫穿系統的可度量目標,證實并度量實際的性能,并在項目的各個階段考慮性能。
最常犯的錯誤之一是,我們對系統性能和可伸縮性的見解會被我們自己的經驗或道聽途說所混淆。你可能要審核對工程做出的其它決策,這樣做的原因之一是要滿足系統的非功能性特性。比如說,非功能性需求可能會影響你選擇不使用標準,改用非主流/流行的一些東西。非功能性需求可能會打破僵化的教條,證據勝過教條。
6. 架構先行
或許對構建可伸縮的系統來說最重要的原則是,如果你需要使系統具備這樣的性質,就必須預先設計出這樣的性質。很多人(包括我自己)陷入的陷阱,就是以為可以構建一個應用,它會自動地垂直伸縮(scale up)或水平伸縮(scale out),尤其是在J2EE剛出現的時候。設計為可水平伸縮的應用幾乎總能垂直伸縮,但是設計為垂直伸縮的應用幾乎不可能水平伸縮。大多數應用能通過在更加強大的硬件上運行來垂直伸縮,但水平伸縮卻是一個更為復雜的問題。比如說,你怎么確保數據在應用實例之間保持一致性?你如何使你的單例和同步代碼塊跨線程工作?
當然,預先思考這件事情不一定等同于做一個瀑布式的、預先的大設計。迭代和敏捷過程都是助力,它們能提供一個框架幫助我們可以做出剛好夠用的設計來解決問題。要務實。哦,不管我們自認為是多么擅長于設計可伸縮的應用,不要相信自己、盡早地編寫/測試代碼才是最好的舉動。
7. 著眼于全局
最后,記著要著眼于全局——看到樹木之前先看看森林。對我們來說,在細粒度代碼級別調整組件確實很容易,但最終需要優化的卻是作為一個整體的系統。關注每一個環節的性能和可伸縮性,必要時犧牲局部的優化。如果你需要使用性能分析工具確定瓶頸,不妨去做,但在對全局的性能有所認識之前先不要急于動手。由于性能與整個系統所有等待時間的集合成反比,任何等待時間增加得比負載還快的操作都會成為問題。盡管說了這么多,但我還想指出,如果你發現滿足性能和可伸縮性目標很困難,那就有必要懷疑一下是否選擇了正確的架構。還是那句話,著眼于全局,確保有人在承擔架構師的責任。
總結
這篇文章針對構建可伸縮應用提出了一些原則和方針,覆蓋了軟件開發過程中許多不同的方面。無論誰要構建可伸縮的系統,我能給他的最好建議就是你需要明確地考慮并設計你的系統。可伸縮性不是魔術,它也不會無償獲得。最后一點,更快的硬件也許能救你于一時,但還是不要依賴它為妙!
關于本文
2005年年底,英國倫敦舉辦了針對架構師的非官方峰會,本文中的大多數原則就來源于其中一場可伸縮性討論的一些筆記。該峰會由Alexis Richardson、Floyd Marinescu、Rod Johnson、John Davies、Steve Ross-Talbot組織。題為“JP Rangaswami談企業中的開源與信息前景”的視頻也來源于此峰會。
關于作者
Simon是一名注重實踐的軟件架構師,他在Detica'的全球金融市場集團工作。Simon專心參與的項目有桌面客戶端和Web應用,也有高度可伸縮的分布式系統和面向服務的體系架構(SOA)。他的專攻技術是Java,作為一名注重實踐的權威,他被要求建議并設計解決方案;定義、交付并保證所選擇的架構適合于目的,能滿足非功能性需求。Simon已經編寫或與他人合著過很多Java EE Web技術相關的書籍,在數次會議上發表過演講,并創建了Coding the Architecture——一個介紹關于軟件架構實用和務實觀點的網站。