兩年前,我以一名略懂後端的移動軟件工程師身份加入了 Uber,負責開發該應用的支付功能,並最終重寫了整個應用html
https://eng.uber.com/new-rider-app/。 隨後我轉向工程管理 http://blog.pragmaticengineer.com/things-ive-learned-transitioning-from-engineer-to-engineering-manager/, 負責團隊自己的管理工做。這意味着須要更多地接觸後端,由於支付環節涉及到的不少後端系統都是個人團隊負責的。react
入職 Uber 以前,我對分佈式系統幾乎全無任何經驗。做爲傳統計算機科學畢業生,十多年來我一直在從事全棧軟件開發,然而雖然很擅長畫架構圖並討論各類權衡,但我對諸如一致性、可用性或冪等性等分佈式概念並無太多瞭解。數據庫
本文我將總結本身在構建大規模高可用分佈式系統(Uber 所用的支付系統)過程當中學習和應用的一些心得體會。這個系統須要處理每秒高達數千次的請求,同時就算系統的一些組件故障,也要保證某些關鍵支付功能依然正常運轉。我要說的內容足夠全面嗎?未必!但至少這些內容讓個人工做變得史無前例得簡單。接下來,就一塊兒看看這些工做中不可避免會遇到的 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 來定義本身的各類預期。可用性是最重要的要求之一,一旦肯定了目標,就須要考慮架構中的各項權衡,以此來知足本身的目標。架構
假設使用新系統的業務數量開始增加,那麼負載將只增不減。在某一刻,現有配置可能將沒法支撐更多負載,須要擴容。垂直縮放和水平縮放是目前最經常使用的兩種縮放方式。併發
水平縮放旨在給系統中增長更多計算機(節點),藉此得到更多容量。水平縮放是分佈式系統最經常使用的縮放方式,尤爲是爲集羣增長(虛擬)計算機一般只須要點擊按鈕便可完成。app
垂直縮放能夠理解爲「買一臺更大 / 更強的計算機」,或者換用內核更多、處理能力更強、內存更大的(虛擬)計算機。對於分佈式系統,一般不會選擇垂直縮放,由於相比水平縮放這種作法更貴。然而一些大型網站,例如 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/) 介紹了爲什麼分佈式系統中的持久性那麼難實現。
爲什麼說數據持久性對支付系統很重要?對於諸如支付等系統中的不少組件來講,任何數據都不能丟失,任何數據都是相當重要的。爲了實現集羣層面的數據持久性,須要使用分佈式數據存儲,這樣就算有實例崩潰,依然能夠持久保存完整的事務。目前,大部分分佈式數據存儲服務,例如 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/) 。
爲什麼說消息持續性和持久性對大型支付系統很重要?由於一些消息丟失的後果是沒有人能承擔的,例如乘客針對行程發起支付所產生的消息。這意味着咱們所使用的消息系統必須是無損的:每條消息都須要發送一次。然而構建一種可以將每條消息嚴格發送一次的系統,以及構建一種將每條消息至少發送一次的系統,這兩種系統在複雜度上有着天壤之別。咱們決定實現一種能夠確保至少發送一次的持久消息系統,並選擇一種消息總線,以此爲基礎開發咱們的支付系統(最終咱們選擇了 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)