使用GO实现Paxos共识算法的方法
怎么入门Golang编程?需要学习哪些知识点?这是新手们刚接触编程时常见的问题;下面golang学习网就来给大家整理分享一些知识点,希望能够给初学者一些帮助。本篇文章就来介绍《使用GO实现Paxos共识算法的方法》,涉及到GOPaxos、共识算法,有需要的可以收藏一下
什么是Paxos共识算法
最初的服务往往都是通过单体架构对外提供的,即单Server-单Database模式。随着业务的不断扩展,用户和请求数都在不断上升,如何应对大量的请求就成了每个服务都需要解决的问题,这也就是我们常说的高并发。为了解决单台服务器面对高并发的苍白无力,可以通过增加服务器数量来解决,即多Server-单Database(Master-Slave)模式,此时的压力就来到了数据库一方,数据库的IO效率决定了整个服务的效率,继续增加Server数量将无法提升服务性能。这就衍生出了当前火热的微服务架构。当用户请求经由负载均衡分配到某一服务实例上后,如何保证该服务的其他实例最终能够得到相同的数据变化呢?这就要用到Paxos分布式共识协议,Paxos解决的就是共识问题,也就是一段时间后,无论get哪一个服务实例,都能获取到相同的数据。目前国内外的分布式产品很多都使用了Paxos协议,可以说Paxos几乎就是共识协议的标准和代名词。
Paxos有两种协议,我们常常提到的其实是Basic Paxos,另一种叫Multi Paxos,如无特殊说明,本文中提到的Paxos协议均为Basic Paxos。
Paxos协议是由图灵奖获得者Leslie Lamport于1998年在其论文《The Part-Time Parliament》中首次提出的,讲述了一个希腊小岛Paxos是如何通过决议的。但由于该论文晦涩艰深,当时的计算机界大牛们也没几个人能理解。于是Lamport2001年再次发表了《Paxos Made Simple》,摘要部分是这么写的:
The Paxos algorithm, when presented in plain English, is very simple.
翻译过来就是:不会吧,不会吧,这么简单的Paxos算法不会真的有人弄不懂吧?然而事实却是很多人对Paxos都望而却步,理解Paxos其实并不难,但是Paxos的难点在于工程化,如何利用Paxos协议写出一个能过够真正在生产环境中跑起来的服务才是Paxos最难的地方,关于Paxos的工程化可以参考微信后台团队撰写的《微信自研生产级paxos类库PhxPaxos实现原理介绍》
Paxos如何保证一致性的
Paxos协议一共有两个阶段:Prepare和Propose,两种角色:Proposer和Acceptor,每一个服务实例既是Proposer,同时也是Acceptor,Proposer负责提议,Acceptor决定是否接收来自Proposer的提议,一旦提议被多数接受,那么我们就可以宣称对该提议包含的值达成了一致,而且不会再改变。
阶段一:Prepare 准备
- Proposer生成全局唯一ProposalID(时间戳+ServerID)
- Proposer向所有Acceptor(包括Proposer自己)发送Prepare(n = ProposalID)请求
- Acceptor比较n和minProposal, if n > minProposal, minProposal = n,Acceptor返回已接受的提议(acceptedProposal, acceptedValue)
- 承诺1:不再接受n
- 承诺2:不再接受n
- 应答1:返回此前已接受的提议
- 当Proposer收到大于半数的返回后
- Prepare请求被拒绝,重新生成ProposalID并发送Prepare请求
- Prepare请求被接受且有已接受的提议,选择最大的ProposalID对应的值作为提议的值
- Prepare请求被接受且没有已接受的提议,可选择任意提议值
阶段二:Propose 提议
- Proposer向所有Acceptor(包括Proposer自己)发送Accept(n=ProposalID,value=ProposalValue)请求
- Acceptor比较n和minProposal, if n >= minProposal, minProposal = n, acceptedValue = value,返回已接受的提议(minProposal,acceptedValue)
- 当Proposer收到大于半数的返回后
- Propose请求被拒绝,重新生成ProposalID并发送Prepare请求
- Propose请求被接受,则数据达成一致性
一旦提议被半数以上的服务接受,那么我们就可以宣称整个服务集群在这一提议上达成了一致。
需要注意的是,在一个服务集群中以上两个阶段是很有可能同时发生的。 例如:实例A已完成Prepare阶段,并发送了Propose请求。同时实例B开始了Prepare阶段,并生成了更大的ProposalID发送Prepare请求,可能导致实例A的Propose请求被拒绝。 每个服务实例也是同时在扮演Proposer和Acceptor角色,向其他服务发送请求的同时,可能也在处理别的服务发来的请求。
使用GO语言实现Paxos协议
服务注册与发现
由于每个服务实例都是在执行相同的代码,那我们要如何知晓其他服务实例的入口呢(IP和端口号)?方法之一就是写死在代码中,或者提供一份配置文件。服务启动后可以读取该配置文件。但是这种方法不利于维护,一旦我们需要移除或添加服务则需要在每个机器上重新休息配置文件。
除此之外,我们可以通过一个第三方服务:服务的注册与发现来注册并获知当前集群的总服务实例数,即将本地的配置文件改为线上的配置服务。
服务注册:Register函数,服务实例启动后通过调用这个RPC方法将自己注册在服务管理中
func (s *Service) Register(args *RegisterArgs, reply *RegisterReply) error {
s.mu.Lock()
defer s.mu.Unlock()
server := args.ServerInfo
for _, server := range s.Servers {
if server.IPAddress == args.ServerInfo.IPAddress && server.Port == args.ServerInfo.Port {
reply.Succeed = false
return nil
}
}
reply.ServerID = len(s.Servers)
reply.Succeed = true
s.Servers = append(s.Servers, server)
fmt.Printf("Current registerd servers:\n%v\n", s.Servers)
return nil
}
服务发现:GetServers函数,服务通过调用该RPC方法获取所有服务实例的信息(IP和端口号)
func (s *Service) GetServers(args *GetServersArgs, reply *GetServersReply) error {
// return all servers
reply.ServerInfos = s.Servers
return nil
}
Prepare阶段
Proposer,向所有的服务发送Prepare请求,并等待直到半数以上的服务返回结果,这里也可以等待所有服务返回后再处理,但是Paxos协议可以容忍小于半数的服务宕机,因此我们只等待大于N/2个返回即可。当返回的结果有任何一个请求被拒绝,那Proposer即认为这次的请求被拒绝,返回重新生成ProposalID并发送新一轮的Prepare请求。
func (s *Server) CallPrepare(allServers []ServerInfo, proposal Proposal) PrepareReply {
returnedReplies := make([]PrepareReply, 0)
for _, otherS := range allServers {
// use a go routine to call every server
go func(otherS ServerInfo) {
delay := rand.Intn(10)
time.Sleep(time.Second * time.Duration(delay))
args := PrepareArgs{s.Info, proposal.ID}
reply := PrepareReply{}
fmt.Printf("【Prepare】Call Prepare on %v:%v with proposal id %v\n", otherS.IPAddress, otherS.Port, args.ProposalID)
if Call(otherS, "Server.Prepare", &args, &reply) {
if reply.HasAcceptedProposal {
fmt.Printf("【Prepare】%v:%v returns accepted proposal: %v\n", otherS.IPAddress, otherS.Port, reply.AcceptedProposal)
} else {
fmt.Printf("【Prepare】%v:%v returns empty proposal\n", otherS.IPAddress, otherS.Port)
}
s.mu.Lock()
returnedReplies = append(returnedReplies, reply)
s.mu.Unlock()
}
}(otherS)
}
for {
// wait for responses from majority
if len(returnedReplies) > (len(allServers))/2.0 {
checkReplies := returnedReplies
// three possible response
// 1. deny the prepare, and return an empty/accepted proposal
// as the proposal id is not higher than minProposalID on server (proposal id acceptedProposal.ID {
acceptedProposal = r.AcceptedProposal
}
}
// if some other server has accepted proposal, return that proposal with max proposal id
// if no other server has accepted proposal, return an empty proposal
return PrepareReply{HasAcceptedProposal: !acceptedProposal.IsEmpty(), AcceptedProposal: acceptedProposal, PrepareAccepted: true}
}
//fmt.Printf("Waiting for response from majority...\n")
time.Sleep(time.Second * 1)
}
}
Acceptor,通过比较ProposalID和minProposal,如果ProposalID小于等于minProposal,则拒绝该Prepare请求,否则更新minProposal为ProposalID。最后返回已接受的提议
func (s *Server) Prepare(args *PrepareArgs, reply *PrepareReply) error {
s.mu.Lock()
defer s.mu.Unlock()
// 2 promises and 1 response
// Promise 1
// do not accept prepare request which ProposalID s.minProposalID; reply.PrepareAccepted {
// ready to accept the proposal with Id s.minProposalID
s.minProposalID = args.ProposalID
}
reply.HasAcceptedProposal = s.readAcceptedProposal()
reply.AcceptedProposal = s.Proposal
return nil
}
Propose阶段
Proposer,同样首先向所有的服务发送Propose请求,并等待知道半数以上的服务返回结果。如果返回的结果有任何一个请求被拒绝,则Proposer认为这次的请求被拒绝,返回重新生成ProposalID并发送新一轮的Prepare请求
func (s *Server) CallPropose(allServers []ServerInfo, proposal Proposal) ProposeReply {
returnedReplies := make([]ProposeReply, 0)
for _, otherS := range allServers {
go func(otherS ServerInfo) {
delay := rand.Intn(5000)
time.Sleep(time.Millisecond * time.Duration(delay))
args := ProposeArgs{otherS, proposal}
reply := ProposeReply{}
fmt.Printf("【Propose】Call Propose on %v:%v with proposal: %v\n", otherS.IPAddress, otherS.Port, args.Proposal)
if Call(otherS, "Server.Propose", &args, &reply) {
fmt.Printf("【Propose】%v:%v returns: %v\n", otherS.IPAddress, otherS.Port, reply)
s.mu.Lock()
returnedReplies = append(returnedReplies, reply)
s.mu.Unlock()
}
}(otherS)
}
for {
// wait for responses from majority
if len(returnedReplies) > (len(allServers))/2.0 {
checkReplies := returnedReplies
for _, r := range checkReplies {
if !r.ProposeAccepted {
return r
}
}
return checkReplies[0]
}
time.Sleep(time.Second * 1)
}
}
Acceptor,通过比较ProposalID和minProposal,如果ProposalID小于minProposal,则拒绝该Propose请求,否则更新minProposal为ProposalID,并将提议持久化到本地磁盘中。
func (s *Server) Propose(args *ProposeArgs, reply *ProposeReply) error {
if s.minProposalID
<p>运行</p>
<p>运行结果:</p>
<p>这里我一共开启了3个服务实例,并在每次请求之前加入了随机的延迟,模拟网络通信中的延迟,因此每个服务的每个请求并不是同时发出的</p>
<p>动图一张:</p>
<p style="text-align: center;"><img src="/uploads/20221228/167222521963ac21c35e822.gif" loading="lazy" alt=""></p>
<p>静态结果一张:</p>
<p style="text-align: center;"><img src="/uploads/20221228/167222522063ac21c4276a2.png" loading="lazy" alt=""></p>
<p>可以看到3个服务尽管一开始会尝试以他们自己的端口号(5001,5002,5003)作为提议值,在Prepare/Propose失败后,都会重新生成更大的ProposalID并开启新一轮的提议过程(Prepare,Propose),且最后都以5003达成一致。</p>
<p>小结</p>
<p>至此,我们就用GO实现了Paxos协议的核心逻辑。但显而易见的是,这段代码仍然存在很多问题,完全无法满足生产环境的需求</p>
- 通过channel而不是mutex锁来共享数据
- 如何处理服务实例的移除和增加
- 如何避免陷入活锁
今天带大家了解了GOPaxos、共识算法的相关知识,希望对你有所帮助;关于Golang的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~
golang实现java uuid的序列化方法
- 上一篇
- golang实现java uuid的序列化方法
- 下一篇
- Go 实现热重启的详细介绍
-
- Golang · Go教程 | 8分钟前 |
- Go语言处理JSON浮点数编码技巧
- 391浏览 收藏
-
- Golang · Go教程 | 27分钟前 |
- Golangselect多路复用实战教程详解
- 307浏览 收藏
-
- Golang · Go教程 | 38分钟前 |
- MGO存储嵌套结构体方法全解析
- 119浏览 收藏
-
- Golang · Go教程 | 8小时前 | 格式化输出 printf fmt库 格式化动词 Stringer接口
- Golangfmt库用法与格式化技巧解析
- 140浏览 收藏
-
- Golang · Go教程 | 8小时前 |
- Golang配置Protobuf安装教程
- 147浏览 收藏
-
- Golang · Go教程 | 8小时前 |
- Golang中介者模式实现与通信解耦技巧
- 378浏览 收藏
-
- Golang · Go教程 | 9小时前 |
- Golang多协程通信技巧分享
- 255浏览 收藏
-
- Golang · Go教程 | 9小时前 |
- Golang如何判断变量类型?
- 393浏览 收藏
-
- Golang · Go教程 | 9小时前 |
- Golang云原生微服务实战教程
- 310浏览 收藏
-
- Golang · Go教程 | 10小时前 |
- Golang迭代器与懒加载结合应用
- 110浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3167次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3380次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3409次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4513次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3789次使用
-
- Golangmap实践及实现原理解析
- 2022-12-28 505浏览
-
- go和golang的区别解析:帮你选择合适的编程语言
- 2023-12-29 503浏览
-
- 试了下Golang实现try catch的方法
- 2022-12-27 502浏览
-
- 如何在go语言中实现高并发的服务器架构
- 2023-08-27 502浏览
-
- 提升工作效率的Go语言项目开发经验分享
- 2023-11-03 502浏览

