一、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 → 日志条目3 Multi-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 的承诺链。所有处理均在浏览器本地完成,不会泄露你的协议数据。