Understanding Paxos contains only pseudocode, which is helpful but not fun. Now that everyone is paying attention to Talk is cheap. Show me the code. This time, I will use the Go language to realize the pseudo-code in the article, hoping to help friends feel the details in the Paxos paper more intuitively.
But we need to simplify the algorithm a little bit. How simple is it? We do not persist any variables and use CHAN directly instead of RPC calls.
Code address: github.com/tangwz/paxo…
Remember to switch to the Naive branch.
Define related structures
We define a Proposer as follows:
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
}
Copy the code
The members of these structures are easy to understand. Acceptors are used primarily to store the addresses of acceptors and to record the success/failure responses we receive from acceptors.
Acceptor constructs:
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
}
Copy the code
There are notes for the main members’ explanations. In short, we need to record three pieces of information:
promiseNumber
: Proposal number of the commitmentacceptedNumber
: Number of the proposal acceptedacceptedValue
: Accepted proposal value
Define the message structure
The message structure defines the protocol between proposers and acceptors and between acceptors and leaners. The most important is the Paxos two-phase four messages.
- Phase 1 Request: Proposal number
- Phase 1 response: If any proposal was Accepted, the proposal number and proposal value are returned
- Phase 2 Request: Proposal number and proposal value
- Phase 2 response: Accepted proposal number and proposal value
In this way, all our message structure needs is the proposal number and proposal value, plus a message type that distinguishes the phase of the message. The message structure is defined in message.go as follows:
// 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
}
Copy the code
The network
There are many choices and optimizations you can make on the network, but for simplicity’s sake, let’s define the network as an interface. This can be done with RPC or API (yes, I’ve already implemented a version of Go RPC).
type network interface {
send(m message)
recv(timeout time.Duration) (message, bool)}Copy the code
Next, we will implement the network interface:
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}}Copy the code
Queue is used to record the chan of each node, and key is the server ID of the node.
Sending a Message sends the Message to the CHAN of the target node. Receiving the Message reads the data directly from the CHAN and waits for the corresponding timeout.
You don’t have to do anything else with network addresses and packages, so it’s pretty simple. The details are in the network.go file.
Implementing unit tests
In this project, go unit tests were mainly used to verify correctness. We mainly tested two scenarios:
- TestSingleProposer (single Proposer)
- TestTwoProposers (a number of proposers)
The test code checks that the proposal value returned by Chosen meets expectations by running Paxos.
Implementation algorithm flow
The files are numbered numbered numbered numbered. Go, acceptor. Go, and learner.
It is easy to implement Phases 1 and 2 as described in pseudocode, treating each Phase’s request response as a function, as we walk through it step by step.
First round Prepare RPCs request phase:
// 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(a) []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(a) int {
return p.round<< 16 | p.id
}
Copy the code
In the Prepare request phase we send round+1 and send it to the majority Acceptors.
Note: Many blogs and tutorials here send the Prepare RPC to all Acceptors. The 6.824 PAXOS experiment sent the RPC to all Acceptors. To a majority of acceptors.
The first round of Prepare RPCs response stage:
Next process the request in the acceptor.go file:
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
}
Copy the code
- if
args.number
Is greater thanacceptor.promiseNumber
, promises not to receive numbers less than or equal toargs.number
The proposal (i.ea.promiseNumber = args.number
). The response should also contain a.aceptedNumber and A.aceptedValue if a previous proposal was Accepted. - Otherwise ignore and return
false
.
Second Round Accept RPCs request stage:
func (p *proposer) accept(a) []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
}
Copy the code
When a Proposer receives a response with a majority of acceptors, it sends a request to those acceptors with a proposal number and proposal value.
The second round of Accept RPCs response stage:
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
}
Copy the code
An Acceptor receives an Accept() request, and if an Acceptor does not Promise another Acceptor number larger than a.plomenumber during this period, it accepts the proposal.
Remember: Learning a Chosen Value!!
There’s a very confusing concept in Paxos: Chosen Value and Accepted Value, but if you’ve seen the paper, it’s pretty straightforward. Section 2.3 of the paper begins with 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.
So an Acceptor will broadcast the Accepted proposal to Leaners. Once a Leaners receives more than half of the Acceptors’ Accepted proposals, we know that the proposal is Chosen.
func (l *learner) chosen(a) (message, bool) {
acceptCounts := make(map[int]int)
acceptMsg := make(map[int]message)
for _, accepted := range l.acceptors {
ifaccepted.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
}
Copy the code
Run and test
After the code is pulled down, run directly:
go test
Copy the code
Write in the back
Why not use MIT 6.824 course code?
Before, I pushed the answer Raft of MIT 6.824 to my Github, but when the course started in 2020, the TEACHING assistant of MIT sent me an email asking me to make my code private, because this would lead to the students of the course to directly search the code, and the homework could not be completed independently.
Indeed, experiment is the most indispensable part of computer. Using PAxOS code of MIT 6.824 2015 will lead many learners to search the code directly on the Internet instead of solving difficulties by themselves, which will lead to poor learning effect and violate the original intention of MIT.
Of course, you can also say that various codes for 6.824 are easy to find on the Internet, but DUE to the previous email from MIT ta, I will not send out the assignment code directly.
Interested students can go to the 2015 version of the study: nil.csail.mit.edu/6.824/2015/
Future plans
- Implement a complete Paxos (including network and storage)
- Based on Paxos to achieve a Paxos KV storage
- Implement other Paxos variants
Welcome friends urge more……
conclusion
This code is on Github, if there is anything missing or wrong in this article, or any new ideas from friends, welcome to issue discussion.
Recommended reading
-
Understand Paxos (including pseudocode)
-
Paxos variants 1: How did multi-Paxos convince people to choose Raft
-
Paxos by Raft. What score would you get?