使用GO实现Paxos分布式一致性协议
什么是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 <= minProposal的Prepare请求
- 承诺2:不再接受n < minProposal的Propose请求
- 应答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 <= server.minProposalID)
// 2. accept the prepare, and return an empty proposal as the server has not accept any proposal yet
// 3. accept the prepare, and return an accepted proposal // check responses from majority
// find the response with max proposal id
acceptedProposal := NewProposal() for _, r := range checkReplies {
// if any response refused the prepare, this server should resend prepare
if !r.PrepareAccepted {
return r
} if r.HasAcceptedProposal && r.AcceptedProposal.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 <= minProposalID
// Promise 2
// do not accept propose request which ProposalID < minProposalID
// Response 1
// respond with accepted proposal if any if reply.PrepareAccepted = args.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 <= args.Proposal.ID {
s.mu.Lock()
s.minProposalID = args.Proposal.ID
s.Proposal = args.Proposal
s.SaveAcceptedProposal()
s.mu.Unlock() reply.ProposeAccepted = true
} reply.ProposalID = s.minProposalID return nil
}
运行
运行结果:
这里我一共开启了3个服务实例,并在每次请求之前加入了随机的延迟,模拟网络通信中的延迟,因此每个服务的每个请求并不是同时发出的
动图一张:
静态结果一张:
可以看到3个服务尽管一开始会尝试以他们自己的端口号(5001,5002,5003)作为提议值,在Prepare/Propose失败后,都会重新生成更大的ProposalID并开启新一轮的提议过程(Prepare,Propose),且最后都以5003达成一致。
小结
至此,我们就用GO实现了Paxos协议的核心逻辑。但显而易见的是,这段代码仍然存在很多问题,完全无法满足生产环境的需求
- 通过channel而不是mutex锁来共享数据
- 如何处理服务实例的移除和增加
- 如何避免陷入活锁
下一篇我们将探讨如何解决上述问题。
最后欢迎关注我的个人公众号:SoBrian,期待与大家共同交流,共同成长!
使用GO实现Paxos分布式一致性协议的更多相关文章
- 搞懂分布式技术2:分布式一致性协议与Paxos,Raft算法
搞懂分布式技术2:分布式一致性协议与Paxos,Raft算法 2PC 由于BASE理论需要在一致性和可用性方面做出权衡,因此涌现了很多关于一致性的算法和协议.其中比较著名的有二阶提交协议(2 Phas ...
- [转帖]分布式一致性协议介绍(Paxos、Raft)
分布式一致性协议介绍(Paxos.Raft) https://www.cnblogs.com/hugb/p/8955505.html 两阶段提交 Two-phase Commit(2PC):保证一个 ...
- [转帖]图解分布式一致性协议Paxos
图解分布式一致性协议Paxos https://www.cnblogs.com/hugb/p/8955505.html Paxos协议/算法是分布式系统中比较重要的协议,它有多重要呢? <分 ...
- 分布式一致性协议Raft原理与实例
分布式一致性协议Raft原理与实例 1.Raft协议 1.1 Raft简介 Raft是由Stanford提出的一种更易理解的一致性算法,意在取代目前广为使用的Paxos算法.目前,在各种主流语言中都有 ...
- Zookeeper——分布式一致性协议及Zookeeper Leader选举原理
文章目录 一.引言 二.从ACID到CAP/BASE 三.分布式一致性协议 1. 2PC和3PC 2PC 发起事务请求 事务提交/回滚 3PC canCommit preCommit doCommit ...
- 浅谈 Raft 分布式一致性协议|图解 Raft
前言 本篇文章将模拟一个KV数据读写服务,从提供单一节点读写服务,到结合分布式一致性协议(Raft)后,逐步扩展为一个分布式的,满足一致性读写需求的读写服务的过程. 其中将配合引入Raft协议的种种概 ...
- 分布式一致性协议 --- Paxos
问题 Paxos 到底解决什么样的问题,动机是什么 Paxos 流程是怎么样的? Paxos 算法的缺陷是什么 概述 Paxos 是分布式一致性算法,根据少数服从多数的原则多个节点确定某个数值.通过学 ...
- 分布式一致性协议之:Raft算法
一致性算法Raft详解 背景 熟悉或了解分布性系统的开发者都知道一致性算法的重要性,Paxos一致性算法从90年提出到现在已经有二十几年了,而Paxos流程太过于繁杂实现起来也比较复杂,可能也是以为过 ...
- 分布式一致性协议之:Zab(Zookeeper的分布式一致性算法)
Zookeeper使用了一种称为Zab(Zookeeper Atomic Broadcast)的协议作为其一致性复制的核心,据其作者说这是一种新发算法,其特点是充分考虑了Yahoo的具体情况:高吞吐量 ...
随机推荐
- linux驱动之内核多线程(一)
本文摘自http://www.cnblogs.com/zhuyp1015/archive/2012/06/11/2545624.html Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进 ...
- python爬虫以及后端开发--实用加密模板整理
都是作者累积的,且看其珍惜,大家可以尽量可以保存一下,如果转载请写好出处https://www.cnblogs.com/pythonywy 一.md5加密 1.简介 这是一种使用非常广泛的加密方式,不 ...
- BIGI行情-实时行情数据源接口websocket接入方法
BIGI行情-实时行情数据源接口socket接入方法1.国际期货.国内期货.外汇.贵金属.现货.期权.股指.数字货币和A股实时行情和历史行情2.推送的有:socket,websocket,http接收 ...
- php 解决表单重复提交实现方法介绍
重复提交是我们开发中会常碰到的一个问题,除了我们使用js来防止表单的重复提交,同时还可以使用php来防止重复提交哦. 例1 代码如下 复制代码 <?php/* * php中如何防止表单的重复提 ...
- W3C标准和语义化
一.语义化的理解 根据内容选择合适的标签,便于开发者阅读,在写出更优雅的代码的同时让浏览器很好的解析. 目的 1.在没有CSS的情况下,页面也能呈现出很好的内容结构和代码结构: 2.有利于SEO:和搜 ...
- 尝试写一写4gl与4fd
4gl DATABASE ds GLOBALS "../../config/top.global" DEFINE g_curs_index LIKE t ...
- 《spring源码解读》 - ioc之验证模型获取
我们上一篇文章最后调用到 `org.springframework.beans.factory.xml. XmlBeanDefinitionReader#doLoadDocument(...) ` 方 ...
- Java面试题(异常篇)
异常 74.throw 和 throws 的区别? throws是用来声明一个方法可能抛出的所有异常信息,throws是将异常声明但是不处理,而是将异常往上传,谁调用我就交给谁处理.而throw则是指 ...
- 前端系列-CS与BS的区别
现在的系统架构有B/S与C/S之分. C/S,即Client/Server(客户端/服务器).我们一般使用的软件架构都是C/S架构,比如各个系统版本中的软件如qq管家.腾讯qq.office等.C/S ...
- SwiftUI - Grid View 的实现方法,逐步剖析助你实现
简介 在当前正式 SwiftUI 版本而言,很多控件都是缺少的.比如在 UIKit 框架里有 UICollectionView 组件,可以很方便地做 Gird 格子类型的视图.但是在 SwiftUI ...