這些經驗起源於ebay,後來在國內經淘寶發揚光大。最近幾年,ebay好像沒有聲音了,不少年輕的架構師可能已經不知道ebay了,早個幾年,它仍是世界的「淘寶」,「淘寶」是中國的ebay,相似於「百度」是中國的google。
在ebay,全球數億的用戶量,天天超過10億的pv,數據量已PB計算,這些最佳實踐能夠說是ebay全部開發和運維的集體經驗結晶,也是ebay爲整個互聯網做出的極大貢獻。
我拿着這些最佳實踐,在不一樣場合作了幾回分享,如今再翻出來,從新複習一遍,也分享給天天爲可伸縮,高併發拼搏的同窗。
如下內容,儘量的照搬ebay分享的原有內容,加了部分本身實踐過程當中的體會(藍色部分)。
最佳實踐#一、按功能分隔
相關的功能部分應該合在一塊兒,不相關的功能部分應該分割開來——前幾年,你們習慣叫SOA或者功能拆分,最近幾年你們喜歡叫微服務。按功能拆分後,將會帶來如下幾個好處:
一、不想關的功能,將其應用邏輯、數據存儲獨立成一個應用而單獨部署,在應對流量、數據量大增時,就能夠對其進行單獨的擴容而不影響其餘模塊。
二、開發管理,在大規模團隊開發時,能夠將團隊分多個團隊,每一個小組負責其中一個應用,小組內部的協調就會方便不少。這點好處,也許你們容易忽略。
舉個我經歷過的反面教材:
某個大型實體零售公司,作科技轉型,作電商。早期的系統商品、交易、會員、促銷等全部模塊全在一個大包裏面,100多人到200人的開發團隊全在一個代碼工程下工做,任何一次代碼更新,都會有大量的衝突,好不容易解決衝突,想啓動開發環境進行調試,半個小時還沒起來,開發機資源幾乎所有耗盡。各位自行腦補是一個什麼場景。過完春節後,離職申請堆了好高一摞,工位最多看到1/3的熟面孔。
最佳實踐#二、水平拆分
按功能分割對咱們的幫助很大,但單憑它還不足以獲得徹底可伸縮的架構。即便將功能一一解耦,單項功能的資源需求隨着時間增加,仍然有可能超出單一系 統的能力。咱們經常提醒本身,「沒有分割就沒有伸縮」。在單項功能內部,咱們須要能把工做負載分解成許多咱們有能力駕馭的小單元,讓每一個單元都能維持良好狀態。這就是水平分割出場的時候了。
在應用層次,因爲 eBay 將各類交互都設計成無狀態的,因此水平分割是垂手可得之事。用標準的負載均衡服務器來路由進入的流量。全部應用服務器都是均等的,並且任何服務器都不會維持事務性的狀態,所以負載均衡能夠任意選擇應用服務器。若是須要更多處理能力,只須要簡單地增長新的應用服務器。
應用層無狀態,比較簡單的實現是將用戶id存放cookie中,每次請求會帶上cookie,應用層根據cookie從集中存儲(數據庫、緩存)中獲取相關的數據。也許會有一些年輕的架構師和開發同窗有疑問:cookie不安全啊,每次傳輸cookie耗帶寬啊,能夠作session綁定啊等等,實踐當中,常常會被問相似的問題,你們能夠留言交流。
舉個反面例子:
仍是上面例子的那個公司,因爲流量大增,一臺服務器確定是沒法支撐全部請求的,因此就用了集羣,集羣內部有100多個節點,每次發佈的時候,更新到70幾個節點時,就會異常的慢,經常引發發佈失敗。致使這個的主要緣由是請求是有狀態的,集羣內部須要維護session同步,不然用戶第一次訪問到第一個服務器,第二次訪問到第二臺服務器,就會出問題。
數據庫層次的問題比較有挑戰性,緣由是數據天生就是有狀態的。咱們會按照主要的訪問路徑對數據做水平分割(或稱爲「sharding」)。例如用戶 數據目前被分割到 20 臺主機上,每臺主機存放 1/20 的用戶。隨着用戶數量的增加,以及每一個用戶的數據量增加,咱們會增長更多的主機,將用戶分散到更多的 機器上去。商品數據、購買數據、賬戶數據等等也都用一樣的方式處理。用例不一樣,咱們分割數據的方案也不一樣:有些是對主鍵簡單取模(ID 尾數爲 1 的放到第一 臺主機,尾數爲二的放到下一臺,以此類推),有些是按照 ID 的區間分割(1-1M、1-2M 等等),有些用一個查找表,還有些是綜合以上的策略。不過具體 的分割方案如何,總的思想是支持數據分割及重分割的基礎設施在可伸縮性上遠比不支持的優越。
數據庫分隔,常說的分庫分表,一旦作分庫分表,會帶來不少問題,好比跨庫查詢、統計等等,這須要你們仔細分析各類應用場景,而後作出最適合的拆分策略。
最佳實踐 #3:避免分佈式事務
看到這裏,你可能在疑惑按功能劃分數據和水平劃分數據的實踐如何知足事務要求。畢竟,幾乎任何有意義的操做都要更新一個以上的實體——當即就能夠舉 出用戶和商品的例子。正統的廣爲人知的答案是:創建跨資源的分佈式事務,用兩段式提交來保證要麼全部資源全都更新,要麼全都不更新。很不幸,這種悲觀方案 的成本很可觀。伸縮、性能和響應延遲都受到協調成本的反面影響,隨着依賴的資源數量和客戶數量的上升,這些指標都會以幾何級數惡化。可用性亦受到限制,因 爲全部依賴的資源都必須就位。實用主義的答案是,對於不相關的系統,放寬對它們的跨系統事務的保證。
左右逢源是辦不到的。保證跨多個系統或分區之間的即時的一致性,一般既無必要,也不現實。Inktomi 的 Eric Brewer 十年前提出的 CAP 公理是這樣說的:分佈式系統的三項重要指標——一致性(Consistency)、可用性(Availability)和 分區耐受性(Partition-tolerance)——在任意時刻,只有兩項能同時成立。對於高流量的網站來講,咱們必須選擇分區耐受性,由於它是實 現可伸縮的根本。對於 24x7 運行的網站,選擇可用性也是理所固然的。因而只好放棄即時一致性(immediate consistency)。
在 eBay,咱們絕對不容許任何形式的客戶端或者分佈式事務——所以毫不須要兩段式提交。在某些通過仔細定義的情形下,咱們會將做用於同一個數據庫 的若干語句捆綁成單個事務性的操做。而對於絕大部分操做,單條語句是自動提交的。雖然咱們故意放寬正統的 ACID 屬性,以至不能在全部地方保證即時一致 性,但現實的結果是大部分系統在絕大部分時間都是可用的。固然咱們也採用了一些技術來幫助系統達到最終的一致性(eventual consistency):周密調整數據庫操做的次序、異步恢復事件,以及數據覈對(reconciliation)或者集中決算(settlement batches)。具體選擇哪一種技術要根據特定用例對一致性的需求來決定。
對於架構師和系統的設計者來講,關鍵是要明白一致性並不是「有」和「沒有」的單選題。現實中大多數的用例都不要求即時一致性。正如咱們常常根據成本和其餘壓力因素來權衡可用性的高低,一致性也一樣能夠量體裁衣,根據特定操做的須要而保證適當程度的一致性。
放棄事務控制,對於不少開發同窗來講難以接受,總會舉出一堆的反例來證實必須有事務控制。咱們仔細理解這一句話:「周密調整數據庫操做的次序、異步恢復事件,以及數據覈對(reconciliation)或者集中決算(settlement batches)」,可能可以幫助理解這一條最佳實踐。
舉個例子:
系統提供幾個微服務模塊:庫存、促銷、訂單,訂單模塊下單的常規邏輯以下:
事務開始
一、查庫存是否知足 (庫存微服務提供的接口)
二、查積分是否知足(庫存微服務提供的接口)
三、查優惠券是否知足(庫存微服務提供的接口)
四、訂單表新增訂單信息
五、訂單明細表新增訂單明細
六、扣減庫存
七、扣減積分
八、優惠券設置爲已使用
事務提交
咱們根據最佳實踐「周密調整數據庫操做的次序、異步恢復事件」,將下單邏輯調整爲:
一、扣減庫存(不成功返回)//首先扣減庫存,是由於你們都在下單,庫存最有可能扣減失敗
二、扣減積分(不成功,回滾庫存)//扣減積分,積分屬於客戶一人,很難產生衝突,固然也會有一個帳號多個地方登錄,同時下單,但可能性小不少。
三、設置優惠券爲已使用(不成功,回滾庫存、回滾積分)//設置優惠券同積分相似。
四、單庫事務開始
1)訂單表新增訂單信息
2)訂單明細表新增訂單明細
五、提交事務(事務失敗,回滾庫存、回滾積分、回滾優惠券)
六、若是以上某一個回滾步驟失敗,記錄回滾日誌,異步處理恢復,處理失敗,監控報警,人工干預。
經過以上調整,避免了分佈式事務,同時保證了最終一致性。性能獲得了大幅度的提高。壞處是增長了不少回滾操做,代碼確實要多不少,大多數狀況下,回滾邏輯不會被執行到。
保證最終一致性的策略還有不少,你們仔細分析,總會想到不少策略,這裏重點強調一點,代碼通過仔細的設計、完善測試的狀況下,出現異常的狀況是極少數的,咱們應該平衡成本產出比,對於極少的異常狀況,經過很是規方法處理,是能夠接受的。
最佳實踐 #4:用異步策略解耦程序
提升可伸縮性的另外一項關鍵措施是積極地採起異步策略。若是組件 A 同步調用組件 B,那麼 A 和 B 就是緊密耦合的,而緊耦合的系統其可伸縮性特徵是各部分必須共同進退——要伸縮 A 必須同時伸縮B。同步調用的組件在可用性方面也面臨着一樣的問題。咱們回到最基本的邏輯:若是 A推出B,那麼非 B 推出非 A。也就 是說,若 B 不可用,則 A 也不可用。若是反過來 A 和 B 的聯繫是異步的,無論是經過隊列、多播消息、批處理仍是什麼其餘手段,它們就能夠分別地伸縮。並且,此時A和B 的可用性特徵是相互獨立的——即便 B 受困或者死掉,A 仍然可以繼續前進。
整個基礎設施從上到下都應該貫徹這項原則。即便在單個組件內部也可經過 SEDA(分階段的事件驅動架構,Staged Event-Driven Architecture)等技術實現異步性,同時保持一個易於理解的編程模型。組件之間也遵照一樣的原則——儘量避免同步帶來的耦合。在多數狀況下, 兩個組件在任何事件中都不會有直接的業務聯繫。在全部的層次,把過程分解爲階段(stages or phases),而後將它們異步地鏈接起來,這是伸縮的關鍵。
這一條簡單理解就是,有些操做客戶是不須要實時知道結果的,那這種操做就能夠異步處理,這樣帶來的好處是:把耗性能的,客戶無需實時知道結果的操做異步以後,整個系統的實時響應獲得大幅提高。舉個例子:
下單流程:
一、下單
二、付款
三、配貨
四、物流
五、收貨
客戶只關心下單是否成功、付款是否成功,至於怎麼配貨,怎麼物流,客戶是不怎麼關心的,或者不須要付款成功後就立刻告訴客戶的。這樣,配貨邏輯就能夠異步操做了。
反例:
仍是以前例子裏提到的同一個項目,貨物存放在不一樣的倉庫、庫位,還要分批次,配貨的邏輯就是:
根據客戶信息判斷應該從哪一個倉庫、庫位、批次出貨,而後進行庫存扣減,由於數據量巨大,這是一個很耗性能的操做,
下單邏輯裏,實時執行了配貨的邏輯,致使整個下單過程很慢,搞一個促銷,下單量一上來,系統就宕機了。
解決方案就是:
下單成功後,發送MQ,配貨邏輯接收到MQ消息,執行配貨邏輯。也許你會問,那怎麼扣減庫存,異步以後,告訴用戶下單成功了,可是後面沒貨了怎麼辦?請仔細理解客戶並不關心貨是哪一個倉庫、哪一個庫位,哪一個批次的,理解了這個場景,天然就很容易解決了。
最佳實踐 #5:將過程轉變爲異步的流
用異步的原則解耦程序,儘量將過程變爲異步的。對於要求快速響應的系統,這樣作能夠從根本上減小請求者所經歷的響應延遲。對於網站或者交易系統, 犧牲數據或執行的延遲時間(完成所有工做的實踐)來換取用戶的延遲時間(用戶獲得響應的時間)是值得的。活動跟蹤、單據開付、決算和報表等處理過程顯然都 應該屬於後臺活動。主要用例過程當中經常有不少步驟能夠進一部分解成異步運行。任何能夠晚點再作的事情都應該晚點再作。
還有一個同等重要的方面認識到的人很少:異步性能夠從根本上下降基礎設施的成本。同步地執行操做迫使你必須按照負載的峯值來配備基礎設施——即便在 任務最重的那一天裏任務最重的那一秒,設施也必須有能力當即完成處理。而將昂貴的處理過程轉變爲異步的流,基礎設施就不須要按照峯值來配備,只須要知足平 均負載。並且也不須要當即處理全部的請求,異步隊列能夠將處理任務分攤到較長的時間裏,於是起到削峯的做用。系統的負載變化越大,曲線越多尖峯,就越能從 異步處理中得益。
這一條和第四條差很少,但第四條更強調兩個模塊以前的解耦,第五條強調系統性能削峯。
最佳實踐 #6:虛擬化全部層次
虛擬化和抽象化無所不在,計算機科學裏有一句老話:全部問題均可以經過增長一個間接層次來解決。操做系統是對硬件的抽象,而許多現代語言所用的虛擬 機又是對操做系統的抽象。對象 - 關係映射層抽象了數據庫。負載均衡器和虛擬 IP 抽象了網絡終端。當咱們經過分割數據和程序來提升基礎設施的可伸縮性,爲各 種分割增長額外的虛擬層次就成爲重中之重。
在 eBay,咱們虛擬化了數據庫。應用與邏輯數據庫交互,邏輯數據庫再按照配置映射到某個特定的物理機器和數據庫實例。應用也抽象於執行數據分割的 路由邏輯,路由邏輯會把特定的記錄(如用戶 XYZ)分配到指定的分區。這兩類抽象都是在咱們本身開發的 O/R 層上實現的。這樣虛擬化以後,咱們的運營團隊 能夠按須要在物理主機羣上從新分配邏輯主機——分離、合併、移動——而徹底不須要接觸應用程序代碼。
搜索引擎一樣是虛擬化的。爲了獲得搜索結果,一個聚合器組件會在多個分區上執行並行的查詢,但這個高度分割的搜索網格在客戶看來只是單一的邏輯索引。
以上種種措施並不僅是爲了程序員的方便,運營上的靈活性也是一大動機。硬件和軟件系統都會故障,請求須要從新路由。組件、機器、分區都會不時增減、 移動。明智地運用虛擬化,可以使高層的設施對以上變化可貴糊塗,你也就有了騰挪的餘地。虛擬化使基礎設施的伸縮成爲可能,由於它使伸縮變成可管理的。
這一條實踐當中用得最可能是一、虛擬IP,常常應用在雙擊互備,二、容器虛擬化好比docker,將全部的物理資源組成資源池,根據不一樣模塊的資源利用率,動態調整資源。
最佳實踐 #7:適當地使用緩存
最後要適當地使用緩存。這裏給出的建議不必定廣泛適用,由於緩存是否高效極大地依賴於用例的細節。說到底,要在存儲約束、對可用性的需求、對陳舊數 據的容忍程度等條件下最大化緩存的命中率,這纔是一個高效的緩存系統的最終目標。經驗證實,要平衡衆多因素是極其困難的,即便暫時達到目標,狀況也很可能 隨着時間而改變。
最適合緩存的是不多改變、以讀爲主的數據——好比元數據、配置信息和靜態數據。在 eBay,咱們積極地緩存這種類型的數據,而且結合使用「推」和「 拉」兩種方法保持系統在必定程度上的更新同步。減小對相同數據的重複請求能達到很是顯著的效果。頻繁變動、讀寫兼有的數據很難有效地緩存。在 eBay,我 們大多有意識地迴避這樣的難題。咱們一直不對請求間短暫存在的會話數據做任何緩存。也不在應用層緩存共享的業務對象,好比商品和用戶數據。咱們有意地犧牲 緩存這些數據的潛在利益,換取可用性和正確性。在此必須指出,其餘網站採起了不一樣的途徑,做了不一樣的取捨,也一樣取得了成功。
好東西也會過猶不及。爲緩存分配的內存越多,能用來服務單個請求的內存就越少。應用層經常有內存不足的壓力,所以這是很是現實的權衡。更重要的一 點,當你開始依賴於緩存,那麼主要系統就只須要知足緩存未命中時的處理要求,天然而然你就會想到能夠削減主要系統。但當你這樣作以後,系統就徹底離不開緩 存了。如今主要系統沒辦法直接應付所有流量,也就是說網站的可用性取決於緩存可否 100% 正常運行——潛在的危局。哪怕是例行的操做,好比從新配置緩存資 源、把緩存移動到別的機器、冷啓動緩存服務器,都有可能引起嚴重的問題。
作得好,緩存系統能讓可伸縮性的曲線向下彎曲,也就是比線性增加還要好——後續請求從緩存中取數據比從主存儲取數據成本低廉。反過來,緩存作得很差 會引入至關多額外的常常耗費,也會妨礙到可用性。我還沒見過哪一個系統沒機會讓緩存大展拳腳的,關鍵是要根據具體狀況找到適當緩存策略。
這一條,不少同窗都深有體會,但實踐過程當中,每每作得不太好。後面可能會寫一篇如何使用緩存的文章。使用緩存的過程當中,咱們應該牢記如下幾點:
一、緩存不該該在多個節點以前同步,早些年緩存同步方案很常見,好比session在集羣內部同步。
二、必須設計一個好的機制,保證緩存數據已數據庫的同步。
三、緩存就應該幹緩存的事情,不該該把緩存當成持久化存儲使用,不少同窗討論redis如何持久化,如何使用持久化,這會帶來不少不少不可預計的問題和系統設計複雜度。我認爲redis的持久化特性,主要是應對災備和恢復場景的,至少我是這麼用的。
四、緩存失效不該該致使系統流程失敗,好比緩存失效了,讀不到緩存,應用應該可以從DB讀取數據進行計算。
五、不該該以緩存中的數據做爲最終決策的依據,好比說緩存了庫存數據,不能緩存中的庫存扣減成功就能下單成功。
總結
可伸縮性有時候被叫作「非功能性需求」,言下之意是它與功能無關,也就比較不重要。這麼說簡直錯到了極點。個人觀點是,可伸縮性是功能的先決條件——優先級爲 0 的需求,比一切需求的優先級都高。
任何一個場景,須要不一樣的架構策略,沒有一個架構策略能解決全部問題,也沒有最好的架構,只有最合適的架構。不少同窗一上來就會把淘寶、阿里的各類框架、最佳實踐全用上,每每忽略了當前的業務場景、業務規模、預算、團隊水平等等,而致使最終效果不佳。