1. 程式人生 > >構建大型支付系統時學到的分散式體系結構概念

構建大型支付系統時學到的分散式體系結構概念

雨中

兩年前我作為一名擁有後臺開發經驗的移動端軟體工程師入職 Uber,並負責 APP 端支付功能的開發以及重構。後來我進入了工程師管理團隊,並獨立帶領一個團隊。由於我的團隊負責很多後端支付相關的系統,因此我有更多的機會接觸整個支付系統的後端知識。

來 Uber 工作之前,我幾乎沒有分散式系統的工作經驗。我的背景是一個傳統的電腦科學學位和十年的全棧軟體開發。然而,雖然我能夠畫架構圖並討論折中方案,但我對分散式的相關概念(一致性,可用性或者冪等性)瞭解的並不多。

在本文中,我總結了一些我認為在構建大規模,高可用分散式系統(為 Uber 提供底層支援的支付系統)時必須學習和應用的概念。這是一個每秒負載高達數千個請求的系統,其中關鍵的支付功能必須能夠正確的工作,即使整個系統的某些部分出現故障。本文會是一個完整的清單嗎?應該不會。但如果我早點知道這些概念的話,我的工作和生活會輕鬆很多。因此,就讓我們開始來了解諸如 SLA,一致性,資料持久化,訊息持久化,冪等性以及其他一些我在工作中需要學習的東西吧。

SLA

對大型系統來說,每天需要處理百萬級別的事件,因此必可避免的會出現問題。在深入設計一個系統前,我發現最重要的事情是確定什麼是一個健康的系統。系統的健康度應該是可衡量的,常用的方法是 SLA:服務等級協議(Service Level Agreements)。我見過的一些最常見的 SLA 有:

  • 可用性:服務正常執行時間的百分比。雖然擁有一個 100% 可用的系統的想法很誘人,但實現這個目標是非常困難的,而且費用高昂。即使像 VISA 信用卡網路,Gmail 或者網際網路提供商這樣的大型和關鍵系統也達不到 100% 的可用性,多年來,它們也會停機幾秒鐘,幾分鐘或者幾小時。對於許多系統來說,四個九的可用性(99.99%,即大約每年有
    50 分鐘的停機時間
    )就被認為是高可用的,通常為了達到這個水平就要花費不少的工作。
  • 準確性:表示在系統中是否允許某些資料不準確或者丟失?如果是,可接受的百分比是多少?對於我從事的支付系統,準確性要求是 100%,這意味著不允許丟失任何資料。
  • 負載能力:系統預期能夠支援多少負載?這通常以每秒請求數來表示。
  • 延遲率:系統應該在多長時間內做出響應?95% 的請求和 99% 的請求的響應時間是多少?系統通常有大量的噪聲請求,因此,P95 和 P99 的響應時間對現實系統而言更加實用。

構建大型支付系統時 SLA 為什麼很重要呢?我們建立一個新系統,並用來取代現有的系統。為了確保我們構建了正確的系統,需要保證新系統比舊系統更好。這時我們就可以使用 SLA 來定義期望值。可用性是最高要求之一。一旦確定了可用性目標,我們就需要在設計架構時為了滿足這一目標作出折中的選擇。

水平擴充套件和垂直擴充套件

假設使用新系統的業務不斷增長,負載會隨著不斷增加。在某個時間點,現有的配置將無法支援更多的負載,需要增加更多的系統容量。這時有兩種最常用的擴充套件策略:水平擴充套件和垂直擴充套件。

水平擴充套件指的是向系統中增加更多的機器/節點,以增加系統總體容量。水平擴充套件是最流行的分散式系統擴容方法,尤其是向叢集中新增(虛擬)機器通常簡單到只需要在網頁上點選一下按鈕。

垂直擴充套件基本上就是通過購買配置更強大的機器來實現的,無非是給(虛擬)機器增加更多的處理器核心,更多的記憶體等。對於分散式系統而言,垂直擴充套件通常不那麼流行,因為它比水平擴充套件費用更高。然而,一些重要的站點,例如 StackOverflow,已經成功的實現垂直擴充套件以滿足系統需求。

為什麼構建大規模的支付系統時,系統擴充套件策略至關重要呢?我們很早就決定建立一個可水平擴充套件的系統。雖然在某些情況下垂直擴充套件是可能的,但由於我們的支付系統已經處於預估的負載,我們對單臺昂貴的大型機在今天這種情況下能否支撐它持悲觀態度,更不用說將來了。我們團隊中也有工程師曾經在大型支付供應商工作過,他們曾試圖在當時能夠買到的大型機上進行系統的垂直擴充套件,但以失敗告終。

一致性

任何系統的可用性都是很重要的。分散式系統通常建立在具有較低可用性的機器上。假設我們的目標是建立一個有 99.999% 可用性的系統(大約每年 5 分鐘時間不可用)。我們使用的機器/節點有平均 99.9% 的可用性(大約每年 8 小時時間不可用)。一個簡單的達到我們目標可用性的方法是把一批機器/節點新增到一個叢集中。即使叢集中一些節點出現故障,也有其他的節點可用,系統總體的可用性將比單個節點的可用性更高。

一致性在高可用系統中是一個關鍵問題。如果叢集中所有節點同時看到並返回相同的資料,則系統是一致的。回到之前的模型,我們通過新增一組節點來獲得更高的可用性,這時確保系統保持一致性並不是一件微不足道的事情。為了確保每個節點具有相同的資料,它們需要互相傳送訊息,以保持之間的資料同步。但是傳送到對方的訊息可能無法到達,它們可能丟失,或者有些節點可能不可用。

一致性是我花費了大量時間去理解的一個概念。有幾種一致性模型,在分散式系統中最常用的是強一致性,弱一致性最終一致性這三種。Hackernoon 網站上這篇最終一致性和強一致性的對比文章對這些模型之間的權衡給出了很好的實用的概述。通常一致性越弱,系統可能越快,但它也更有可能不會返回最新的資料集。

為什麼構建一個大型支付系統時一致性很重要呢?系統中的資料需要保持一致,但到底多一致呢?對於系統中某些部分,只有強一致性的資料才行。例如知道使用者付款操作是否已經開始是需要以強一致性的方式儲存下來的。對於其他不是關鍵業務的部分來說,最終一致性被認為是合理的權衡。一個好的例子是列出最近交易這個功能,這種可以以最終一致性方式實現(也就是說,最近一次交易可能只會在一段時間後才在叢集中某些節點中顯示出來,作為回報,查詢操作將以較低的延遲或者耗費較少資源的方式返回)。

資料持久化

持久化意味著一旦資料成功新增到資料儲存中,那麼將來它將是一直可用的。即使系統中某些節點離線,崩潰或者資料損壞,也不影響資料的可用性。

不同的分散式資料庫具有不同的持久化。 有些支援機器/節點級別的持久化,有些在群集級別支援此功能,有些則不支援該功能。 某種形式的複製通常用於增加持久化: 如果資料儲存在多個節點上,如果一個或多個節點發生故障,資料仍然是可用的。這是一篇關於為什麼在分散式系統中實現資料持久化是具有挑戰性的好文章

為什麼構建大型支付系統時資料持久化很重要呢?對於系統的大部分功能來說,是不允許資料丟失的,因為資料是非常關鍵的,例如支付功能。我們構建的分散式資料儲存需要支援叢集級別的資料持久化:這樣即使叢集中有例項崩潰,已完成的交易依然會被持久化。目前大多數分散式資料儲存服務,如 Cassandra,MongoDB,HDFS 或 Dynamodb 都支援不同級別的資料持久化,並且都可以通過配置提供叢集級別的持久化。

訊息持久化

分散式系統中的節點負責執行計算,儲存資料和相互間傳送訊息。訊息傳送的一個關鍵特性是訊息的可靠性。對於業務關鍵性系統,通常要求訊息零丟失。

對於分散式系統,訊息傳遞通常由某些分散式訊息服務完成,例如 RabbitMQ,Kafka 等。這些訊息服務可以支援(或者通過配置支援)不同級別的訊息傳遞可靠性。

訊息持久化意味著當正在處理訊息的節點發送某些故障時,訊息仍將在故障解決後繼續進行處理。訊息持久化通常用於訊息佇列級別。對於持久化的訊息佇列,如果佇列(或節點)在訊息傳送後離線了,它將在重新聯機後仍然能夠收到訊息。關於這個主題的更多資訊可以看下這篇文章

為什麼構建大型支付系統時訊息持久化至關重要呢?因為我們系統存在不能丟失的訊息,例如消費者為他們的乘車付款的訊息。這意味著我們使用的訊息系統必須是無損的:每條訊息都必須傳遞一次。但是構建一個每條訊息只傳遞一次的系統,和構建一個每條訊息至少傳遞一次的系統,這兩者複雜度是不同的。我們決定實現一個訊息至少傳遞一次的持久化訊息系統,並選擇一個訊息匯流排,並將在此基礎上構建它(我們最終選擇了 Kafka,為此案例配置了訊息無損的叢集)。

冪等性

分散式系統往往存在出錯的可能性,例如連線中斷或請求超時等。客戶端通常會重試這些請求。冪等系統能夠確保無論特定請求執行多少次,該請求的實際執行只發生一次。一個很好的例子就是付款,如果客戶端發出付款的請求,請求成功但客戶端超時了,客戶端可能會重試相同的請求。對於冪等系統,付費的人不會被兩次扣款,對於非冪等系統,則會發生兩次扣款操作。

設計冪等的分散式系統需要某種分散式鎖定策略。這是一些早期分散式系統概念發揮作用的地方。假設我們打算通過樂觀鎖來實現冪等性,以避免併發更新。為了獲得樂觀鎖,系統必須是強一致性的,這樣在操作時,我們可以使用某種版本控制來檢查是否已經有另外一個操作正在進行。

根據系統的約束和操作型別,有多種方法可以實現冪等性。設計冪等方法是一個很好的挑戰,Ben Nadel 在文章中介紹了他使用的不同策略,包括分散式鎖或者資料庫約束。當設計分散式系統時,冪等性可能是最容易被忽視的部分之一。我就遇到過這樣的場景,我的團隊因為沒有確保某些關鍵操作的正確冪等性而飽受煎熬。

為什麼構建大型支付系統時冪等性很重要呢? 最重要的是:避免雙重收費或雙重退款。 鑑於我們的訊息系統至少有一次無損傳遞,我們需要假設所有訊息可能多次傳遞,但系統需要確保冪等性。 我們選擇通過版本控制和樂觀鎖來處理這個問題,讓實現冪等行為的系統使用強一致性儲存作為其資料來源。

分片和 Quorom

分散式系統通常必須比單個節點儲存更多的資料。那麼如何在一定數量的機器上儲存大量資料呢?最常見的技術是使用分片。資料通過某種型別的雜湊演算法進行水平分割槽,並分配給某個分割槽。雖然很多分散式資料庫在底層已經實現了分片,但分片是一個值得進一步瞭解的有趣的領域,尤其是關於重新分片。Foursquare 的系統在 2010 年有 17 小時的停機時間,就是因為遇到了一個分片邊緣案例,關於根本原因有一個很好的剖析

許多分散式系統具有跨多個節點複製的資料或者計算。為了確保以一致的方式執行這些操作,定義了基於投票的方法,其中一定數量的節點需要獲得相同的結果,以使操作成功,這稱為 Quorum。

為什麼在構建 Uber 的支付系統時,quorum 和 分片很重要呢?這兩個都是非常常用的基本概念。我在研究如何配置 Cassandra 副本時遇到了這個概念。Cassandra(以及其他分散式系統)使用 quorum 和 本地 quorum 來確保叢集之間的一致性。作為一個有趣的副作用,在我們的一些會議上,當有足夠的人在房間裡時,有人會問:“我們可以開始嗎?我們有法定人數(quorum)嗎?”

Actor 模型

描述程式設計實踐的常用詞彙,如變數,介面,呼叫方法等,都假設在單機系統上。在討論分散式系統時,我們需要使用一套不同的方法。描述這些系統的常用方法是遵循 actor 模型,它從通訊的角度考慮程式碼。這種模型很流行,因為它與我們想到的心理模型相匹配,例如,描述人們在組織中的溝通方式。另一個描述分散式系統的流行方式是 CSP:通訊順序流程

Actor 模型基於 actors 之間相互發送訊息,並對它們作出響應。每個 actor 可以做一組有限的事情:建立其他 actor,向其他 actor 傳送訊息或者決定如何處理下一條訊息。通過一些簡單的規則,可以很好的描述複雜的分散式系統,在 actor 崩潰後,這些系統也可以自我修復。簡單的總結,我推薦 Brian Storti 寫的十分鐘瞭解 actor 模型 一文。很多程式語言都實現了 actor 函式庫或者框架。例如在 Uber,我們在一些系統中使用 Akka 工具包

為什麼構建大型分散式系統時 actor 模型很重要呢?我們有很多工程師在一起開發系統,其中很多人有分散式的經驗。我們決定遵循一個標準的分散式模型,而不是我們自己提出一個分散式模型概念,從而可能導致重新發明輪子。

響應式架構

在構建大型分散式系統時,目標通常是彈性可擴充套件。可能這是一個支付系統,或者是另外一個高負載系統,但這樣做的模式可能是類似的。業內人士一直在發現和分享這些情況下能夠良好執行的最佳實踐,而其中響應式架構在這個領域是一種流行且廣泛應用的模式。

為什麼構建大型支付系統時,響應式架構很重要呢? Akka,我們用於構建大部分新支付系統的工具包,就深受響應式架構的影響。我們在開發這個系統的很多工程師也熟悉響應式的最佳實踐。遵循響應式原則:建立一個響應的,彈性的且基於訊息驅動的系統,因此這對我們來說非常自然的。我發現它的好處在於擁有一個可信賴的模型,並檢查進度是否處於正確的軌道上,我將繼續使用這個模型來構建以後的系統。

總結

我很幸運的參與了對 Uber 的支付系統這樣一個高可擴充套件,分散式且關鍵的系統的重建。通過在這種環境中工作,我學到了很多以前沒有使用過的分散式概念。通過本文的總結,希望能夠有助於其他人開始或者繼續對分散式系統的學習。

本文重點關注這些系統的設計和架構,關於在高負載系統之間構建,部署和遷移以及可靠的操作它們,還有很多東西要說。但所有這些都是另一篇文章的主題了。