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