一、Paxos 要解决什么问题
想象你有一个分布式系统,三个节点各自维护一份数据副本。当客户端请求写入 x=5 时,你需要确保:
- 只有一个值被选定 ------ 不能出现有的节点认为
x=5,有的认为x=3 - 一旦选定,不可更改 ------ 不能先选了
x=5,后来又改成x=3 - 即使部分节点宕机,系统仍能工作 ------ 只要多数派活着就能推进
这就是 共识问题。Paxos 是第一个被严格证明正确的共识算法。
传统思维(错的):
客户端 → 给所有节点发 "x=5"
节点A: 收到 x=5
节点B: 收到 x=5
节点C: 收到 x=5 ← 但 C 宕机了,消息丢了
客户端 → 重试,发给 B 和 C
节点B: 我已经有 x=5 了
节点C: 第一次收到,记下 x=5
另一个客户端 → 发 "x=3" 给 A 和 C
节点A: 我有 x=5,但新的来了,改成 x=3?
节点C: 我有 x=5,但新的来了,改成 x=3?
→ 混乱:A 和 C 改成了 3,B 还是 5
Paxos 的做法:
用提案编号排序,编号大的获胜
一旦一个值被多数派接受,后面的提案必须接受这个值
二、Basic Paxos 角色与流程
角色
| 角色 | 职责 | 类比 |
|---|---|---|
| **Proposer(提议者)** | 发起提案,争取让自己的值被接受 | 竞标者 |
| **Acceptor(接受者)** | 投票决定是否接受提案 | 评委 |
| **Learner(学习者)** | 学习最终选定的值 | 观众 |
一个节点可以同时扮演多个角色。
两阶段流程
第一阶段:Prepare(准备阶段)
Proposer Acceptors
│ │
│ 1. Prepare(n=1) │
│ ──────────────────────────────────→ │
│ │
│ 2. Promise(n=1, last_accepted=∅) │
│ ←────────────────────────────────── │
│ │
│ 3. Promise(n=1, last_accepted=∅) │
│ ←────────────────────────────────── │
│ │
│ (收到多数派 Promise,进入第二阶段) │
第二阶段:Accept(接受阶段)
Proposer Acceptors
│ │
│ 4. Accept!(n=1, value="x=5") │
│ ──────────────────────────────────→ │
│ │
│ 5. Accepted(n=1, value="x=5") │
│ ←────────────────────────────────── │
│ │
│ 6. Accepted(n=1, value="x=5") │
│ ←────────────────────────────────── │
│ │
│ (收到多数派 Accepted,值已选定) │
关键约束
P1:Acceptor 必须接受它收到的第一个提案
P2:如果一个值为 v 的提案被选定,那么所有更高编号的提案也必须包含 v
P2 保证了:一旦选定,不可推翻。
三、核心数据结构
// distributed/paxos/types.go
package paxos
import (
"fmt"
"sync"
"time"
)
// Proposal 提案
type Proposal struct {
Number uint64 // 提案编号(全局唯一、递增)
Value interface{} // 提案值
}
// Promise 承诺
type Promise struct {
Number uint64 // 承诺的提案编号
LastAccepted *Proposal // 该 Acceptor 上次接受的提案(如果有)
}
// Accepted 接受确认
type Accepted struct {
Number uint64
Value interface{}
}
// AcceptorState Acceptor 的状态
type AcceptorState struct {
mu sync.RWMutex
PromisedNum uint64 // 承诺的最高编号
AcceptedProposal *Proposal // 已接受的提案
}
// MessageType 消息类型
type MessageType int
const (
MsgPrepare MessageType = iota // Prepare 请求
MsgPromise // Promise 响应
MsgAccept // Accept 请求
MsgAccepted // Accepted 响应
MsgLearn // Learn 通知
)
// Message 节点间通信的消息
type Message struct {
Type MessageType
From string
To string
Number uint64
Proposal *Proposal
}
// Node 共识节点
type Node struct {
ID string
Addresses map[string]string // nodeID -> address
acceptor *AcceptorState
proposer *ProposerState
learner *LearnerState
network Network
}
// ProposerState Proposer 的状态
type ProposerState struct {
mu sync.Mutex
CurrentNum uint64
HighestSeen uint64
Value interface{}
Promises int
Accepts int
Phase int // 0=idle, 1=prepare, 2=accept
}
// LearnerState Learner 的状态
type LearnerState struct {
mu sync.Mutex
Decided bool
Value interface{}
Accepts map[string]*Accepted // nodeID -> accepted
}
// Network 网络抽象
type Network interface {
Send(msg Message) error
Receive() (Message, error)
Broadcast(msg Message, targets []string) error
}
四、Basic Paxos 实现
// distributed/paxos/basic.go
package paxos
import (
"fmt"
"log"
"math/rand"
"sync"
"time"
)
// BasicPaxos Basic Paxos 实现
type BasicPaxos struct {
nodes map[string]*Node
majority int
mu sync.Mutex
msgQueue chan Message
learnCh chan struct{} // 值选定通知
decidedVal interface{}
}
func NewBasicPaxos(nodeIDs []string) *BasicPaxos {
bp := &BasicPaxos{
nodes: make(map[string]*Node),
majority: len(nodeIDs)/2 + 1,
msgQueue: make(chan Message, 1000),
learnCh: make(chan struct{}, 1),
}
for _, id := range nodeIDs {
bp.nodes[id] = &Node{
ID: id,
acceptor: &AcceptorState{
PromisedNum: 0,
},
proposer: &ProposerState{
Phase: 0,
},
learner: &LearnerState{
Accepts: make(map[string]*Accepted),
},
}
}
// 启动消息处理循环
go bp.messageLoop()
return bp
}
// Propose 发起提案
func (bp *BasicPaxos) Propose(value interface{}) (interface{}, error) {
// 选择一个 Proposer 节点
proposerID := bp.selectProposer()
proposer := bp.nodes[proposerID]
log.Printf("[PAXOS] Node %s proposing value: %v", proposerID, value)
// 生成提案编号(使用时间戳 + 节点ID后缀保证唯一性)
number := uint64(time.Now().UnixNano()) + uint64(rand.Intn(1000))
// 第一阶段:Prepare
log.Printf("[PAXOS] Phase 1: Prepare(n=%d) from %s", number, proposerID)
promises := bp.broadcastPrepare(proposerID, number)
if len(promises) < bp.majority {
return nil, fmt.Errorf("prepare phase failed: got %d promises, need %d",
len(promises), bp.majority)
}
// 检查是否有已接受的值
highestNum := uint64(0)
for _, promise := range promises {
if promise.LastAccepted != nil && promise.LastAccepted.Number > highestNum {
highestNum = promise.LastAccepted.Number
value = promise.LastAccepted.Value
}
}
// 第二阶段:Accept
log.Printf("[PAXOS] Phase 2: Accept(n=%d, value=%v) from %s", number, value, proposerID)
accepts := bp.broadcastAccept(proposerID, number, value)
if len(accepts) < bp.majority {
return nil, fmt.Errorf("accept phase failed: got %d accepts, need %d",
len(accepts), bp.majority)
}
// 值已选定
bp.decidedVal = value
log.Printf("[PAXOS] Value DECIDED: %v (by %s, n=%d)", value, proposerID, number)
// 通知所有 Learner
bp.broadcastLearn(proposerID, number, value)
return value, nil
}
// broadcastPrepare 广播 Prepare 请求
func (bp *BasicPaxos) broadcastPrepare(from string, number uint64) []*Promise {
var mu sync.Mutex
promises := make([]*Promise, 0)
var wg sync.WaitGroup
for id, node := range bp.nodes {
if id == from {
continue
}
wg.Add(1)
go func(nodeID string, n *Node) {
defer wg.Done()
n.acceptor.mu.Lock()
defer n.acceptor.mu.Unlock()
if number > n.acceptor.PromisedNum {
n.acceptor.PromisedNum = number
promise := &Promise{
Number: number,
LastAccepted: n.acceptor.AcceptedProposal,
}
mu.Lock()
promises = append(promises, promise)
mu.Unlock()
log.Printf("[PAXOS] Node %s PROMISE(n=%d, last=%v)",
nodeID, number, promise.LastAccepted)
} else {
log.Printf("[PAXOS] Node %s REJECT prepare(n=%d): already promised %d",
nodeID, number, n.acceptor.PromisedNum)
}
}(id, node)
}
wg.Wait()
return promises
}
// broadcastAccept 广播 Accept 请求
func (bp *BasicPaxos) broadcastAccept(from string, number uint64, value interface{}) []*Accepted {
var mu sync.Mutex
accepts := make([]*Accepted, 0)
var wg sync.WaitGroup
for id, node := range bp.nodes {
if id == from {
continue
}
wg.Add(1)
go func(nodeID string, n *Node) {
defer wg.Done()
n.acceptor.mu.Lock()
defer n.acceptor.mu.Unlock()
// 只有在没有承诺更高编号时才接受
if number >= n.acceptor.PromisedNum {
proposal := &Proposal{Number: number, Value: value}
n.acceptor.AcceptedProposal = proposal
accepted := &Accepted{Number: number, Value: value}
mu.Lock()
accepts = append(accepts, accepted)
mu.Unlock()
log.Printf("[PAXOS] Node %s ACCEPTED(n=%d, value=%v)", nodeID, number, value)
} else {
log.Printf("[PAXOS] Node %s REJECT accept(n=%d): promised %d",
nodeID, number, n.acceptor.PromisedNum)
}
}(id, node)
}
wg.Wait()
return accepts
}
// broadcastLearn 广播 Learn 通知
func (bp *BasicPaxos) broadcastLearn(from string, number uint64, value interface{}) {
for id, node := range bp.nodes {
if id == from {
continue
}
node.learner.mu.Lock()
node.learner.Decided = true
node.learner.Value = value
node.learner.mu.Unlock()
log.Printf("[PAXOS] Node %s LEARNED(value=%v, n=%d)", id, value, number)
}
}
// selectProposer 选择一个 Proposer
func (bp *BasicPaxos) selectProposer() string {
ids := make([]string, 0, len(bp.nodes))
for id := range bp.nodes {
ids = append(ids, id)
}
return ids[rand.Intn(len(ids))]
}
// messageLoop 消息处理循环
func (bp *BasicPaxos) messageLoop() {
for msg := range bp.msgQueue {
switch msg.Type {
case MsgPrepare:
bp.handlePrepare(msg)
case MsgPromise:
bp.handlePromise(msg)
case MsgAccept:
bp.handleAccept(msg)
case MsgAccepted:
bp.handleAccepted(msg)
case MsgLearn:
bp.handleLearn(msg)
}
}
}
func (bp *BasicPaxos) handlePrepare(msg Message) {
node := bp.nodes[msg.To]
if node == nil {
return
}
node.acceptor.mu.Lock()
defer node.acceptor.mu.Unlock()
if msg.Number > node.acceptor.PromisedNum {
node.acceptor.PromisedNum = msg.Number
log.Printf("[PAXOS] Node %s handles PREPARE(n=%d): PROMISE", msg.To, msg.Number)
}
}
func (bp *BasicPaxos) handlePromise(msg Message) {
// 由 Propose 方法处理
}
func (bp *BasicPaxos) handleAccept(msg Message) {
node := bp.nodes[msg.To]
if node == nil {
return
}
node.acceptor.mu.Lock()
defer node.acceptor.mu.Unlock()
if msg.Number >= node.acceptor.PromisedNum {
node.acceptor.AcceptedProposal = msg.Proposal
log.Printf("[PAXOS] Node %s handles ACCEPT(n=%d): ACCEPTED", msg.To, msg.Number)
}
}
func (bp *BasicPaxos) handleAccepted(msg Message) {
// 由 Propose 方法处理
}
func (bp *BasicPaxos) handleLearn(msg Message) {
node := bp.nodes[msg.To]
if node == nil {
return
}
node.learner.mu.Lock()
node.learner.Decided = true
node.learner.Value = msg.Proposal.Value
node.learner.mu.Unlock()
log.Printf("[PAXOS] Node %s LEARNS(value=%v)", msg.To, msg.Proposal.Value)
}
五、活锁问题与解决方案
活锁场景:
Proposer A (n=1) Acceptor群 Proposer B (n=2)
│ │ │
│---- Prepare(1) -------->│ │
│<------- Promise --------│ │
│ │ │
│ │<--- Prepare(2) -----│
│ │------- Promise ---->│
│ │ │
│---- Accept!(1,v1) ---->│ │
│<--- REJECT(promised 2)-│ │
│ │ │
│ │<-- Accept!(2,v2) --│
│ │-- REJECT(promised?)│
│ │ │
│ │ │
│ A 发现被拒,提更大编号 n=3 │
│ B 发现被拒,提更大编号 n=4 │
│ ... 无限循环下去 ... │
解决方案:随机退避 + Leader 选举
// distributed/paxos/livelock.go
package paxos
import (
"math/rand"
"time"
)
// RandomizedProposer 带随机退避的 Proposer
type RandomizedProposer struct {
baseBackoff time.Duration
maxBackoff time.Duration
}
func NewRandomizedProposer() *RandomizedProposer {
return &RandomizedProposer{
baseBackoff: 10 * time.Millisecond,
maxBackoff: 5 * time.Second,
}
}
// ProposeWithBackoff 带退避的提案
func (rp *RandomizedProposer) ProposeWithBackoff(paxos *BasicPaxos, value interface{}, attempt int) (interface{}, error) {
for attempt <= 5 {
result, err := paxos.Propose(value)
if err == nil {
return result, nil
}
// 随机退避
backoff := rp.baseBackoff * time.Duration(1<<uint(attempt))
jitter := time.Duration(rand.Int63n(int64(backoff)))
totalBackoff := backoff + jitter
if totalBackoff > rp.maxBackoff {
totalBackoff = rp.maxBackoff
}
log.Printf("[PAXOS] Attempt %d failed, backing off %v", attempt, totalBackoff)
time.Sleep(totalBackoff)
attempt++
}
return nil, fmt.Errorf("proposal failed after %d attempts", attempt)
}
六、Multi-Paxos 优化思路
Basic Paxos 每次提案都需要两轮 RTT,性能很差。Multi-Paxos 的优化:
-
选一个稳定的 Leader:只有 Leader 才能发起 Prepare
-
一轮 Prepare 管多次 Accept:Leader 上任时跑一次 Prepare,后续 Accept 直接用这个编号
-
日志复制:把 Paxos 应用到日志条目序列上
Basic Paxos(每条日志两轮 RTT):
Prepare(1) → Promise → Accept!(1, v1) → Accepted → 日志条目1
Prepare(2) → Promise → Accept!(2, v2) → Accepted → 日志条目2
Prepare(3) → Promise → Accept!(3, v3) → Accepted → 日志条目3Multi-Paxos(一轮 Prepare 管所有日志):
Prepare(1) → Promise → 领导权确立
Accept!(1, v1) → Accepted → 日志条目1
Accept!(2, v2) → Accepted → 日志条目2
Accept!(3, v3) → Accepted → 日志条目3
(后续 Accept 只需要一轮 RTT)
七、生产坑位
| # | 坑位 | 现象 | 解决方案 |
|---|---|---|---|
| 1 | 活锁 | 多个 Proposer 互相抢占,永远达不成共识 | 随机退避;Leader 选举 |
| 2 | 编号冲突 | 不同节点的提案编号重复 | 编号 = 时间戳高位 + 节点ID低位 |
| 3 | Acceptor 持久化缺失 | 宕机后忘记已承诺的编号,导致一致性破坏 | WAL 日志持久化 PromisedNum 和 AcceptedProposal |
| 4 | 网络分区下的双 Leader | 两个分区各自选出 Leader,都认为自己能提交 | 多数派原则:只有拿到多数派 Promise 的 Leader 才能提交 |
| 5 | 幽灵复现 | 旧 Leader 的提案在网络延迟后在任期外被接受 | 任期号机制(Raft 的做法);逻辑时钟验证 |
| 6 | 性能瓶颈 | 所有提案经过单一 Leader 成为瓶颈 | 并行 Paxos(不同日志条目并行);分片 |
八、本讲总结
┌──────────────────────────────────────────────────────────────────────────┐
│ 第2讲 核心收获 │
│ │
│ ✅ 理解了 Paxos 的核心思想: │
│ - 两阶段提交(Prepare → Promise → Accept → Accepted) │
│ - 多数派原则保证一致性 │
│ - 一旦选定,不可更改(P2 约束) │
│ │
│ ✅ 实现了 Basic Paxos: │
│ - Proposer/Acceptor/Learner 三种角色 │
│ - 提案编号生成(时间戳 + 随机数) │
│ - 完整的 Prepare/Accept/Learn 流程 │
│ │
│ ✅ 理解了活锁问题及其解决方案: │
│ - 随机退避 │
│ - Leader 选举 │
│ │
│ ✅ 理解了 Multi-Paxos 的优化思路: │
│ - 稳定 Leader 减少 Prepare 次数 │
│ - 一轮 Prepare 管多次 Accept │
│ │
│ ✅ 覆盖了 6 个生产坑位与解决方案 │
│ │
└──────────────────────────────────────────────────────────────────────────┘
九、下一讲预告
第3讲:《工业级共识:Raft 算法》
你将学到:
- Raft 的三个子问题:Leader 选举、日志复制、安全性
- 任期(Term)与心跳机制
- 从零实现 Raft 核心逻辑(约 600 行 Go)
- Raft 与 Paxos 的对比
- etcd 中的 Raft 实践
🧰 开发之余的小工具推荐
在调试 Paxos 协议的活锁问题时,zz365.top 的文本差异比较工具可以快速对比不同 Proposer 的提案编号序列,帮助定位编号冲突和抢占模式。其 JSON 格式化工具则可以将节点状态导出为结构化数据,便于分析 Acceptor 的承诺链。所有处理均在浏览器本地完成,不会泄露你的协议数据。