用 Golang 快速实现 Paxos 分布式共识算法

简介:   前文《理解 Paxos》只包含伪代码,帮助了理解但又不够爽,既然现在都讲究 Talk is cheap. Show me the code.这次就把文章中的伪代码用 Go 语言实现出来,希望能帮助各位朋友更直观的感受 Paxos 论文中的细节。  但我们需要对算法做一些简化,有多简单呢?我们不持久化存储任何变量,并且用 chan直接代替 RPC 调用。

  前文《理解 Paxos》只包含伪代码,帮助了理解但又不够爽,既然现在都讲究 Talk is cheap. Show me the code.这次就把文章中的伪代码用 Go 语言实现出来,希望能帮助各位朋友更直观的感受 Paxos 论文中的细节。

  但我们需要对算法做一些简化,有多简单呢?我们不持久化存储任何变量,并且用 chan直接代替 RPC 调用。

  记得切换到 naive 分支。

  我们定义 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

  }

  这些结构体成员都很容易理解,其中 acceptors我们主要用来存储 Acceptors 的地址,以及记录我们收到 Acceptor 的成功/失败响应。

  Acceptor 的结构体:

  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 if it has never accepted a proposal

  acceptedValue string

  learners int

  net network

  }

  主要成员解释都有注释,简单来说我们需要记录三个信息:

  promiseNumber:承诺的提案编号

  acceptedNumber:接受的提案编号

  acceptedValue:接受的提案值

  消息结构体定义了 Proposer 和 Acceptor 之间、Acceptor 和 Leaner 之间的通讯协议。最主要的还是 Paxos 的两阶段的四个消息。

  Phase 1 请求:提案编号

  Phase 1 响应:如果有被 Accepted 的提案,返回提案编号和提案值

  Phase 2 请求:提案编号和提案值

  Phase 2 响应:Accepted 的提案编号和提案值

  这样看,我们的消息结构体只需要提案编号和提案值,加上一个消息类型,用来区分是哪个阶段的消息。消息结构体定义在 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

  }

  网络上可以做的选择和优化很多,但这里为了保持简单的原则,我们将网络定义成 interface。后面完全可以改成 RPC 或 API 等其它通信方式来实现(没错,我已经实现了一个 Go RPC 的版本了)。

  type network interface {

  send(m message)

  recv(timeout time.Duration) (message, bool)

  }

  接下里我们去实现 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来记录每个节点的chan,key 则是节点的 server id。

  发送消息则将 Message发送到目标节点的chan中,接受消息直接从chan中读取数据,并等待对应的超时时间。

  不需要做其它网络地址、包相关的东西,所以非常简单。具体在 network.go文件。

  这个项目主要使用 go 单元测试来检验正确性,我们主要测试两种场景:

  TestSingleProposer(单个 Proposer)

  TestTwoProposers(多个 Proposer)

  测试代码通过运行 Paxos 后检查 Chosen 返回的减肥提案值是否符合预期。

  按照角色将文件分为 proposer.go, acceptor.go 和 learner.go,每个文件都有一个 run函数来运行程序,run函数执行条件判断,并在对应的阶段执行对应的函数。

  按照伪代码描述,我们很容易实现 Phase 1 和 Phase 2,把每个阶段的请求响应都作为一个函数,我们一步步来看。

  // 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 然后发送给多数派 Acceptors。

  注:这里很多博客和教程都会将 Prepare RPC 发给所有的Acceptors,6.824 的 paxos 实验就将 RPC 发送给所有 Acceptors。这里保持和论文一致,只发送给 a majority of acceptors。

  接下来在 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 的话,响应还应包含 a.acceptedNumber 和 a.acceptedValue。

  否则忽略,返回 false。

  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

  }

  当 Proposer 收到超过半数 Acceptor 的响应后,Proposer 向多数派的 Acceptor 发起请求并带上提案编号和提案值。

  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,则接受该提案。

  在 Paxos 中有一个十分容易混淆的概念:Chosen Value 和 Accepted Value,但如果你看过论文,其实已经说得非常直接了。论文的 2.3 节 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 收到超过半数的 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 的 Raft 答案推到自己的 Github,直到 2020 开课的时候 mit 的助教发邮件让我将我的代码转为 private,因为这样会导致学习课程的人直接搜到代码,而无法保证作业独立完成。

  确实,实验是计算机最不可或缺的环节,用 mit 6.824 2015 的 paxos 代码会导致很多学习者不去自己解决困难,直接上网搜代码,从而导致学习效果不好,违背了 mit 的初衷。

  当然,你也可以说现在网上以及很容易搜到 6.824 的各种代码了,但出于之前 mit 助教的邮件,我不会将作业代码直接发出来。

目录
相关文章
|
7天前
|
存储 人工智能 算法
解锁分布式文件分享的 Java 一致性哈希算法密码
在数字化时代,文件分享成为信息传播与协同办公的关键环节。本文深入探讨基于Java的一致性哈希算法,该算法通过引入虚拟节点和环形哈希空间,解决了传统哈希算法在分布式存储中的“哈希雪崩”问题,确保文件分配稳定高效。文章还展示了Java实现代码,并展望了其在未来文件分享技术中的应用前景,如结合AI优化节点布局和区块链增强数据安全。
|
5月前
|
算法 Go
[go 面试] 雪花算法与分布式ID生成
[go 面试] 雪花算法与分布式ID生成
|
2月前
|
算法 关系型数据库 MySQL
分布式唯一ID生成:深入理解Snowflake算法在Go中的实现
在分布式系统中,确保每个节点生成的 ID 唯一且高效至关重要。Snowflake 算法由 Twitter 开发,通过 64 位 long 型数字生成全局唯一 ID,包括 1 位标识位、41 位时间戳、10 位机器 ID 和 12 位序列号。该算法具备全局唯一性、递增性、高可用性和高性能,适用于高并发场景,如电商促销时的大量订单生成。本文介绍了使用 Go 语言的 `bwmarrin/snowflake` 和 `sony/sonyflake` 库实现 Snowflake 算法的方法。
104 1
分布式唯一ID生成:深入理解Snowflake算法在Go中的实现
|
1月前
|
存储 算法 安全
分布式系统架构1:共识算法Paxos
本文介绍了分布式系统中实现数据一致性的重要算法——Paxos及其改进版Multi Paxos。Paxos算法由Leslie Lamport提出,旨在解决分布式环境下的共识问题,通过提案节点、决策节点和记录节点的协作,确保数据在多台机器间的一致性和可用性。Multi Paxos通过引入主节点选举机制,优化了基本Paxos的效率,减少了网络通信次数,提高了系统的性能和可靠性。文中还简要讨论了数据复制的安全性和一致性保障措施。
61 1
|
2月前
|
存储 缓存 算法
分布式缓存有哪些常用的数据分片算法?
【10月更文挑战第25天】在实际应用中,需要根据具体的业务需求、数据特征以及系统的可扩展性要求等因素综合考虑,选择合适的数据分片算法,以实现分布式缓存的高效运行和数据的合理分布。
|
2月前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
3月前
|
算法
基于粒子群算法的分布式电源配电网重构优化matlab仿真
本研究利用粒子群算法(PSO)优化分布式电源配电网重构,通过Matlab仿真验证优化效果,对比重构前后的节点电压、网损、负荷均衡度、电压偏离及线路传输功率,并记录开关状态变化。PSO算法通过迭代更新粒子位置寻找最优解,旨在最小化网络损耗并提升供电可靠性。仿真结果显示优化后各项指标均有显著改善。
|
5月前
|
存储 算法 NoSQL
(七)漫谈分布式之一致性算法下篇:一文从根上儿理解大名鼎鼎的Raft共识算法!
Raft通过一致性检查,能在一定程度上保证集群的一致性,但无法保证所有情况下的一致性,毕竟分布式系统各种故障层出不穷,如何在有可能发生各类故障的分布式系统保证集群一致性,这才是Raft等一致性算法要真正解决的问题。
155 11
|
3月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
1月前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
180 5

热门文章

最新文章