可伸縮性的最差實踐
相關文章:可伸縮性原則
英文原文:Scalability Worst Practices
引言
在擴展大量大型的分布式系統期間,我有機會觀察(并實踐)了一些最差實踐。這些最差實踐中的大部分在開始時都沒有危害,但如果疏忽大意,它們就會對系統的發展和可伸縮性構成危害。很多文章都聚焦于最佳實踐,以確保擁有一個易于維護和可伸縮的系統,但在本文中,我主要強調的則是一些應該規避的最差實踐。
技術
沒有任何一種技術或架構能實現所有的需求。了解何時該反思現有的方法、如何拓寬視野以超越局部范圍、或如何進行依賴的有效控制,這些都是可伸縮性的關鍵特性。讓我們進一步分別研究一下。
金錘子
金錘子起源于一條古老的諺語:如果你只有一把錘子,那么任何東西在你眼里都是一枚釘子。很多開發人員都局限在僅使用一種技術的觀念中——其代價是不得不使用選定的技術來構建和維護基礎設施,即便已經存在另一種技術更適用于特定問題域的功能和抽象。強行把一種技術用在它所不擅長的方面,有時會適得其反。
舉例來說,持久化鍵-值對問題的常見解決方案是使用數據庫。之所以常常這樣選擇是因為組織或開發者有堅實的數據庫實踐,針對許多問題自然而然就會沿用同樣的解決途徑。當數據庫的特性(關系完整性、鎖、連接和方案)成為瓶頸或阻礙了其伸縮擴展時,問題也就出現了。這是因為應用基于數據庫的解決方案要發展,其成本通常要比使用其它可用技術更為昂貴。隨著鍵-值存儲訪問率的增加,數據庫并發模式的性能就開始降低,而數據庫具備的高級特性卻被閑置。許多傳統關系數據庫的替代方案都是針對這些缺點的,比如CouchDB、SimpleDB或BigTable。
另一個常見的“錘子”就是總利用線程來進行并發編程。盡管線程確實是針對并發的,但它們也帶來了成本,這些成本包括代碼復雜性的增加、以及由于目前線程的的鎖定和訪問模型造成的組件編排(composability )方面的固有不足。由于如今最流行的編程語言都使用線程處理并發,因此數千行代碼都含有競態條件、潛在的死鎖和不一致的數據訪問管理。有些正在成長的社區提出了另一些并發方案,這些方案不存在線程的可伸縮性問題,也就是由Erlang或Stackless Python提倡的并發模型。即便不在實際生產中選擇那些語言,研究一下它們的概念(比如消息傳遞或異步I/O)仍然是一種不錯的實踐。
資源濫用
小范圍的問題開發者們一般都能處理得得心應手:使用分析工具、了解算法的空間和時間復雜度、或者了解哪種場合應該用哪種列表實現。但并非每個人都善于認識到大型系統的約束條件,比如識別共享資源的性能要求、了解服務的各種客戶、或發掘數據庫的訪問模式。
應用程序實現伸縮性的普遍方法是不斷橫向部署冗余的、無狀態的、彼此不共享內容的服務,以此作為最理想的體系架構。但以我的經驗看來,這種擴展往往會忽視新增服務對共享資源的影響。
比如說,如果一個特定的服務使用數據庫作為持久存儲,它通常通過一個線程池來管理數據庫連接。使用池是不錯的方法,有助于避免進行過多的數據庫連接處理。然而數據庫仍然是共享資源,除了單個池配置,還必須對所有池從總體上進行管理。下面兩個實踐就會導致失敗:
- 持續增加服務數,但并不減小池的最大數。
- 增大單個池的大小,而不減小服務數量。
以上兩種情況中,除了按性能要求配置應用之外,連接的總數也必須加以管理。此外,還要持續監控數據庫的容量,以保持連接均衡。
處理共享資源的可用性至關重要,準確的說,這是因為它們一旦失效,由于其“共享”的本質,失效會對系統造成全面的影響,而非孤立存在。
大泥球
依賴是很多系統討厭卻又必不可少的東西,不積極地處理好依賴及其版本會損害靈活性和可伸縮性。
代碼的依賴管理有多種不同的模式:
- 同時編譯整個代碼集
- 基于已知版本選取構件和服務
- 發布的模型和服務所有變更都向后兼容
讓我們看看這些情形。首先,在大泥球模式下,整個系統作為一個單元編譯和部署。這種模式擁有明顯的優勢,也就是將依賴管理交給編譯器處理,并能提前捕獲一些問題,但它會因每次都部署整個系統(包括測試、交付和大范圍變化引起的風險)而引發可伸縮性的問題。在這種模式下,會更難隔離系統的變化。
在第二種模式中,依賴都是按需挑選的,但是變化經過依賴傳遞之后依舊出現第一種模式一樣的難題。
第三種模式中,服務負責依賴的版本化,并向客戶端提供向后兼容的接口。這明顯減輕了客戶端的負擔,從而允許逐步升級到新的模型和服務接口。此外,當數據需要轉換的時候,它是依靠服務而不是客戶端完成的——這進一步穩固了隔離性。向后兼容的變更意味著打補丁、升級和回滾都不能干擾客戶端操作。
采用變更能向后兼容的服務體系架構在最大程度上避免了依賴問題。它同時方便了在受控環境下進行獨立測試,隔離了客戶端和版本化數據的變化。這三個優點對隔離變化來說都很重要。最近發布的Google Protocol Buffers項目也在倡導向后兼容的服務模型和接口。
全部打包還是部分打包
處理依賴時要考慮的另一件事情是如何對應用內容打包。
在一些場景中,比如Amazon Machine Images或Google AppEngine應用,它們的整個應用和所有的依賴都一起打包發布。這種囊括一切的打包方法保持了應用的自包含,但它增加了包的總大小,而且應用中任何地方的一個小小改變,都會迫使系統重新部署整個應用包(甚至對同一臺物理機器上許多應用使用的共享庫也是如此)。
替代方案是將應用的依賴移出主機系統,令應用包只包含依賴圖的若干部分。這控制了包的大小,但由于應用在能提供服務之前需要將特定的組件傳遞到每臺機器上,所以增加了部署配置。依賴項目沒有立即準備好、機器沒有經常測試、抑或是依賴錯誤,由于以上種種,不將整個包部署為自包含的方式會制約將應用部署到異構的、非標準化的機器上。
后一種方案——分成不同范圍(全局的、機器的、應用的)去處理依賴——必然會增加疏漏和復雜性。它減少了配置和依賴隔離,增加了操作的復雜性。一般而言,隔離能增加可伸縮性,所以盡可能選用囊括一切的方法,除非有例外情況。
無論在代碼還是在依賴處理中,最差實踐就是不清楚模塊間的關系,沒有規劃好模塊以便于對其進行管理。未能增強控制是可伸縮性的一大絆腳石。
忘記檢查時間
在分布式系統中,通常的目標是盡可能地將開發者和負責分布式調用的復雜方法隔離開來。這使主要的開發工作集中于核心的業務邏輯上,而不用擔心失效恢復、超時以及其它分布式系統必需的需求。但是,讓遠程調用看起來像本地調用一樣就意味著開發者要像本地調用一樣編碼。
我常發現很多代碼都期望所有的遠程請求能及時完成,但這樣的期望是不合理的。比如說,Java在JDK1.5中僅為HTTPURLConnection
類引入了讀超時,而讓開發者要么創建線程去殺死進程,要么天真地等待響應。
Java中,另一個潛在的時間處理不合理的例子是DNS查找。在一個長時間運行的典型系統中,執行完最初的DNS查找之后,如果不進行明確的配置,結果會緩存在JVM的生命期內。如果外部系統更改了主機的IP地址,將不能正確處理該條目,而且在很多情況下,因為編程時沒有設置連接超時時間,連接就會被掛起。
為了對系統進行合適的伸縮擴展,為請求處理分配好時間是極其重要的。有很多方法可以實現,有一些是語言內置的(像Erlang),其它的則作為庫的形式提供,比如libevent或Java的NIO
。拋開實現語言或架構不談,正確地管理操作等待時間是非常必要的。
運行時
建立一個符合成本效益的可擴展方案、處理好依賴、預先考慮到失效都是創建優秀架構的各方面要求。而在生產環境中,系統易于部署和運維的能力也同等重要。這里同樣有很多不利于系統可伸縮性的最差實踐。
英雄模式
運維問題普遍的解決方案是有一個“英雄”(關鍵性人物),他能處理、并經常處理大部分的操作需求。在小規模環境中,當某個人有天賦和能力熟悉整個系統(包括保持系統正常運行的許多細節之處),英雄模式可以正常運行。盡管這是最常見的實施方案之一,但對擁有許多組件的大型系統而言,這種方法就不能進行伸縮擴展了。
在沒有形式說明的情況下,“英雄”往往要理解服務依賴,牢記如何開、關特性,或了解其他人已經遺忘了的系統。“英雄”雖然至關重要,但他不應該是一個個體。
我認為英雄模式最好的解決方案是自動化。如果組織的情況允許,讓個人在團隊之間輪換也有幫助。在銀行里,休假有時是強制性的,好讓“你這里不行,要到我的機器上做”之類的問題及時暴露出來。
非自動化
系統過度依賴于人工干預往往是存在“英雄”的后果,這面臨著可重復生產能力的問題和“英雄”出現意外情況帶來的問題。能重現特定的構建、部署和環境很重要,而明確定義的元數據控制下的自動化是實現可重復能力的成功關鍵。
在一些開源項目中,工件的發布過程依賴于個體開發者在自己工作站上構建工件,沒有任何措施保證產生出來的工件版本能實際對應到源碼控制系統中的某個分支。在這些情況下,完全有可能發布軟件,其代碼從未被提交到源碼控制系統。
綜上所述,“英雄”的活動應該由自動化取代,從而確保個人(或許多人)可以相對容易地替換其他人。自動化的替代方案是增加流程——Clay Shirky為流程給出了一個有趣的定義:流程是對先前蠢行的內在反應。
先前的蠢行在所難免——自動化應該吸取教訓。
監控
當時間緊迫時,監控(比如測試)往往是第一個犧牲的環節。有時,在我問及有關組件的運行時表現方面的細節問題時,總沒有答案。缺乏對運行系統內部的深入了解和迅速切入問題的能力,不利于對從哪里入手和著手做什么做出正確攸關的決策。
Orbitz很幸運地擁有久經考驗的監控軟件,它們既能提供服務調用的細粒度詳細信息,也能精確顯現出問題域的數據。來自監控基礎設施的可用度量數據有利于快速有效地解決問題。
總結
在不久前Amazon的S3出現服務中斷之后,Jeff Bezos說道:遇到問題的時候,我們知道直接原因,我們從那里入手分析并找到了根本原因,然后從根本上進行了修復,又向前邁進了一步。
軟件和系統的開發是一個迭代的過程,在這個過程中,失敗和成功的機會并存。簡單但較難伸縮的解決方案有其一席之地,特別是計劃或應用尚處于不成熟的階段。“好”和“完美”不是對立的。但隨著系統的日臻完善,應該除去其中的那些最差實踐,這樣,成功也就是理所當然的了。
非常感謝Monika Szymanski對本文初稿提出的建議。
關于作者
Brian Zimmer是旅游業新創企業Yapta的架構師,是一位受人尊敬的開源社區成員,也是Python軟件基金會的成員。他之前作為高級架構師服務于Orbitz。他的博客在http://bzimmer.ziclix.com。