一致性协议浅析:从逻辑时钟到Raft
前言
春節(jié)在家閑著沒事看了幾篇論文,把一致性協(xié)議的幾篇論文都過了一遍。在看這些論文之前,我一直有一些疑惑,比如同樣是有Leader和兩階段提交,Zookeeper的ZAB協(xié)議和Raft有什么不同,Paxos協(xié)議到底要怎樣才能用在實際工程中,這些問題我都在這些論文中找到了答案。接下來,我將嘗試以自己的語言給大家講講這些協(xié)議,使大家能夠理解這些算法。同時,我自己也有些疑問,我會在我的闡述中提出,也歡迎大家一起討論。水平有限,文中難免會有一些紕漏門也歡迎大家指出。
邏輯時鐘
邏輯時鐘其實算不上是一個一致性協(xié)議,它是Lamport大神在1987年就提出來的一個想法,用來解決分布式系統(tǒng)中,不同的機器時鐘不一致可能帶來的問題。在單機系統(tǒng)中,我們用機器的時間來標(biāo)識事件,就可以非常清晰地知道兩個不同事件的發(fā)生次序。但是在分布式系統(tǒng)中,由于每臺機器的時間可能存在誤差,無法通過物理時鐘來準(zhǔn)確分辨兩個事件發(fā)生的先后順序。但實際上,在分布式系統(tǒng)中,只有兩個發(fā)生關(guān)聯(lián)的事件,我們才會去關(guān)心兩者的先來后到關(guān)系。比如說兩個事務(wù),一個修改了rowa,一個修改了rowb,他們兩個誰先發(fā)生,誰后發(fā)生,其實我們并不關(guān)心。那所謂邏輯時鐘,就是用來定義兩個關(guān)聯(lián)事件的發(fā)生次序,即‘happens before’。而對于不關(guān)聯(lián)的事件,邏輯時鐘并不能決定其先后,所以說這種‘happens before’的關(guān)系,是一種偏序關(guān)系。
圖和例子來自于這篇博客
此圖中,箭頭表示進(jìn)程間通訊,ABC分別代表分布式系統(tǒng)中的三個進(jìn)程。
邏輯時鐘的算法其實很簡單:每個事件對應(yīng)一個Lamport時間戳,初始值為0
如果事件在節(jié)點內(nèi)發(fā)生,時間戳加1
如果事件屬于發(fā)送事件,時間戳加1并在消息中帶上該時間戳
如果事件屬于接收事件,時間戳 = Max(本地時間戳,消息中的時間戳) + 1
這樣,所有關(guān)聯(lián)的發(fā)送接收事件,我們都能保證發(fā)送事件的時間戳小于接收事件。如果兩個事件之間沒有關(guān)聯(lián),比如說A3和B5,他們的邏輯時間一樣。正是由于他們沒有關(guān)系,我們可以隨意約定他們之間的發(fā)生順序。比如說我們規(guī)定,當(dāng)Lamport時間戳一樣時,A進(jìn)程的事件發(fā)生早于B進(jìn)程早于C進(jìn)程,這樣我們可以得出A3 ‘happens before’ B5。而實際在物理世界中,明顯B5是要早于A3發(fā)生的,但這都沒有關(guān)系。
邏輯時鐘貌似目前并沒有被廣泛的應(yīng)用,除了DynamoDB使用了vector clock來解決多版本的先后問題(如果有其他實際應(yīng)用的話請指出,可能是我孤陋寡聞了),Google的Spanner 也是采用物理的原子時鐘來解決時鐘問題。但是從Larmport大師的邏輯時鐘算法上,已經(jīng)可以看到一些一致性協(xié)議的影子。
Replicated State Machine
說到一致性協(xié)議,我們通常就會講到復(fù)制狀態(tài)機。因為通常我們會用復(fù)制狀態(tài)機加上一致性協(xié)議算法來解決分布式系統(tǒng)中的高可用和容錯。許多分布式系統(tǒng),都是采用復(fù)制狀態(tài)機來進(jìn)行副本之間的數(shù)據(jù)同步,比如HDFS,Chubby和Zookeeper。
所謂復(fù)制狀態(tài)機,就是在分布式系統(tǒng)的每一個實例副本中,都維持一個持久化的日志,然后用一定的一致性協(xié)議算法,保證每個實例的這個log都完全保持一致,這樣,實例內(nèi)部的狀態(tài)機按照日志的順序回放日志中的每一條命令,這樣客戶端來讀時,在每個副本上都能讀到一樣的數(shù)據(jù)。復(fù)制狀態(tài)機的核心就是圖中 的Consensus模塊,即今天我們要討論的Paxos,ZAB,Raft等一致性協(xié)議算法。
Paxos
Paxos是Lamport大神在90年代提出的一致性協(xié)議算法,大家一直都覺得難懂,所以Lamport在2001又發(fā)表了一篇新的論文《Paxos made simple》,在文中他自己說Paxos是世界上最簡單的一致性算法,非常容易懂……但是業(yè)界還是一致認(rèn)為Paxos比較難以理解。在我看過Lamport大神的論文后,我覺得,除去復(fù)雜的正確性論證過程,Paxos協(xié)議本身還是比較好理解的。但是,Paxos協(xié)議還是過于理論,離具體的工程實踐還有太遠(yuǎn)的距離。我一開始看Paxos協(xié)議的時候也是一頭霧水,看來看去發(fā)現(xiàn)Paxos協(xié)議只是為了單次事件答成一致,而且答成一致后的值無法再被修改,怎么用Paxos去實現(xiàn)復(fù)制狀態(tài)機呢?另外,Paxos協(xié)議答成一致的值只有Propose和部分follower知道,這協(xié)議到底怎么用……但是,如果你只是把Paxos協(xié)議當(dāng)做一個理論去看,而不是考慮實際工程上會遇到什么問題的話,會容易理解的多。Lamport的論文中對StateMachine的應(yīng)用只有一個大概的想法,并沒有具體的實現(xiàn)邏輯,想要直接把Paxos放到復(fù)制狀態(tài)機里使用是不可能的,得在Paxos上補充很多的東西。這些是為什么Paxos有這么多的變種。
Basic-Paxos
Basic-Paxos即Lamport最初提出的Paxos算法,其實很簡單,用三言兩語就可以講完,下面我嘗試著用我自己的語言描述下Paxos協(xié)議,然后會舉出一個例子。要理解Paxos,只要記住一點就好了,Paxos只能為一個值形成共識,一旦Propose被確定,之后值永遠(yuǎn)不會變,也就是說整個Paxos Group只會接受一個提案(或者說接受多個提案,但這些提案的值都一樣)。至于怎么才能接受多個值來形成復(fù)制狀態(tài)機,大家可以看下一節(jié)Multi-Paxos.
Paxos協(xié)議中是沒有Leader這個概念的,除去Learner(只是學(xué)習(xí)Propose的結(jié)果,我們可以不去討論這個角色),只有Proposer和Acceptor。Paxos并且允許多個Proposer同時提案。Proposer要提出一個值讓所有Acceptor答成一個共識。首先是Prepare階段,Proposer會給出一個ProposeID n(注意,此階段Proposer不會把值傳給Acceptor)給每個Acceptor,如果某個Acceptor發(fā)現(xiàn)自己從來沒有接收過大于等于n的Proposer,則會回復(fù)Proposer,同時承諾不再接收ProposeID小于等于n的提議的Prepare。如果這個Acceptor已經(jīng)承諾過比n更大的propose,則不會回復(fù)Proposer。如果Acceptor之前已經(jīng)Accept了(完成了第二個階段)一個小于n的Propose,則會把這個Propose的值返回給Propose,否則會返回一個null值。當(dāng)Proposer收到大于半數(shù)的Acceptor的回復(fù)后,就可以開始第二階段accept階段。但是這個階段Propose能夠提出的值是受限的,只有它收到的回復(fù)中不含有之前Propose的值,他才能自由提出一個新的value,否則只能是用回復(fù)中Propose最大的值做為提議的值。Proposer用這個值和ProposeID n對每個Acceptor發(fā)起Accept請求。也就是說就算Proposer之前已經(jīng)得到過acceptor的承諾,但是在accept發(fā)起之前,Acceptor可能給了proposeID更高的Propose承諾,導(dǎo)致accept失敗。也就是說由于有多個Proposer的存在,雖然第一階段成功,第二階段仍然可能會被拒絕掉。
下面我舉一個例子,這個例子來源于這篇博客
假設(shè)有Server1,Server2, Server3三個服務(wù)器,他們都想通過Paxos協(xié)議,讓所有人答成一致他們是leader,這些Server都是Proposer角色,他們的提案的值就是他們自己server的名字。他們要獲取Acceptor1~3這三個成員同意。首先Server2發(fā)起一個提案【1】,也就是說ProposeID為1,接下來Server1發(fā)起來一個提案【2】,Server3發(fā)起一個提案【3】.
首先是Prepare階段:
假設(shè)這時Server1發(fā)送的消息先到達(dá)acceptor1和acceptor2,它們都沒有接收過請求,所以接收該請求并返回【2,null】給Server1,同時承諾不再接受編號小于2的請求;?
??緊接著,Server2的消息到達(dá)acceptor2和acceptor3,acceptor3沒有接受過請求,所以返回proposer2 【1,null】,并承諾不再接受編號小于1的消息。而acceptor2已經(jīng)接受Server1的請求并承諾不再接收編號小于2的請求,所以acceptor2拒絕Server2的請求;?
??最后,Server3的消息到達(dá)acceptor2和acceptor3,它們都接受過提議,但編號3的消息大于acceptor2已接受的2和acceptor3已接受的1,所以他們都接受該提議,并返回Server3 【3,null】;?
??此時,Server2沒有收到過半的回復(fù),所以重新取得編號4,并發(fā)送給acceptor2和acceptor3,此時編號4大于它們已接受的提案編號3,所以接受該提案,并返回Server2 【4,null】。
接下來進(jìn)入Accept階段,
Server3收到半數(shù)以上(2個)的回復(fù),并且返回的value為null,所以,Server3提交了【3,server3】的提案。?
Server1在Prepare階段也收到過半回復(fù),返回的value為null,所以Server1提交了【2,server1】的提案。?
Server2也收到過半回復(fù),返回的value為null,所以Server2提交了【4,server2】的提案。?
Acceptor1和acceptor2接收到Server1的提案【2,server1】,acceptor1通過該請求,acceptor2承諾不再接受編號小于4的提案,所以拒絕;?
Acceptor2和acceptor3接收到Server2的提案【4,server2】,都通過該提案;?
Acceptor2和acceptor3接收到Server3的提案【3,server3】,它們都承諾不再接受編號小于4的提案,所以都拒絕。?
此時,過半的acceptor(acceptor2和acceptor3)都接受了提案【4,server2】,learner感知到提案的通過,learner開始學(xué)習(xí)提案,所以server2成為最終的leader。
Multi-Paxos
剛才我講了,Paxos還過于理論,無法直接用到復(fù)制狀態(tài)機中,總的來說,有以下幾個原因
- Paxos只能確定一個值,無法用做Log的連續(xù)復(fù)制
- 由于有多個Proposer,可能會出現(xiàn)活鎖,如我在上面舉的例子中,Server2的一共提了兩次Propose才最終讓提案通過,極端情況下,次數(shù)可能會更多
- 提案的最終結(jié)果可能只有部分Acceptor知曉,沒法達(dá)到復(fù)制狀態(tài)機每個instance都必須有完全一致log的需求。
那么其實Multi-Paxos,其實就是為了解決上述三個問題,使Paxos協(xié)議能夠?qū)嶋H使用在狀態(tài)機中。解決第一個問題其實很簡單。為Log Entry每個index的值都是用一個獨立的Paxos instance。解決第二個問題也很簡答,讓一個Paxos group中不要有多個Proposer,在寫入時先用Paxos協(xié)議選出一個leader(如我上面的例子),然后之后只由這個leader做寫入,就可以避免活鎖問題。并且,有了單一的leader之后,我們還可以省略掉大部分的prepare過程。只需要在leader當(dāng)選后做一次prepare,所有Acceptor都沒有接受過其他Leader的prepare請求,那每次寫入,都可以直接進(jìn)行Accept,除非有Acceptor拒絕,這說明有新的leader在寫入。為了解決第三個問題,Multi-Paxos給每個Server引入了一個firstUnchosenIndex,讓leader能夠向向每個Acceptor同步被選中的值。解決這些問題之后Paxos就可以用于實際工程了。
Paxos到目前已經(jīng)有了很多的補充和變種,實際上,之后我要討論的ZAB也好,Raft也好,都可以看做是對Paxos的修改和變種,另外還有一句流傳甚廣的話,“世上只有一種一致性算法,那就是Paxos”。
ZAB
ZAB即Zookeeper Atomic BoardCast,是Zookeeper中使用的一致性協(xié)議。ZAB是Zookeeper的專用協(xié)議,與Zookeeper強綁定,并沒有抽離成獨立的庫,因此它的應(yīng)用也不是很廣泛,僅限于Zookeeper。但ZAB協(xié)議的論文中對ZAB協(xié)議進(jìn)行了詳細(xì)的證明,證明ZAB協(xié)議是能夠嚴(yán)格滿足一致性要求的。
ZAB隨著Zookeeper誕生于2007年,此時Raft協(xié)議還沒有發(fā)明,根據(jù)ZAB的論文,之所以Zookeeper沒有直接使用Paxos而是自己造輪子,是因為他們認(rèn)為Paxos并不能滿足他們的要求。比如Paxos允許多個proposer,可能會造成客戶端提交的多個命令沒法按照FIFO次序執(zhí)行。同時在恢復(fù)過程中,有一些follower的數(shù)據(jù)不全。這些斷論都是基于最原始的Paxos協(xié)議的,實際上后來一些Paxos的變種,比如Multi-Paxos已經(jīng)解決了這些問題。當(dāng)然我們只能站在歷史的角度去看待這個問題,由于當(dāng)時的Paxos并不能很好的解決這些問題,因此Zookeeper的開發(fā)者創(chuàng)造了一個新的一致性協(xié)議ZAB。
ZAB其實和后來的Raft非常像,有選主過程,有恢復(fù)過程,寫入也是兩階段提交,先從leader發(fā)起一輪投票,獲得超過半數(shù)同意后,再發(fā)起一次commit。ZAB中每個主的epoch number其實就相當(dāng)于我接下來要講的Raft中的term。只不過ZAB中把這個epoch number和transition number組成了一個zxid存在了每個entry中。
ZAB在做log復(fù)制時,兩階段提交時,一個階段是投票階段,只要收到過半數(shù)的同意票就可以,這個階段并不會真正把數(shù)據(jù)傳輸給follower,實際作用是保證當(dāng)時有超過半數(shù)的機器是沒有掛掉,或者在同一個網(wǎng)絡(luò)分區(qū)里的。第二個階段commit,才會把數(shù)據(jù)傳輸給每個follower,每個follower(包括leader)再把數(shù)據(jù)追加到log里,這次寫操作就算完成。如果第一個階段投票成功,第二個階段有follower掛掉,也沒有關(guān)系,重啟后leader也會保證follower數(shù)據(jù)和leader對其。如果commit階段leader掛掉,如果這次寫操作已經(jīng)在至少一個follower上commit了,那這個follower一定會被選為leader,因為他的zxid是最大的,那么他選為leader后,會讓所有follower都commit這條消息。如果leader掛時沒有follower commit這條消息,那么這個寫入就當(dāng)做沒寫完。
由于只有在commit的時候才需要追加寫日志,因此ZAB的log,只需要append-only的能力,就可以了。
另外,ZAB支持在從replica里做stale read,如果要做強一致的讀,可以用sync read,原理也是先發(fā)起一次虛擬的寫操作,到不做任何寫入,等這個操作完成后,本地也commit了這次sync操作,再在本地replica上讀,能夠保證讀到sync這個時間點前所有的正確數(shù)據(jù),而Raft所有的讀和寫都是經(jīng)過主節(jié)點的
Raft
Raft是斯坦福大學(xué)在2014年提出的一種新的一致性協(xié)議。作者表示之所以要設(shè)計一種全新的一致性協(xié)議,是因為Paxos實在太難理解,而且Paxos只是一個理論,離實際的工程實現(xiàn)還有很遠(yuǎn)的路。因此作者狠狠地吐槽了Paxos一把:
因此,Raft的作者在設(shè)計Raft的時候,有一個非常明確的目標(biāo),就是讓這個協(xié)議能夠更好的理解,在設(shè)計Raft的過程中,如果遇到有多種方案可以選擇的,就選擇更加容易理解的那個。作者舉了一個例子。在Raft的選主階段,本來可以給每個server附上一個id,大家都去投id最大的那個server做leader,會更快地達(dá)成一致(類似ZAB協(xié)議),但這個方案又增加了一個serverid的概念,同時在高id的server掛掉時,低id的server要想成為主必須有一個等待時間,影響可用性。因此Raft的選主使用了一個非常簡單的方案:每個server都隨機sleep一段時間,最早醒過來的server來發(fā)起一次投票,獲取了大多數(shù)投票即可為主。在通常的網(wǎng)絡(luò)環(huán)境下,最早發(fā)起投票的server也會最早收到其他server的贊成票,因此基本上只需要一輪投票就可以決出leader。整個選主過程非常簡單明了。
除了選主,整個Raft協(xié)議的設(shè)計都非常簡單。leader和follower之間的交互(如果不考慮snapshot和改變成員數(shù)量)一共只有2個RPC call。其中一個還是選主時才需要的RequestVote。也就是說所有的數(shù)據(jù)交互,都只由AppendEntries 這一個RPC完成。
理解Raft算法,首先要理解Term這個概念。每個leader都有自己的Term,而且這個term會帶到log的每個entry中去,來代表這個entry是哪個leader term時期寫入的。另外Term相當(dāng)于一個lease。如果在規(guī)定的時間內(nèi)leader沒有發(fā)送心跳(心跳也是AppendEntries這個RPC call),Follower就會認(rèn)為leader已經(jīng)掛掉,會把自己收到過的最高的Term加上1做為新的term去發(fā)起一輪選舉。如果參選人的term還沒自己的高的話,follower會投反對票,保證選出來的新leader的term是最高的。如果在time out周期內(nèi)沒人獲得足夠的選票(這是有可能的),則follower會在term上再加上1去做新的投票請求,直到選出leader為止。最初的raft是用c語言實現(xiàn)的,這個timeout時間可以設(shè)置的非常短,通常在幾十ms,因此在raft協(xié)議中,leader掛掉之后基本在幾十ms就能夠被檢測發(fā)現(xiàn),故障恢復(fù)時間可以做到非常短。而像用Java實現(xiàn)的Raft庫,如Ratis,考慮到GC時間,我估計這個超時時間沒法設(shè)置這么短。
在Leader做寫入時也是一個兩階段提交的過程。首先leader會把在自己的log中找到第一個空位index寫入,并通過AppendEntries這個RPC把這個entry的值發(fā)給每個follower,如果收到半數(shù)以上的follower(包括自己)回復(fù)true,則再下一個AppendEntries中,leader會把committedIndex加1,代表寫入的這個entry已經(jīng)被提交。如在下圖中,leader將x=4寫入index=8的這個entry中,并把他發(fā)送給了所有follower,在收到第一臺(自己),第三臺,第五臺(圖中沒有畫index=8的entry,但因為這臺服務(wù)器之前所有的entry都和leader保持了一致,因此它一定會投同意),那么leader就獲得了多數(shù)票,再下一個rpc中,會將Committed index往前挪一位,代表index<=8的所有entry都已經(jīng)提交。至于第二臺和第四臺服務(wù)器,log內(nèi)容已經(jīng)明顯落后,這要么是因為前幾次rpc沒有成功。leader會無限重試直到這些follower和leader的日志追平。另外一個可能是這兩臺服務(wù)器重啟過,處于恢復(fù)狀態(tài)。那么這兩臺服務(wù)器在收到寫入index=8的RPC時,follower也會把上一個entry的term和index發(fā)給他們。也就是說prevLogIndex=7,prevLogTerm=3這個信息會發(fā)給第二臺服務(wù)器,那么對于第二臺服務(wù)器,index=7的entry是空的,也就是log和leader不一致,他會返回一個false給leader,leader會不停地從后往前遍歷,直到找到一個entry與第二臺服務(wù)器一致的,從這個點開始重新把leader的log內(nèi)容發(fā)送給該follower,即可完成恢復(fù)。raft協(xié)議保證了所有成員的replicated log中每個index位置,如果他們的term一致,內(nèi)容也一定一致。如果不一致,leader一定會把這個index的內(nèi)容改寫成和leader一致。
其實經(jīng)過剛才我的一些描述,基本上就已經(jīng)把Raft的選主,寫入流程和恢復(fù)基本上都講完了。從這里,我們可以看出Raft一些非常有意思的地方。
第一個有意思的地方是Raft的log的entry是可能被修改的,比如一個follower接收了一個leader的prepare請求,把值寫入了一個index,而這個leader掛掉,新選出的leader可能會重新使用這個index,那么這個follower的相應(yīng)index的內(nèi)容,會被改寫成新的內(nèi)容。這樣就造成了兩個問題,首先第一個,raft的log無法在append-only的文件或者文件系統(tǒng)上去實現(xiàn),而像ZAB,Paxos協(xié)議,log只會追加,只要求文件系統(tǒng)有append的能力即可,不需要隨機訪問修改能力。
第二個有意思的地方是,為了簡單,Raft中只維護了一個Committed index,也就是任何小于等于這個committedIndex的entry,都是被認(rèn)為是commit過的。這樣就會造成在寫入過程中,在leader獲得大多數(shù)選票之前掛掉(或者leader在寫完自己的log之后還沒來得及通知到任何follower就掛掉),重啟后如果這個server繼續(xù)被選為leader,這個值仍然會被commit永久生效。因為leader的log中有這個值,leader一定會保證所有的follower的log都和自己保持一致。而后續(xù)的寫入在增長committedIndex后,這個值也默認(rèn)被commit了。
舉例來說,現(xiàn)在有5臺服務(wù)器,其中S2為leader,但是當(dāng)他在為index=1的entry執(zhí)行寫入時,先寫到了自己的log中,還沒來得及通知其他server append entry就宕機了。?
當(dāng)S2重啟后,任然有可能被重新當(dāng)選leader,當(dāng)S2重新當(dāng)選leader后,仍然會把index=1的這個entry復(fù)制給每臺服務(wù)器(但是不會往前移動Committed index)
此時S2又發(fā)生一次寫入,這次寫入完成后,會把Committed index移動到2的位置,因此index=1的entry也被認(rèn)為已經(jīng)commit了。
這個行為有點奇怪,因為這樣等于raft會讓一個沒有獲得大多數(shù)人同意的值最終commit。這個行為取決于leader,如果上面的例子中S2重啟后沒有被選為leader,index=1的entry內(nèi)容會被新leader的內(nèi)容覆蓋,從而不會提交未經(jīng)過表決的內(nèi)容。
雖然說這個行為是有點奇怪,但是不會造成任何問題,因為leader和follower還是會保持一致,而且在寫入過程中l(wèi)eader掛掉,對客戶端來說是本來就是一個未決語義,raft論文中也指出,如果用戶想要exactly once的語義,可以在寫入的時候加入一個類似uuid的東西,在寫入之前l(fā)eader查下這個uuid是否已經(jīng)寫入。那么在一定程度上,可以保證exactly once的語義。
Raft的論文中也比較了ZAB的算法,文中說ZAB協(xié)議的一個缺點是在恢復(fù)階段需要leader和follower來回交換數(shù)據(jù),這里我沒太明白,據(jù)我理解,ZAB在重新選主的過程中,會選擇Zxid最大的那個從成為主,而其他follower會從leader這里補全數(shù)據(jù),并不會出現(xiàn)leader從follower節(jié)點補數(shù)據(jù)這一說。
后話
目前,經(jīng)過改進(jìn)的Paxos協(xié)議已經(jīng)用在了許多分布式產(chǎn)品中,比如說Chubby,PaxosStore,阿里云的X-DB,以及螞蟻的OceanBase,都選用了Paxos協(xié)議,但他們都多多少少做了一些補充和改進(jìn)。而像Raft協(xié)議,普遍認(rèn)為Raft協(xié)議只能順序commit entry,性能沒有Paxos好,但是TiKV中使用了Raft,其公開的文章宣傳對Raft做了非常多的優(yōu)化,使Raft的性能變的非常可觀。阿里的另外一個數(shù)據(jù)庫PolarDB,也是使用了改進(jìn)版的Parallel-Raft,使Raft實現(xiàn)了并行提交的能力。相信未來會有更多的基于Paxos/Raft的產(chǎn)品會面世,同時也對Raft/Paxos也會有更多的改進(jìn)。
?
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的一致性协议浅析:从逻辑时钟到Raft的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SLS机器学习介绍(02):时序聚类建模
- 下一篇: 奥运转播加速上云,北京冬奥组委测试阿里云