Scalable Open Financial Architecture Stack 是螞蟻金服自主研發(fā)的金融級分布式架構(gòu),包含了構(gòu)建金融級云原生架構(gòu)所需的各個(gè)組件,是在金融場景里錘煉出來的最佳實(shí)踐。
前言 SOFAJRaft 是一個(gè)基于 Raft 一致性算法的生產(chǎn)級高性能 Java 實(shí)現(xiàn),支持 MULTI-RAFT-GROUP,適用于高負(fù)載低延遲的場景。SOFAJRaft 是從百度的 braft 移植而來,做了一些優(yōu)化和改進(jìn),感謝百度 braft 團(tuán)隊(duì)開源了如此優(yōu)秀的 C++ Raft 實(shí)現(xiàn)。 GitHub 地址: https://github.com/alipay/sofa-jraft 之前,我們有一篇介紹 SOFAJRaft 的文章,可在文末獲得鏈接,延續(xù)這個(gè)內(nèi)容,今天的演講分為三部分,先簡要介紹 Raft 算法,然后介紹 SOFAJRaft 的設(shè)計(jì),最后說說它的優(yōu)化。 分享嘉賓:力鯤 螞蟻金服 SOFAJRaft 核心成員 Raft 共識算法 Raft 是一種共識算法,其特點(diǎn)是讓多個(gè)參與者針對某一件事達(dá)成完全一致:一件事,一個(gè)結(jié)論。同時(shí)對已達(dá)成一致的結(jié)論,是不可推翻的??梢耘e一個(gè)銀行賬戶的例子來解釋共識算法:假如由一批服務(wù)器組成一個(gè)集群來維護(hù)銀行賬戶系統(tǒng),如果有一個(gè) Client 向集群發(fā)出“存 100 元”的指令,那么當(dāng)集群返回成功應(yīng)答之后,Client 再向集群發(fā)起查詢時(shí),一定能夠查到被存儲成功的這 100 元錢,就算有機(jī)器出現(xiàn)不可用情況,這 100 元的賬也不可篡改。這就是共識算法要達(dá)到的效果。 Raft 算法和其他的共識算法相比,又有了如下幾個(gè)不同的特性:
共識算法有一個(gè)很典型的應(yīng)用場景就是復(fù)制狀態(tài)機(jī)。Client 向復(fù)制狀態(tài)機(jī)發(fā)送一系列能夠在狀態(tài)機(jī)上執(zhí)行的命令,共識算法負(fù)責(zé)將這些命令以 Log 的形式復(fù)制給其他的狀態(tài)機(jī),這樣不同的狀態(tài)機(jī)只要按照完全一樣的順序來執(zhí)行這些命令,就能得到一樣的輸出結(jié)果。所以這就需要利用共識算法保證被復(fù)制日志的內(nèi)容和順序一致。 Leader 選舉 復(fù)制狀態(tài)機(jī)集群在利用 Raft 算法保證一致性時(shí),要做的第一件事情就是 Leader 選舉。在講 Leader 選舉之前我們先要說一個(gè)重要的概念:Term。Term 用來將一個(gè)連續(xù)的時(shí)間軸在邏輯上切割成一個(gè)個(gè)區(qū)間,它的含義類似于“美國第 26 屆總統(tǒng)”這個(gè)表述中的“26”。 每一個(gè) Term 期間集群要做的第一件事情就是選舉 Leader。起初所有的 Server 都是 Follower 角色,如果 Follower 經(jīng)過一段時(shí)間( election timeout )的等待卻依然沒有收到其他 Server 發(fā)來的消息時(shí),F(xiàn)ollower 就可以認(rèn)為集群中沒有可用的 Leader,遂開始準(zhǔn)備發(fā)起選舉。在發(fā)起選舉的時(shí)候 Server 會從 Follower 角色轉(zhuǎn)變成 Candidate,然后開始嘗試競選 Term + 1 屆的 Leader,此時(shí)他會向其他的 Server 發(fā)送投票請求,當(dāng)收到集群內(nèi)多數(shù)機(jī)器同意其當(dāng)選的應(yīng)答之后,Candidate 成功當(dāng)選 Leader。但是如下兩種情況會讓 Candidate 退回 (step down) 到 Follower,放棄競選本屆 Leader:
當(dāng)然了,當(dāng)一個(gè) Leader 發(fā)現(xiàn)有 Term 更高的 Leader 時(shí)也會退回到 Follower 狀態(tài)。 當(dāng)選舉 Leader 成功之后,整個(gè)集群就可以向外提供正常讀寫服務(wù)了,如圖所示,集群由一個(gè) Leader 兩個(gè) Follower 組成,Leader 負(fù)責(zé)處理 Client 發(fā)起的讀寫請求,同時(shí)還要跟 Follower 保持心跳或者把 Log 復(fù)制給 Follower。 Log 復(fù)制 下面我們就詳細(xì)說一下 Log 復(fù)制。我們之前已經(jīng)說了 Log 就是 Client 發(fā)送給復(fù)制狀態(tài)機(jī)的一系列命令。這里我們再舉例解釋一下 Log,比如我們的復(fù)制狀態(tài)機(jī)要實(shí)現(xiàn)的是一個(gè)銀行賬戶系統(tǒng),那么這個(gè) Log 就可以是 Client 發(fā)給賬戶系統(tǒng)的一條存錢的命令,比如“存 100 元錢”。 Leader 與 Follower 之間的日志復(fù)制是共識算法運(yùn)用于復(fù)制狀態(tài)機(jī)的重要目的,在 Raft 算法中 Log 由 TermId、LogIndex、LogValue 這三要素構(gòu)成,在這張圖上每一個(gè)小格代表一個(gè) Log。當(dāng) Leader 在向 Follower 復(fù)制 Log 的時(shí)候,F(xiàn)ollower 還需要對收到的 Log 做檢查,以確保這些 Log 能和本地已有的 Log 保持連續(xù)。我們之前說了,Raft 算法是要嚴(yán)格保證 Log 的連續(xù)性的,所以 Follower 會拒絕無法和本地已有 Log 保持連續(xù)的復(fù)制請求,那么這種情況下就需要走 Log 恢復(fù)的流程??傊?,Log 復(fù)制的目的就是要讓所有的 Server 上的 Log 無論在內(nèi)容上還是在順序上都要保持完全一致,這樣才能保證所有狀態(tài)機(jī)執(zhí)行結(jié)果一致。 目前已經(jīng)有一些很優(yōu)秀的對 Raft 的實(shí)現(xiàn),比如 C++ 寫的 braft,Go 寫的 etcd,Rust 寫的 TiKV。當(dāng)然了,SOFAJRaft 并不是 Raft 算法的第一個(gè) Java 實(shí)現(xiàn),在我們之前已經(jīng)有了很多項(xiàng)目。但是經(jīng)過我們的評估,覺得目前還是沒有一個(gè) Raft 的 Java 實(shí)現(xiàn)庫類能夠滿足螞蟻生產(chǎn)環(huán)境的要求,這也是我們?nèi)?SOFAJRaft 的主要原因。 SOFAJRaft 介紹 接下來我們介紹 SOFAJRaft。 SOFAJRaft 是基于 Raft 算法的生產(chǎn)級高性能 Java 實(shí)現(xiàn),支持 MULTI-RAFT-GROUP。從去年 3 月開發(fā)到今年 2 月完成,并在今年 3 月開源。應(yīng)用場景有 Leader 選舉、分布式鎖服務(wù)、高可靠的元信息管理、分布式存儲系統(tǒng),目前使用案例有 RheaKV,這是 SOFAJRaft 中自帶的一個(gè)分布式 KV 存儲,還有今天開源的 SOFA 服務(wù)注冊中心中的元信息管理模塊也是用到了 SOFAJRaft,除此之外還有一些內(nèi)部的項(xiàng)目也有使用,但是因?yàn)闆]有開源,所以就不再詳述了。 這張圖就是 SOFAJRaft 的設(shè)計(jì)圖,Node 代表了一個(gè) SOFAJRaft Server 節(jié)點(diǎn),這些方框代表他內(nèi)部的各個(gè)模塊,我們依然用之前的銀行賬戶系統(tǒng)舉例來說明 SOFAJRaft 的各模塊是如何工作的。 當(dāng) Client 向 SOFAJRaft 發(fā)來一個(gè)“存 100 元”的命令之后,Node 的 Log 存儲模塊首先將這個(gè)命令以 Log 的形式存儲到本地,同時(shí) Replicator 會把這個(gè) Log 復(fù)制給其他的 Node,Replicator 是有多個(gè)的,集群中有多少個(gè) Follower 就會有多少個(gè) Replicator,這樣就能實(shí)現(xiàn)并發(fā)的日志復(fù)制。當(dāng) Node 收到集群中半數(shù)以上的 Node 返回的“復(fù)制成功” 的響應(yīng)之后,就可以把這條 Log 以及之前的 Log 有序的送到狀態(tài)機(jī)里去執(zhí)行了。狀態(tài)機(jī)是由用戶來實(shí)現(xiàn)的,比如我們現(xiàn)在舉的例子是銀行賬戶系統(tǒng),所以狀態(tài)機(jī)執(zhí)行的就是賬戶金額的借貸操作。如果 SOFAJRaft 在別的場景中使用,狀態(tài)機(jī)就會有其他的執(zhí)行方式。 Meta Storage 是用來存儲記錄 Raft 實(shí)現(xiàn)的內(nèi)部狀態(tài),比如當(dāng)前 Term、 投票給哪個(gè)節(jié)點(diǎn)等信息。 Snapshot 是快照,所謂快照就是對數(shù)據(jù)當(dāng)前值的一個(gè)記錄,Leader 生成快照有這么幾個(gè)作用:
剛才我們說的是一個(gè)節(jié)點(diǎn)內(nèi)部的情況,那在 Raft Group 中至少需要 3 個(gè)節(jié)點(diǎn),所以這是一個(gè)三副本的架構(gòu)圖。 我們會因?yàn)楦鞣N各樣的需求而去構(gòu)建一個(gè) Raft 集群,如果你的目標(biāo)是實(shí)現(xiàn)一個(gè)存儲系統(tǒng)的話,那單個(gè) Raft 集群可能沒有辦法承載你所有的存儲需求;如果你的目標(biāo)是實(shí)現(xiàn)一個(gè)為用戶請求提供 Service 的系統(tǒng)的話,因?yàn)?Raft 集群內(nèi)只有 Leader 提供讀寫服務(wù),所以讀寫也會形成單點(diǎn)的瓶頸。因此為了支持水平擴(kuò)展,SOFAJRaft 提供了 Multi-Group 部署模式。如圖所示,我們可以按某種 Key 進(jìn)行分片部署,比如用戶 ID,我們讓 Group 1 對 [0, 10000) 的 ID 提供服務(wù),讓 Group 2 對 [10000, 20000) 的 ID 提供服務(wù),以此類推。 SOFAJRaft 特性 這是我們所支持的 Raft 特性,其中:
SOFAJRaft 定位是生產(chǎn)級的 Raft 算法實(shí)現(xiàn),所以除了幾百個(gè)單元測試以及部分 Chaos 測試之外, SOFAJRaft 還使用 jepsen 這個(gè)分布式驗(yàn)證和故障注入測試框架模擬了很多種情況,都已驗(yàn)證通過:
網(wǎng)絡(luò)分區(qū)包括兩種,一種是非對稱網(wǎng)絡(luò)分區(qū),一種是對稱網(wǎng)絡(luò)分區(qū)。 在對稱網(wǎng)絡(luò)分區(qū)中,S2 和其他節(jié)點(diǎn)通信中斷,由于無法和 Leader 通信,導(dǎo)致它不斷嘗試競選 Leader,這樣等到網(wǎng)絡(luò)恢復(fù)的時(shí)候,S2 由于之前的不斷嘗試,其 Term 已經(jīng)高于 Leader 了。這會迫使 S1 退回到 Follower 狀態(tài),集群重新進(jìn)行選舉。為避免這種由于對稱網(wǎng)絡(luò)分區(qū)造成的不必要選舉,SOFAJRaft 增加了預(yù)投票(pre-vote),一個(gè) Follower 在發(fā)起投票前會先嘗試預(yù)投票,只有超過半數(shù)的機(jī)器認(rèn)可它的預(yù)投票,它才能繼續(xù)發(fā)起正式投票。在上面的情況中,S2 在每次發(fā)起選舉的時(shí)候會先嘗試預(yù)選舉,由于在預(yù)選舉中它依然得不到集群內(nèi)多數(shù)派的認(rèn)可,所以預(yù)投票無法成功,S2 也就不會發(fā)起正式投票了,因此他的 Term 也就不會在網(wǎng)絡(luò)分區(qū)的時(shí)候持續(xù)增加了。 在非對稱網(wǎng)絡(luò)分區(qū)中,S2 和 Leader S1 無法通信,但是它和另一個(gè) Follower S3 依然能夠通信。在這種情況下,S2 發(fā)起預(yù)投票得到了 S3 的響應(yīng),S2 可以發(fā)起投票請求。接下來 S2 的投票請求會使得 S3 的 Term 也增加以至于超過 Leader S1(S3 收到 S2 的投票請求后,會相應(yīng)把自己的 Term 提升到跟 S2 一致),因此 S3 接下來會拒絕 Leader S1 的日志復(fù)制。為解決這種情況,SOFAJRaft 在 Follower 本地維護(hù)了一個(gè)時(shí)間戳來記錄收到 Leader 上一次數(shù)據(jù)更新的時(shí)間,F(xiàn)ollower S3 只有超過 election timeout 之后才允許接收預(yù)投票請求,這樣也就避免了 S2 發(fā)起投票請求。 SOFAJRaft 優(yōu)化 接下來我們說一下 SOFAJRaft 的優(yōu)化。 為了提供支持生產(chǎn)環(huán)境運(yùn)行的高性能,SOFAJRaft 主要做了如下幾部分的性能優(yōu)化,其中:
下面我們再說說另外三項(xiàng):批量化、復(fù)制流水線以及線性一致讀。 批量化是性能優(yōu)化最常用的手段之一。SOFAJRaft 通過批量化的手段合并 IO 請求、減少方法調(diào)用和上下文切換,具體包括批量提交 Task、批量網(wǎng)絡(luò)發(fā)送、本地 IO 批量寫入以及狀態(tài)機(jī)批量應(yīng)用。值得一提的是 SOFAJRaft 主要是通過 Disruptor 來實(shí)現(xiàn)批量的消費(fèi)模型,通過這種 Ring Buffer 的方式既可以實(shí)現(xiàn)批量消費(fèi),又不需要為了攢批而等待。 復(fù)制流水線主要是利用 Pipeline 的通信方式來提高日志復(fù)制的效率,如果 Leader 跟 Followers 節(jié)點(diǎn)的 Log 同步是串行 Batch 的方式,那么每個(gè) Batch 發(fā)送之后需要等待 Batch 同步完成之后才能繼續(xù)發(fā)送下一批(ping-pong), 這樣會導(dǎo)致較長的延遲。通過 Leader 跟 Followers 節(jié)點(diǎn)之間的 Pipeline 復(fù)制可以有效降低更新的延遲, 提高吞吐。 什么是線性一致讀呢?簡單來說就是要在分布式環(huán)境中實(shí)現(xiàn) Java volatile 語義的效果,也就是說當(dāng)一個(gè) Client 向集群發(fā)起寫操作的請求并且得到成功響應(yīng)之后,該寫操作的結(jié)果要對所有后來的讀請求可見。和 volatile 的區(qū)別是 volatile 是實(shí)現(xiàn)線程之間的可見,而 SOFAJRaft 需要實(shí)現(xiàn) Server 之間的可見。實(shí)現(xiàn)這個(gè)目的最常規(guī)的辦法是走 Raft 協(xié)議,將讀請求同樣按照 Log 處理,通過 Log 復(fù)制和狀態(tài)機(jī)執(zhí)行來得到讀結(jié)果,然后再把結(jié)果返回給 Client。這種辦法的缺點(diǎn)是需要 Log 存儲、復(fù)制,這樣會帶來刷盤開銷、存儲開銷、網(wǎng)絡(luò)開銷,因此在讀操作很多的場景下對性能影響很大。所以 SOFAJRaft 采用 ReadIndex 來替代走 Raft 狀態(tài)機(jī)的方案,簡單來說就是依靠這樣的原則直接從 Leader 讀取結(jié)果:所有已經(jīng)復(fù)制到多數(shù)派上的 Log(可視為寫操作)就可以被視為安全的 Log,Leader 狀態(tài)機(jī)只要按序執(zhí)行到這條 Log 之后,該 Log 所體現(xiàn)的數(shù)據(jù)就能對 Client 可見了。具體可以分解為以下四個(gè)步驟:
通過 ReadIndex 的優(yōu)化,SOFAJRaft 已經(jīng)能夠達(dá)到 RPC 上限的 80%了。但是我們其實(shí)還可以再往前走一步,上面的步驟中可以看到第 3 步還是需要 Leader 通過向 Followers 發(fā)心跳來確認(rèn)自己的 Leader 身份,因?yàn)?Raft 集群中的 Leader 身份隨時(shí)可能發(fā)生改變。所以我們可以采用 LeaseRead 的方式把這一步 RPC 省略掉。租約可以理解為集群會給 Leader 一段租期(lease)的身份保證,在此期間 Leader 的身份不會被剝奪,這樣當(dāng) Leader 收到讀請求之后,如果發(fā)現(xiàn)租期尚未到期,就無需再通過和 Followers 通信來確認(rèn)自己的 Leader 身份,這樣就可以跳過第 3 步的網(wǎng)絡(luò)通信開銷。通過 LeaseRead 優(yōu)化,SOFAJRaft 幾乎已經(jīng)能夠達(dá)到 RPC 的上限。但是通過時(shí)鐘維護(hù)租期本身并不是絕對的安全(時(shí)鐘漂移問題),所以 SOFAJRaft 中默認(rèn)配置是線性一致讀,因?yàn)橥ǔG闆r下線性一致讀性能已足夠好。 性能 ![]() 這是我們性能測試的情況,測試條件如下:
可以看到在開啟復(fù)制流水線之后,性能可以提升大約 30%。而當(dāng)復(fù)制流水線和 Client-Batching 都開啟之后,8 臺 Client 能夠達(dá)到 40w+ ops。 目前 SOFARaft 最新的版本是 v1.2.4,由于 Raft 算法本身也比較復(fù)雜,而且 SOFAJRaft 在實(shí)現(xiàn)中還做了很多優(yōu)化,所以如果對今天的講演有什么不清楚的地方,歡迎通過 SOFAJRaft wiki 繼續(xù)了解更多細(xì)節(jié),另外我們還有一個(gè)如何使用 SOFAJRaft 的示例,在 wiki 上也有詳細(xì)的說明。除此之外,家純同學(xué)寫過一篇很詳細(xì)的介紹文章《螞蟻金服開源 SOFAJRaft:生產(chǎn)級 Java Raft 算法庫》,大家也可以看一看。 歡迎 Star SOFAJRaft 幫助我們改進(jìn)。 文中涉及到的相關(guān)鏈接
歡迎加入,參與 SOFAJRaft 源碼解析 本文為 SOFAJRaft 的初步介紹,主要還是希望大家對 SOFAJRaft 有一個(gè)初步的認(rèn)識和了解。同時(shí),我們開啟了《剖析 | SOFAJRaft 實(shí)現(xiàn)原理》系列,會逐步詳細(xì)介紹各個(gè)部分的代碼設(shè)計(jì)和實(shí)現(xiàn),預(yù)計(jì)按照如下的目錄進(jìn)行:
|
|