<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Golang 實現(xiàn) Paxos 分布式共識算法

          共 2381字,需瀏覽 5分鐘

           ·

          2020-12-26 16:00

          前文《理解 Paxos》只包含偽代碼,幫助了理解但又不夠爽,既然現(xiàn)在都講究?Talk is cheap. Show me the code.?這次就把文章中的偽代碼用 Go 語言實現(xiàn)出來,希望能幫助各位朋友更直觀的感受 Paxos 論文中的細(xì)節(jié)。

          但我們需要對算法做一些簡化,有多簡單呢?我們不持久化存儲任何變量,并且用?chan?直接代替 RPC 調(diào)用。

          代碼地址:https://github.com/tangwz/paxos/tree/naive

          記得切換到 naive 分支。

          定義相關(guān)結(jié)構(gòu)體

          我們定義 Proposer 如下:

          type proposer struct {
          // server id
          id int
          // the largest round number the server has seen
          round int
          // proposal number = (round number, serverID)
          number int
          // proposal value
          value string
          acceptors map[int]bool
          net network
          }

          這些結(jié)構(gòu)體成員都很容易理解,其中?acceptors?我們主要用來存儲 Acceptors 的地址,以及記錄我們收到 Acceptor 的成功/失敗響應(yīng)。

          Acceptor 的結(jié)構(gòu)體:

          type acceptor struct {
          // server id
          id int
          // the number of the proposal this server will accept, or 0 if it has never received a Prepare request
          promiseNumber int
          // the number of the last proposal the server has accepted, or 0 if it never accepted any.
          acceptedNumber int
          // the value from the most recent proposal the server has accepted, or null if it has never accepted a proposal
          acceptedValue string

          learners []int
          net network
          }

          主要成員解釋都有注釋,簡單來說我們需要記錄三個信息:

          • promiseNumber:承諾的提案編號

          • acceptedNumber:接受的提案編號

          • acceptedValue:接受的提案值

          定義消息結(jié)構(gòu)體

          消息結(jié)構(gòu)體定義了 Proposer 和 Acceptor 之間、Acceptor 和 Leaner 之間的通訊協(xié)議。最主要的還是 Paxos 的兩階段的四個消息。

          • Phase 1 請求:提案編號

          • Phase 1 響應(yīng):如果有被 Accepted 的提案,返回提案編號提案值

          • Phase 2 請求:提案編號提案值

          • Phase 2 響應(yīng):Accepted 的提案編號提案值

          這樣看,我們的消息結(jié)構(gòu)體只需要提案編號和提案值,加上一個消息類型,用來區(qū)分是哪個階段的消息。消息結(jié)構(gòu)體定義在 message.go 文件,具體如下:

          // MsgType represents the type of a paxos phase.
          type MsgType uint8

          const (
          Prepare MsgType = iota
          Promise
          Propose
          Accept
          )

          type message struct {
          tp MsgType
          from int
          to int
          number int // proposal number
          value string // proposal value
          }

          實現(xiàn)網(wǎng)絡(luò)

          網(wǎng)絡(luò)上可以做的選擇和優(yōu)化很多,但這里為了保持簡單的原則,我們將網(wǎng)絡(luò)定義成?interface。后面完全可以改成 RPC 或 API 等其它通信方式來實現(xiàn)(沒錯,我已經(jīng)實現(xiàn)了一個 Go RPC 的版本了)。

          type network interface {
          send(m message)
          recv(timeout time.Duration) (message, bool)
          }

          接下里我們?nèi)崿F(xiàn) network 接口:

          type Network struct {
          queue map[int]chan message
          }

          func newNetwork(nodes ...int) *Network {
          pn := &Network{
          queue: make(map[int]chan message, 0),
          }

          for _, a := range nodes {
          pn.queue[a] = make(chan message, 1024)
          }
          return pn
          }

          func (net *Network) send(m message) {
          log.Printf("net: send %+v", m)
          net.queue[m.to] <- m
          }

          func (net *Network) recvFrom(from int, timeout time.Duration) (message, bool) {
          select {
          case m := <-net.queue[from]:
          log.Printf("net: recv %+v", m)
          return m, true
          case <-time.After(timeout):
          return message{}, false
          }
          }

          就是用?queue?來記錄每個節(jié)點的?chan,key 則是節(jié)點的 server id。

          發(fā)送消息則將?Message?發(fā)送到目標(biāo)節(jié)點的?chan?中,接受消息直接從?chan?中讀取數(shù)據(jù),并等待對應(yīng)的超時時間。

          不需要做其它網(wǎng)絡(luò)地址、包相關(guān)的東西,所以非常簡單。具體在?network.go?文件。

          實現(xiàn)單元測試

          這個項目主要使用 go 單元測試來檢驗正確性,我們主要測試兩種場景:

          • TestSingleProposer(單個 Proposer)

          • TestTwoProposers(多個 Proposer)

          測試代碼通過運行 Paxos 后檢查 Chosen 返回的提案值是否符合預(yù)期。

          實現(xiàn)算法流程

          按照角色將文件分為 proposer.go, acceptor.go 和 learner.go,每個文件都有一個?run()?函數(shù)來運行程序,run()?函數(shù)執(zhí)行條件判斷,并在對應(yīng)的階段執(zhí)行對應(yīng)的函數(shù)。

          按照偽代碼描述,我們很容易實現(xiàn) Phase 1 和 Phase 2,把每個階段的請求響應(yīng)都作為一個函數(shù),我們一步步來看。

          第一輪 Prepare RPCs 請求階段:

          // Phase 1. (a) A proposer selects a proposal number n

          // and sends a prepare request with number n to

          // a majority of acceptors.

          func (p *proposer) prepare() []message {
          p.round++
          p.number = p.proposalNumber()
          msg := make([]message, p.majority())
          i := 0

          for to := range p.acceptors {
          msg[i] = message{
          tp: Prepare,
          from: p.id,
          to: to,
          number: p.number,
          }
          i++
          if i == p.majority() {
          break
          }
          }
          return msg
          }

          // proposal number = (round number, serverID)
          func (p *proposer) proposalNumber() int {
          return p.round<< 16 | p.id
          }

          Prepare 請求階段我們將 round+1 然后發(fā)送給多數(shù)派 Acceptors。

          注:這里很多博客和教程都會將 Prepare RPC 發(fā)給所有的?Acceptors,6.824 的 paxos 實驗就將 RPC 發(fā)送給所有 Acceptors。這里保持和論文一致,只發(fā)送給 a majority of acceptors。

          第一輪 Prepare RPCs 響應(yīng)階段:

          接下來在?acceptor.go?文件中處理請求:

          func (a *acceptor) handlePrepare(args message) (message, bool) {
          if a.promiseNumber >= args.number {
          return message{}, false
          }
          a.promiseNumber = args.number
          msg := message{
          tp: Promise,
          from: a.id,
          to: args.from,
          number: a.acceptedNumber,
          value: a.acceptedValue,
          }
          return msg, true
          }
          • 如果?args.number?大于?acceptor.promiseNumber,則承諾將不會接收編號小于?args.number的提案(即?a.promiseNumber = args.number)。如果之前有提案被 Accepted 的話,響應(yīng)還應(yīng)包含 a.acceptedNumber 和 a.acceptedValue。

          • 否則忽略,返回?false

          第二輪 Accept RPCs 請求階段:

          func (p *proposer) accept() []message {
          msg := make([]message, p.majority())
          i := 0
          for to, ok := range p.acceptors {
          if ok {
          msg[i] = message{
          tp: Propose,
          from: p.id,
          to: to,
          number: p.number,
          value: p.value,
          }
          i++
          }

          if i == p.majority() {
          break
          }
          }
          return msg
          }

          當(dāng) Proposer 收到超過半數(shù) Acceptor 的響應(yīng)后,Proposer 向多數(shù)派的 Acceptor 發(fā)起請求并帶上提案編號和提案值。

          第二輪 Accept RPCs 響應(yīng)階段:

          func (a *acceptor) handleAccept(args message) bool {
          number := args.number
          if number >= a.promiseNumber {
          a.acceptedNumber = number
          a.acceptedValue = args.value
          a.promiseNumber = number
          return true
          }

          return false
          }

          Acceptor 收到?Accept()?請求,在這期間如果 Acceptor 沒有對比 a.promiseNumber 更大的編號另行 Promise,則接受該提案。

          別忘了:Learning a Chosen Value

          在 Paxos 中有一個十分容易混淆的概念:Chosen Value 和 Accepted Value,但如果你看過論文,其實已經(jīng)說得非常直接了。論文的 2.3 節(jié) Learning a Chosen Value 開頭就說:

          To learn that a value has been chosen, a learner must find out that a proposal has been accepted by a majority of acceptors.?

          所以 Acceptor 接受提案后,會將接受的提案廣播 Leaners,一旦 Leaners 收到超過半數(shù)的 Acceptors 的 Accepted 提案,我們就知道這個提案被 Chosen 了。

          func (l *learner) chosen() (message, bool) {
          acceptCounts := make(map[int]int)
          acceptMsg := make(map[int]message)

          for _, accepted := range l.acceptors {
          if accepted.number != 0 {
          acceptCounts[accepted.number]++
          acceptMsg[accepted.number] = accepted
          }
          }

          for n, count := range acceptCounts {
          if count >= l.majority() {
          return acceptMsg[n], true
          }
          }
          return message{}, false
          }

          運行和測試

          代碼拉下來后,直接運行:

          go test

          寫在后面

          為什么不用 mit 6.824 的課程代碼?

          之前我曾把 mit 6.824 的 Raft 答案推到自己的 Github,直到 2020 開課的時候 mit 的助教發(fā)郵件讓我將我的代碼轉(zhuǎn)為 private,因為這樣會導(dǎo)致學(xué)習(xí)課程的人直接搜到代碼,而無法保證作業(yè)獨立完成。

          確實,實驗是計算機(jī)最不可或缺的環(huán)節(jié),用 mit 6.824 2015 的 paxos 代碼會導(dǎo)致很多學(xué)習(xí)者不去自己解決困難,直接上網(wǎng)搜代碼,從而導(dǎo)致學(xué)習(xí)效果不好,違背了 mit 的初衷。

          當(dāng)然,你也可以說現(xiàn)在網(wǎng)上以及很容易搜到 6.824 的各種代碼了,但出于之前 mit 助教的郵件,我不會將作業(yè)代碼直接發(fā)出來。

          感興趣的同學(xué)可以到 2015 版本學(xué)習(xí):http://nil.csail.mit.edu/6.824/2015/

          未來計劃

          • 實現(xiàn)一個完整的(包含網(wǎng)絡(luò)和存儲的) Paxos

          • 基于 Paxos 實現(xiàn)一個 Paxos KV 存儲

          • 實現(xiàn)其它 Paxos 變種

          歡迎各位朋友催更……

          結(jié)語

          本文代碼在 Github 上,如本文有什么遺漏或者不對之處,或者各位朋友有什么新的想法,歡迎提 issue 討論。


          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號 「polarisxu」,回復(fù)?ebook?獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 10
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  三区七区九区一区在线 | 国产精品一区人妻精品阁在线 | 依依成人大香蕉 | 精品aaa| 国产精品久久久久久9999 |