從0到1搞懂分散式架構:Uber大型支付系統構建經驗總結

AI前線發表於2018-05-08

從0到1搞懂分散式架構:Uber大型支付系統構建經驗總結


作者|Gergely Orosz
編譯 & 編輯|Debra
AI 前線導讀:本文介紹了 Uber 支付系統重建過程中,有關分散式系統 SLA、一致性、資料永續性、訊息持續性、冪等性等方面的考量和注意事項。

更多幹貨內容請關注微信公眾號“AI 前線”,(ID:ai-front)

兩年前,我以一名略懂後端的移動軟體工程師身份加入了 Uber,負責開發該應用的支付功能,並最終重寫了整個應用

https://eng.uber.com/new-rider-app/。 隨後我轉向工程管理 http://blog.pragmaticengineer.com/things-ive-learned-transitioning-from-engineer-to-engineering-manager/, 負責團隊本身的管理工作。這意味著需要更多地接觸後端,因為支付環節涉及到的很多後端系統都是我的團隊負責的。

入職 Uber 之前,我對分散式系統幾乎全無任何經驗。作為傳統電腦科學畢業生,十多年來我一直在從事全棧軟體開發,然而雖然很擅長畫架構圖並討論各種權衡,但我對諸如一致性、可用性或冪等性等分散式概念並沒有太多瞭解。

本文我將總結自己在構建大規模高可用分散式系統(Uber 所用的支付系統)過程中學習和應用的一些心得體會。這個系統需要處理每秒高達數千次的請求,同時就算系統的一些元件故障,也要保證某些關鍵支付功能依然正常運轉。我要說的內容足夠全面嗎?未必!但至少這些內容讓我的工作變得前所未有得簡單。接下來,就一起看看這些工作中不可避免會遇到的 SLA、一致性、資料永續性、訊息持續性、冪等性之類的概念吧。

SLA

對於每天需要處理數百萬事件的大型系統,幾乎不可避免會遇到問題。在正式開始規劃整個系統前,我發現更重要的是確定怎樣的系統才算是“健康”的。“健康”應該是一種真正可以衡量的指標。衡量“健康”與否的一種常見做法是使用 SLA:服務級別協議。而我用過的一些最常用的 SLA 包括:

可用性:服務處於正常運轉狀態的時間所佔比率。雖然每個人都想擁有一個具備 100% 可用性的系統,但這一點往往很難實現,同時也極為昂貴。就算 VISA 卡網路、Gmail 及網際網路服務提供商這樣的大型關鍵系統也不可能在長達一年的時間裡維持 100% 可用性,系統可能會停機數秒、數分鐘或數小時。對很多系統來說,四個九的可用性(99.99%,即每年約停機 50 分鐘 https://uptime.is/)

已經足夠高了,而通常這樣的可用性也需要在背後付出大量工作。準確性:系統中部分資料不準確或丟失,這種情況可以接受嗎?如果可以,那麼可接受的最大比率是多少?我所從事的支付系統必須確保 100% 準確,意味著資料決不能丟失。容量:系統預計要為多大規模的負載提供支援?這通常是用每秒請求數衡量的。

延遲:系統要在多長時間內做出響應?95% 的請求及 99% 的請求會在多長時間內獲得響應?系統通常會收到很多無意義的請求,因此 p95 和 p99 延遲 https://www.quora.com/What-is-p99-latency 更能代表實際情況。為何說 SLA 對大型支付系統至關重要?我們釋出一個新系統,要取代一個老的系統。為了確保工作有價值,新的系統必須比上一代“更出色”,而我們要使用 SLA 來定義自己的各種預期。可用性是最重要的要求之一,一旦確定了目標,就需要考慮架構中的各項權衡,以此來滿足自己的目標。

水平和垂直縮放

假設使用新系統的業務數量開始增長,那麼負載將只增不減。在某一刻,現有配置可能將無法支撐更多負載,需要擴容。垂直縮放和水平縮放是目前最常用的兩種縮放方式。

水平縮放旨在給系統中增加更多計算機(節點),藉此獲得更多容量。水平縮放是分散式系統最常用的縮放方式,尤其是為叢集增加(虛擬)計算機通常只需要點選按鈕即可完成。

垂直縮放可以理解為“買一臺更大 / 更強的計算機”,或者換用核心更多、處理能力更強、記憶體更大的(虛擬)計算機。對於分散式系統,通常不會選擇垂直縮放,因為相比水平縮放這種做法更貴。然而一些大型網站,例如 Stack Overflow 就曾成功地進行了垂直縮放並完滿達成目標

(https://www.slideshare.net/InfoQ/scaling-stack-overflow-keeping-it-vertical-by-obsessing-over-performance)。

為何說縮放策略對大型支付系統很重要?儘早決定,就可以著手構建能夠水平縮放的系統。雖然一些情況下也可以進行垂直縮放,但我們的支付系統已經在執行生產負載了,而最初我們就很悲觀地認為,哪怕一臺極為昂貴的大型機也無法應對當前的需求,更不用提未來的需求了。我們團隊還有工程師曾在大型支付服務公司任職,他們當時曾試圖用能買到的最高容量的計算機進行垂直縮放,只可惜最終還是失敗了。

一致性

對任何系統來說,可用性都很重要。分散式系統通常會使用可用性不那麼高的多臺計算機構建。假設我們的目標是構建具備 99.999% 可用性(每年停機約 5 分鐘)的系統,但我們所使用的計算機 / 節點,可用性平均僅為 99.9%(每年停機約 8 小時)。為了獲得所需可用性,最簡單的辦法是向叢集中新增大量此類計算機 / 節點。就算某些節點停機了,其他節點依然可以正常執行,確保系統的整體可用性足夠高,甚至遠高於每一個元件的可用性。

一致性對高可用系統很重要。如果所有節點可以同時看到並返回相同的資料,那麼就認為這個系統具備一致性。上文曾經說過,為了實現足夠高的可用性,我們新增了大量節點,那麼不可避免也要考慮到系統的一致性問題。為了確保每個節點具備相同資訊,它們需要相互傳送訊息,確保所有節點保持同步。然而相互之間傳送的訊息有可能沒能成功送達,可能會丟失,一些節點可能不可用。

我大部分時間都用來理解並實現一致性。目前有多種一致性模型(https://en.wikipedia.org/wiki/Consistency_model),分散式系統最常用的包括強一致性(Strong Consistency https://www.cl.cam.ac.uk/teaching/0910/ConcDistS/11a-cons-tx.pdf)、 弱一致性(Weak Consistency https://www.cl.cam.ac.uk/teaching/0910/ConcDistS/11a-cons-tx.pdf) 和最終一致性(Eventual Consistency http://sergeiturukin.com/2017/06/29/eventual-consistency.html)。 Hackernoon 有關最終一致性和強一致性 (https://hackernoon.com/eventual-vs-strong-consistency-in-distributed-databases-282fdad37cf7) 對比的文章非常清晰實用地介紹了需要在這些模型之間進行的權衡。一般來說,一致性要求越低,系統速度就越快,但也越有可能返回並非最新狀態的資料。

為何一致性對大型支付系統很重要?系統中的資料必須保持一致。但要如何實現一致?對於系統的某些部件,只能使用強一致的資料,例如為了知道某個支付操作是否已經成功發起,這種資訊就必須以強一致的方式儲存。但對於其他部件,尤其是非關鍵業務部件,最終一致通常是一種更合理的做法。例如在顯示歷史行程時,使用最終一致的方式實現就足夠了(也就是說,最新一次行程在短時間內可能只會出現在系統的某些元件中,這樣,相關操作就可以用更低延遲或更小資源佔用的方式返回結果)。

資料永續性

永續性(https://en.wikipedia.org/wiki/Durability_%28database_systems%29) 意味著一旦資料成功放入儲存,那麼以後將一直可用,就算系統中的節點下線、崩潰或資料出錯,已儲存的資料依然不應受到影響。

不同的分散式系統可以實現不同程度的永續性。一些系統會在計算機 / 節點層面實現永續性,一些則會在叢集層面實現,而也有一些系統本身並不提供這樣的能力。為了提高永續性,通常會使用某種形式的複製操作:如果資料儲存在多個節點中而一個或多個節點故障了,資料依然可以保證可用。這裡有一篇很棒的文章(https://drivescale.com/2017/03/whatever-happened-durability/) 介紹了為何分散式系統中的永續性那麼難實現。

從0到1搞懂分散式架構:Uber大型支付系統構建經驗總結

為何說資料永續性對支付系統很重要?對於諸如支付等系統中的很多元件來說,任何資料都不能丟失,任何資料都是至關重要的。為了實現叢集層面的資料永續性,需要使用分散式資料儲存,這樣就算有例項崩潰,依然可以持久儲存完整的事務。目前,大部分分散式資料儲存服務,例如 Cassandra、MongoDB、HDFS 或 Dynamodb 均支援多種層面的永續性,並且都可以通過配置實現叢集層面的永續性。

訊息持續性和永續性

分散式系統中的節點需要執行計算操作,儲存資料,並在節點之間傳送訊息。對於所傳送的訊息,一個重要特徵在於這些訊息的傳輸可靠度如何。對於關鍵業務系統,通常需要保證絕對不會有任何一條訊息丟失。

對於分散式系統來說,通常會使用某種分散式訊息服務來傳送訊息,例如可能會使用 RabbitMQ、Kafka 等。這些訊息服務可以支援(或通過配置可支援)不同層面的訊息傳輸可靠性。

訊息持續性意味著如果處理訊息的某個節點出現故障,那麼在故障解決完畢後,依然可以繼續處理之前未完成的訊息。訊息永續性通常則主要用於訊息佇列層面(https://en.wikipedia.org/wiki/Message_queue) ,在具備持久的訊息佇列情況下,如果傳送訊息的過程中佇列(或節點)離線,那麼可以在重新上線後繼續傳送這些訊息。關於該話題建議閱讀這篇文章(https://developers.redhat.com/blog/2016/08/10/persistence-vs-durability-in-messaging/) 。

從0到1搞懂分散式架構:Uber大型支付系統構建經驗總結

為何說訊息持續性和永續性對大型支付系統很重要?因為一些訊息丟失的後果是沒有人能承擔的,例如乘客針對行程發起支付所產生的訊息。這意味著我們所使用的訊息系統必須是無損的:每條訊息都需要傳送一次。然而構建一種能夠將每條訊息嚴格傳送一次的系統,以及構建一種將每條訊息至少傳送一次的系統,這兩種系統在複雜度上有著天壤之別。我們決定實現一種可以確保至少傳送一次的持久訊息系統,並選擇一種訊息匯流排,以此為基礎開發我們的支付系統(最終我們選擇了 Kafka,並針對該系統設定了一個無損叢集)。

冪等性

分散式系統不可避免會出錯,例如連線可能中途斷開,或者請求可能超時。客戶端通常會重試這些請求。冪等的系統確保了無論遇到任何情況,無論某個具體的請求被執行了多少遍,最終針對該請求的執行只進行一次。付款過程就是一個很好的例子。如果客戶端發起付款請求,請求已經執行成功了,但客戶端超時,客戶端可能會重試同一個請求。對於冪等的系統,使用者並不會付費兩次;但如果是不冪等的系統,很可能就會了。

設計一個冪等的分散式系統需要運用一些分散式鎖定策略,而早期的一些分散式系統概念也正是源自於此。假設要通過樂觀鎖定(Optimistic Locking)實現一個冪等的系統,以避免產生併發更新。為了實現樂觀鎖定,系統需要實現強一致,這樣在執行操作時我們就可以使用某種型別的版本控制機制檢視是否已經發起了另一個操作。

取決於系統本身的約束以及操作型別,冪等的實現方式有很多。冪等方法的設計過程充滿了挑戰,Ben Nadel 曾撰文(https://www.bennadel.com/blog/3390-considering-strategies-for-idempotency-without-distributed-locking-with-ben-darfler.htm) 介紹過他用過的不同策略,這些策略都用到了分散式鎖或資料庫約束。在設計分散式系統時,冪等也許是最容易被忽略的問題之一。我曾遇到過很多情況因為沒能給某些關鍵操作實現正確的冪等,而導致整個團隊焦頭爛額。

為何說冪等性對大型支付系統很重要?最重要的一點在於:避免重複收費或重複退費。考慮到我們的訊息系統選擇了至少一次的無損傳遞,我們需要確保哪怕所有訊息都被傳遞多次,但最終結果必須保證冪等。我們最終決定通過版本控制和樂觀鎖定,併為系統使用強一致的資料來源,藉此為系統實現所需的冪等行為。

分片和仲裁

分散式系統通常需要儲存大量資料,資料量遠超單一節點的容量。那麼如何用特定數量的多臺計算機儲存一大批資料?此時最常見的做法是分片(Shardinghttps://en.wikipedia.org/wiki/Shard_%28database_architecture%29) 。

資料將使用某種型別的雜湊進行水平分割並分配到不同的分割槽。雖然很多分散式資料庫自帶資料分片功能,但資料分片依然是個很有趣,值得深入學習的話題,尤其是有關重分片(https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6) 的技術。Foursquare 在 2010 年曾因遭遇分片上限遇到長達 17 小時的停機,針對此次事件的根源,有一篇很不錯的事後分析文章(http://highscalability.com/blog/2010/10/15/troubles-with-sharding-what-can-we-learn-from-the-foursquare.html) 告訴了我們來龍去脈。

很多分散式系統的資料或計算工作需要在多個節點上覆制,為確保所有操作均能以一致的方式完成,還需要定義一種基於投票的方法,在這種方法中,只有超過某一數量的節點獲得相同結果後,才認定操作已經成功完成。這個過程叫做仲裁。

為何說仲裁和分片對 Uber 的支付系統很重要?分片和仲裁,這些都是很常用的基本概念。我本人是在研究如何配置 Cassandra 的複製時遇到這些概念的。Cassandra(以及其他分散式系統)會使用仲裁(https://docs.datastax.com/en/archived/cassandra/3.x/cassandra/dml/dmlConfigConsistency.html#dmlConfigConsistency__about-the-quorum-level) 以及本地仲裁來確保整個叢集的一致性。但這也導致了一個有趣的副作用,在我們的幾次會議中,當已經有足夠多的人抵達會議室後,就會有人問:“可以開始了嗎?仲裁結果如何?”

參與者模式

用於描述程式設計實踐的常用詞彙,例如變數、介面、呼叫方法等,全部都基於只有一臺計算機的假設。但對於分散式系統,我們需要使用一種不同的方法。在描述此類系統時,一種最常見的做法是使用參與者模式(Actor Model https://en.wikipedia.org/wiki/Actor_model ),用通訊的思路來理解程式碼。這種模式很流行,並且也很貼合我們思考時的心智模型。例如在描述組織中的人們相互通訊的具體方法時。此外還有一種流行的分散式系統描述方法:CSP - 交談循序程式(https://en.wikipedia.org/wiki/Communicating_sequential_processes) 。

參與者模式中,多名參與者相互傳送訊息並對收到的訊息做出響應。每個參與者只能執行有限的操作,例如建立其他參與者,向其他參與者傳送訊息,決定針對下一條訊息要採取的操作。藉此通過一些簡單的規則,就可以很好地描述複雜的分散式系統,並能在一個參與者崩潰後實現自愈。如果想進一步瞭解這個話題,建議閱讀 Brian Storti(https://twitter.com/brianstorti) 撰寫的 10 分鐘瞭解參與者模式一文(https://www.brianstorti.com/the-actor-model/) 。

目前很多語言都已實現了參與者庫或框架(https://en.wikipedia.org/wiki/Actor_model#Actor_libraries_and_frameworks, 例如 Uber 就在某些系統中使用了Akka toolkit(https://doc.akka.io/docs/akka/2.4/intro/what-is-akka.html)。

為何說參與者模式對大型支付系統很重要?我們有很多工程師聯手打造這個系統,很多人在分散式計算方面有豐富的經驗。因此我們決定在工作中遵照某種標準化的分散式模型以及相應的分散式概念,以便儘可能利用現成的車輪。

響應式架構

在構建大型分散式系統時,目標通常在於使其更具適應性、彈性以及縮放性。無論支付系統或其他高負載系統,模式都是類似的。很多業內人士已經發現並分享了各種情況下的最佳實踐,響應式(Reactive)架構則是這一領域最流行,應用最廣泛的。

如果要了解響應式架構,建議閱讀響應式宣言一文(https://www.reactivemanifesto.org/) 並觀看這段 12 分鐘的視訊(https://www.lightbend.com/blog/understand-reactive-architecture-design-and-programming-in-less-than-12-minutes)。

為何說響應式架構對大型支付系統很重要?我們在構建新支付系統時使用的 Akka 工具包就受到了響應式架構的巨大影響。我們的很多工程師也很熟悉響應式方面的最佳實踐。遵循響應式原則,構建具備適應性和彈性,由訊息驅動的響應式系統,這也成了一種自然而然的做法。這樣一種可以回退並檢查進度的模型,在我看來很實用,以後開發其他系統時我也會使用這樣的模型。

總結

Uber 的支付系統,能夠參與到這樣一個大規模、分散式、關鍵業務系統的重建工作,我覺得自己很幸運。在這樣的工作環境中,我掌握了大量以往根本不瞭解的分散式概念。通過本文的分享,希望能為他人提供一些幫助,幫助大家更好地從事或繼續學習分散式系統知識。

本文主要專注於這類系統的規劃與架構。在構建、部署,以及高負載系統間的遷移和可靠的運維等方面,還有很多重要工作。有機會再另行撰文介紹吧。


更多幹貨內容請關注微信公眾號“AI 前線”,(ID:ai-front)


相關文章