如何建立Monzo銀行後端系統?

banq發表於2016-10-03
Monzo需要從頭開始構建一個銀行後端體系,該系統必須全天候具備可用性,可擴充套件到為遍及世界各地的數以百萬計客戶提供服務。這篇文章解釋了我們如何以開源的技術建立這樣現代系統。

Monzo – Building a Modern Bank Backend

從一開始,我們就在後端建立分散式微服務架構,對於早期創業,這個架構是相當不尋常的,因為大多數企業會開始時使用眾所周知的框架和關聯式資料庫建立一種集中式的應用程式。

但是我們有一個野心,打造最好的一個典型案例,能夠擴充套件到為數以百萬計的客戶服務,我們必須是擁有最好技術的銀行。 每日發生間隙停機,單點故障或維護視窗在這裡是不能接受的,客戶希望全天候,不間斷的能夠訪問他們的鈔票賬戶,我們希望在幾個小時內,而不是幾個月時間不斷推出新功能。

像亞馬遜,Netflix和Twitter的大型網路公司已經表明,單片整體程式碼庫是不能擴充套件到為大量使用者服務的,而且關鍵的是無法擴充套件到大量的開發人員同時維護同一個整體程式碼庫。 因此,如果我們要在許多市場中運作,每一個具有獨特的要求,我們會需要很多團隊,對產品的不同領域有不同的工作。 這些團隊需要各自控制自己的開發,部署和規模 - 而不需要協調他們與其他球隊的變化。 總之,開發過程中需要進行分配和去耦合,就像我們的軟體架構一樣。

開始時確實只有3個後端開發人員,這點我們必須務實。 我們必須選擇正確的構建產品方向。

當我們推出了Monzo 公測時 ,我們的後端已發展到近100個服務(目前約150),我們的工程團隊已長得太大。 我們知道我們的銀行牌照頒發也迫在眉睫了,感覺這像一個好時機,重新考慮一些做出的架構選擇。 我們舉行了一個工程師會議,並確定了重點的幾個方面:

1.叢集管理
我們需要一個高效,自動化的方式來管理大量的伺服器,其中有效地分配工作,並對機器故障作出反應。

使用Docker容器化叢集方案,經過一年的Mesos和Marathon使用,決定改用Kubernetes。成本降低了1/4

2.多語種服務
Go是非常適合建設微服務,將可能作為Monzo的主要語言,但使用它僅僅並不意味著我們不能採取其他語言工具的優勢。

共用抽象功能不是透過庫包方式共享,而是直接透過服務方式共享,比如為了得到一個分散式鎖功能,可以透過輕量RPC呼叫專門提供鎖功能的服務,在所有共享基礎設施比如資料庫和訊息佇列之前建立相應服務,這樣可以透過不同語言RPC客戶端呼叫這些基礎設施服務。


3.RPC傳輸
隨著大量服務的跨主機分佈,跨資料中心,甚至是跨大陸,我們系統的成功取決於有一個堅實的RPC層,可以定位元件故障線路,減少了等待時間,並幫助我們理解其執行時的行為。

RPC層有以下好處:
負載平衡:大多數HTTP客戶端庫可以基於DNS的平衡進行迴圈負載 ,但是這是一個相當生硬的手段。 理想情況下,負載平衡器將選擇最合適的宿主主機,儘量減少故障和延遲,即使是在發生故障,但因為慢副本的存在,系統將保持快速恢復執行。

自動重試:在分散式系統中, 失敗是必然的 。 如果下游服務處理失敗的冪等請求時,它可以安全地重試另外不同的副本,這樣以確保該系統即使存在發生故障的元件,也擁有一個整體的彈性。

連線池:為每個請求一個開啟新的連線會有一個巨大延遲。 理想的情況下,請求將分配到預先存在的連線中以實現多路複用。

路由:它是非常強大的,能夠改變RPC系統的執行時行為。 例如,當我們部署服務的新版本,我們可能要整體流量的一小部分傳送給它以檢查它的行為是否符合預期。 這個路由機制也可用於內部測試。

Finagle顯然是最複雜的RPC系統。 它擁有所有我們希望更多的功能,而且它有一個非常乾淨的,模組化的架構。 已經在Twitter的生產中使用了好幾年。 linkerd 是另外一個RPC代理。

我們不是直接伺服器與伺服器對話通訊,我們的服務是跟linkerd本地副本對話,然後請求被負載平衡器路由到下游節點,當冪等請求失敗,它們會自動重試(使用budget )。 這種複雜的邏輯都沒有被包含單個服務中,這意味著我們可以自由地寫我們在任何語言服務,而無需維護許多這樣與通訊對話有關的RPC庫。

部署linkerd作為Kubernetes的 daemon set ,這樣每個服務總是和localhost的linkerd交談,它會轉發請求。 在某些情況下,如果主機上不存在這兩種需要對話交談與通訊的服務複製時。RPC實際上就不會遍歷網路。

4.非同步訊息傳遞
為了使我們的後端高效能和彈性,我們使用訊息佇列來排隊任務。這些佇列必須提供強有力的保證,任務總是可以排隊和出隊,和資訊永不丟失。

我們的後端的大部分工作都是非同步的。 例如,即使終端到終端的支付處理時間不到一秒鐘,我們還要讓支付網路在幾十毫秒內響應“批准”或“拒絕”Monzo銀行卡上的交易。 這是透過讓商家資料傳送推送通知,甚至插入交易到使用者的資料表中的工作都是非同步發生。

儘管如此非同步性,這些步驟是非常重要的, 絕不能跳過。 即使發生錯誤而不能自動恢復,也必須解決它,並恢復該過程。 這給了我們為我們的非同步架構幾個要求:

高可用:傳送者必須能夠對訊息佇列中“fire-and-forget”,無論失敗的節點或下游消費者資訊的狀態是什麼狀態,都不影響傳送者,傳送者傳送訊息後就忘記了,不管後面結果。

擴充套件性:我們必須能夠對訊息佇列擴容,同時無需中斷服務,無需升級硬體 - 訊息佇列本身必須是橫向擴充套件,就像我們服務的其餘部分。

永續性:如果訊息佇列的節點出現故障,我們不能丟失資料。 同樣,如果訊息消費者失敗,它應該可以重新傳遞訊息,然後再試一次。

再現:它是能夠從歷史上的一個點重播訊息流,這對新的程式可以在舊資料執行(和重新執行)非常有用。

至少有一次交付:所有資訊必須傳遞到他們的消費者,雖然確切地一次傳遞資訊一般是不可能的,但是訊息傳遞操作的“正常”模式就不應該是訊息將被傳遞多次。

Kafka似乎是天然的適合這些要求。 它的架構是非常不尋常的一個訊息佇列-它實際上覆制的是提交的日誌 。它的複製,分割槽設計意味著它可以容忍節點故障,並且放大和縮小而無需中斷服務。

其消費者的設計也很有意思:大多數系統可為每個消費者維護其自己的訊息佇列,卡夫卡一個消費者僅僅是對應到訊息日誌一個“遊標”。這使得釋出/訂閱系統的成本要低得多,而且由於訊息無論什麼時候都會儲存一段時間,我們就可以輕鬆播放早期事件回到某些服務。

相關文章