本文讲分布式的共识概念,算法,应用。涉及到raft,paxos(basic,multi,fast),zk,etcd,chubby。以及思考。关注与如何共识,对于zk,etcd,chubby的接口如何应用于服务发现,分布式锁等略过。
共识:一个或多个节点提出提案,算法选择一个值。一致同意,完整(只决定一次),有效(节点投票是有效值,是被提议的值),终止(宕机不回来)。
paxos完全符合,但raft,zap考虑的是宕机还会回来的情况,用日志保证。能解决上篇(https://segmentfault.com/a/11...)中诸如以下问题:
全序广播相当于重复多伦共识:raft和zap等直接实现全序广播,mutil-paxos也是,都有leader,只决定值的顺序。有固定leader比每个节点写入,leader维持全序,其他节点同步,冲突少(若没有leader paxos那种要先同步一遍获取最大值,再投票,多了一次pre),画一个有领导和无领导每个带序号的图可以很好理解带leader会容易些,但单leader有瓶颈。
单领导类的共识:1选出一位领导者,2对领导者的提议进行表决(防止1,一个节点相信自己是领导)投票是同步的,动态成员扩展难,依靠超时检测节点失效,若只有一条特定网络不可靠,会进入领导频繁二人转局面。有领导后也不能领导决定,防止重新恢复等多领导定的情况。
缺点:要多数都同意,很慢。基本动态扩容很难,手动比如etcd
共识算法
raft
- 共识过程
数据一致性是通过日志复制的方式,client发给leader(写只发给leader,follower备份恢复用),leader写入日志,同步给follower,当多数follower写入日志并返回给leader时,leader提交数据,返回给客户端确认消息, 发给follower数据已提交,follower提交数据,发回确认给leader。所有的发送都随着跳频发过去。raft中所有server之间的通信都是RPC调用,并且只有两种类型的RPC调用:第一种是RequestVote,用于选举leader;第二种是AppendEntries。日志和投票结果都需要持续化写在磁盘中,保证宕机后重启任然正常。
- leader选举
leader(有任期字段term),candidate, follower.每个节点有在T到2T之间随机选择超时时间。leader和follower通过跳频联系。当一个follower收不到leader的跳频超时时将发起投自己的票。任何一个follower只能投一票,如果发现自己的日志比请求中携带的更新,则拒绝投票。当一轮投票结束有多个候选者时,这几个候选者重新分配随机的超时时间
。 - leader同步日志给follower
在上述数据共识中,当leader确认提交数据后,leader会一直不断地重试提交的rpc给follower、重试,直到请求成功;即使follower宕机了,重启后leader仍会接着发请求,直到请求成功,当leader宕机,如何向follower继续发;1.leader的日志只能增加,=》所以在选择时选term大,log长
的 2.leader会把自己的log复制到其他机器,如果新达到多数并且此任期内已有数据过半(挂前的一次数据不会被重复提交)就提交,只提交新任期的,同步follower还是要同步。 - log一致性
每个日志entry:iterm+index.每次发送AppendEntries时需要带上一次的,follower检查是否一样,一样才接受来保证所有机器log一致
leader重新选出,为了恢复log一致性,leader为集群中所有follower都保存一个状态变量,即nextIndex:1)nextIndex是leader准备向某个follower发送的下一个log entry的index;2)当leader刚刚即位后,nextIndex的初始值是(1+leader's last index);
当leader看到请求被拒绝时,其动作非常简单:只需将nextIndex-1,再次尝试。
它可以保证新 leader 在当选时就包含了之前所有任期号中已经提交的日志条目,不需要再传送这些日志条目给新 leader 。 这意味着日志条目的传送是单向的,只从 leader 到 follower,并且 leader 从不会覆盖本地日志中已经存在的条目。(第一次从更新logindex,大多数返回给主后主commit且apply,提交后发送从第二阶段commit和apply才更新状态机,每次任期Iterm日志中不变,https://zhuanlan.zhihu.com/p/27207160,快速提交新任期:Leader在当选后立即追加一条Noop并同步到多数节点,实现之前Term uncommitted的entry隐式commit) - 需要持久化term和投票
term需要存盘
任意一个server在一个term内只能投出一票;一旦已经投给了一个candidate,它必须拒绝其他candidate的投票请求;其实server根本不在意把票投给谁,它只会把票投给最先到请求到它的candidate;为了保证这一点,必须把投票信息持久保存到磁盘上,这样可以保证即使该server投完票后宕机,稍后又立即重启了,也不会在同一个term内给第二个candidate投票了。
ParallelRaft
应用于polarfs,是io可乱序的(非交叠部分)。因此当log要存储的数据没有交叠的时候可以乱序,否则不能
每个带了前面N个修改项的摘要。两个条件可乱序确认,一个是缺少的<N一个是缺少的N-n中没有与当前重叠的。选举节点有merge后面的日志(没有不空洞。merge没提交的再次提交),使选主正常。
follower每次接收到append log请求后,更新本地的last commit index,然后从last commit index开始按照log index递减查找空洞日志,直到last apply index,组成一个miss log index数组返回给leader。
跨term的log index一定要重新确认,除非这条日志明确为已经commit。在选举之前每个candi补齐日志,收到半数以上的补齐日志内容开始补齐,如果有日志没有存在大多数节点导致不能merge,则不nop(说明没有提交)。
paxos
如果ACCEPTER已经进入了第二阶段,让proposer妥协为响应里最大的值。
- basic paxos
第一阶段收集最新的N和V,第二阶段发起提议:
实际上这里的proposal是leader。共识算法正常是proposor,leader,accepter,leaner(先忽略),用来决议proposer的提议号和是否成功的。每次proposal先到leader(可随机选取,不重要),leader发给accepter若没有冲突返回any否则返回已选的,继续上述过程。
问题:多个Proposal可能出现死锁一直循环递增N的情况:上面这个是https://www.microsoft.com/en-...。为了方便理解,去除了实现细节。实时应用中,客户端不会自己处理冲突+1再次投票和发送给其他leaner,这些应该由另一个角色,在basic中,由一群c协调者,可以和acceptor一样,或者是其中的部分构成,每轮随机一个c作为leader,负责收集本轮结果和通知leaner。proposal->leader(每个client随机发就可以作为本轮leader)->pre->acceptors返回最大N的值V->带N请求->acceptors->leader->返回给proposal->client失败或者成功或再次投票->投票成功后发给leaner。此过程中CLIENT2再次发送是另一个leader。
- fast paxos
若proposal和acceptor,leader,leaner都是分布式,且要持久化,持久化+发送来回的代价就多了,
若leader发现没有冲突,不再参与
,proposal直接提交给acceptor(同一轮只投给先到的),直接发送给leaner,可以理解为基于乐观锁的思想,leaner和CLIENT都自行决议,
若proposal没有决策成功(先到的就是投票,没有半数以上的),1.重新引入leader,异步发送给协调者,协调者选择(因为acceptor只投一次),发给proposal结果(再次引入leader)。2.无leader,在acceptor决议后发送给所有acceptor,其他acceptor收到此消息后对i+1轮的可以比较投票(即使同时刻一个一半也可以再比较投一次,这两种的比较都复杂,要看各个提议的acceptor集合,这部分看论文吧)。
https://www.microsoft.com/en-... - muti-paxos
当leader稳定,可以省去prepare阶段。简单的说用一个序号来标识是否leader稳定(和raft,zk是一样的,转共识为全序序号的过程),若稳定更新序号直接发送给acceptor,acceptor需要记录序号,若发现有index>当前则返回false重新prepare。因为没有prepare,不知道每次最大的n,不知道leader是否稳定加入了全序。在prepare时并不需要此过程。
具体做法如下:
chubby就是一个典型的Muti-Paxos算法应用,在Master稳定运行的情况下,只需要使用同一个编号来依次执行每一个Instance的Promise->Accept阶段处理。
- disk paxos
disk-paxos也一样,先写入自己的号,读其他的,如果已经有了内容V(第二阶段了),就直接提交V。如果有号比自己大就直接放弃。
如果其他都未决,写入自己的V,否则读取其他的选最大标号对应的V,第二阶段
paxos一点不理论,具体实现伪代码
/*
* i/o required to acquire a free lease
* (1 disk in token, 512 byte sectors, default num_hosts of 2000)
*
* paxos_lease_acquire()
* paxos_lease_read() 1 read 1 MB (entire lease area)
* run_ballot()
* write_dblock() 1 write 512 bytes (1 dblock sector)
* read_iobuf() 1 read 1 MB (round up num_hosts + 2 sectors)
* write_dblock() 1 write 512 bytes (1 dblock sector)
* read_iobuf() 1 read 1 MB (round up num_hosts + 2 sectors)
* write_new_leader() 1 write 512 bytes (1 leader sector)
*
* 6 i/os = 3 1MB reads, 3 512 byte writes
*/
/*
* It's possible that we pick a bk_max from another host which has our own
* inp values in it, and we can end up commiting our own inp values, copied
* from another host's dblock:
*
* host2 leader free
* host2 phase1 mbal 14002
* host2 writes dblock[1] mbal 14002
* host2 reads no higher mbal
* host2 choose own inp 2,1
* host2 phase2 mbal 14002 bal 14002 inp 2,1
* host2 writes dblock[1] bal 14002 inp 2,1
* host1 leader free
* host1 phase1 mbal 20001
* host1 writes dblock[0] mbal 20001
* host1 reads no higher mbal
* host1 choose dblock[1] bal 14002 inp 2,1
* host1 phase2 mbal 20001 bal 20001 inp 2,1
* host1 writes dblock[0] bal 20001 inp 2,1
* host2 reads dblock[0] mbal 20001 > 14002
* abort2, retry
* host2 leader free
* host2 phase1 mbal 16002
* host2 writes dblock[1] mbal 16002
* host2 reads dblock[0] mbal 20001 > 16002
* abort1 retry
* host2 leader free
* host2 phase1 mbal 18002
* host2 writes dblock[1] mbal 18002
* host2 reads dblock[0] mbal 20001 > 18002
* abort1 retry
* host2 leader free
* host2 phase1 mbal 20002
* host2 writes dblock[1] mbal 20002
* host2 reads no higher mbal
* host2 choose dblock[0] bal 20001 inp 2,1
* host1 reads dblock[1] mbal 20002 > 20001
* abort2 retry
* host2 phase2 mbal 20002 bal 20002 inp 2,1
* host2 writes dblock[1] bal 20002 inp 2,1
* host2 reads no higher mbal
* host2 commit inp 2,1
* host2 success
* host1 leader owner 2,1
* host1 fail
*/
raft/paxos/zap/mutli-paxos区别
- raft要有一个leader。在
选主时每个follower只能投一次
,不成功随机时间下一次。有主时的共识由主来给日志编号,follower比较就好,follower保证稳定可替换即可。 - mutli-paxos在去掉pre后和raft相似,都是日志记录,区别是mp允许任何acceptor升级为leader,raft则很严格比如只有最完整日志的才行,mp在preprare后会知道当前最大的index,对于旧的异步补空洞。raft觉得补空洞过程太繁琐,增加了选主的复杂度。
- paxos leader不能那么重要(fast paxos在无冲突时甚至无leader参与),每次可以随机选,只是汇总投票。paxos在fast模式下,冲突处理时,每个acceptor可以更新选票重新投(其实是冲突的解决,也可以不算投票,根据集合等复杂的逻辑,在zk中就按照现有集合票数)。
- zap还是有leader的。
zap在无主的时候选举算法和fast paxos很像
,有最大xid(类似pre阶段,只不过是上次存好的),先选主,每次选主的提案直接给acceptor并且采用无协调者的冲突处理。在有主时,用paxos的思想先pre收集并同步信息保证一致,主处理写,多数处理成功后回复。 - paxos优势就是单主能不能抗住了,单主投票只能一次一个。
zookeeper
zk定位于分布式协调服务
官方:https://zookeeper.apache.org/...
下面介绍zk的常用功能,架构,共识过程。
架构
本身的数据组织以文件形式,每个叶节点znodes,非页节点只是命名空间的路径标识;但是存储于内存,记录磁盘日志,副本包含完整内存数据和日志,znodes维护节点的版本,zxid等所有信息。
Zookeeper对于每个节点QuorumPeer的设计相当的灵活,QuorumPeer主要包括四个组件:客户端请求接收器(ServerCnxnFactory)、数据引擎(ZKDatabase)、选举器(Election)、核心功能组件(Leader/Follower/Observer不同)
作用
1.单独zk集群元数据的可靠性和一致性保证,元数据保存在zk所有副本中(少量完全可以放在内存中数据)
路由,选择数据库,调度程序
2.单独zk集群,锁,防护令牌,获取锁或者zxid
3.变更通知,每个变更都会发送到所有节点
watch机制
4.用于检测,服务发现
session:
每个ZooKeeper客户端的配置中都包括集合体中服务器的列表。在启动时,客户端会尝试连接到列表中的一台服务器。如果连接失败,它会尝试连接另一台服务器,以此类推,直到成功与一台服务器建立连接或因为所有ZooKeeper服务器都不可用而失败。
只要一个会话空闲超过一定时间,都可以通过客户端发送ping请求(也称为心跳)保持会话不过期。ping请求由ZooKeeper的客户端库自动发送,因此在我们的代码中不需要考虑如何维护会话。这个时间长度的设置应当足够低,以便能档检测出服务器故障(由读超时体现),并且能够在会话超时的时间段内重新莲接到另外一台服务器。
zookeeper数据同步过程
采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。
zab protocol
Leader election leader选举过程,electionEpoch自增,在选举的时候lastProcessedZxid越大,越有可能成为leader Discovery: 第一:leader收集follower的lastProcessedZxid,这个主要用来通过和leader的lastProcessedZxid对比来确认follower需要同步的数据范围 第二:选举出一个新的peerEpoch,主要用于防止旧的leader来进行提交操作(旧leader向follower发送命令的时候,follower发现zxid所在的peerEpoch比现在的小,则直接拒绝,防止出现不一致性) Synchronization: follower中的事务日志和leader保持一致的过程,就是依据follower和leader之间的lastProcessedZxid进行,follower多的话则删除掉多余部分,follower少的话则补充,一旦对应不上则follower删除掉对不上的zxid及其之后的部分然后再从leader同步该部分之后的数据 Broadcast 正常处理客户端请求的过程。leader针对客户端的事务请求,然后提出一个议案,发给所有的follower,一旦过半的follower回复OK的话,leader就可以将该议案进行提交了,向所有follower发送提交该议案的请求,leader同时返回OK响应给客户端
实际上zookeeper中算法三阶段:FSE=>Recovery=>Broadcast(广播和上面的一致)
fast leader election
基于fast paxos。发送给所有的节点。没有随机leader参与收集。LOOKING:进入leader选举状态 FOLLOWING:leader选举结束,进入follower状态 LEADING:leader选举结束,进入leader状态 OBSERVING:处于观察者状态 1.serverA首先将electionEpoch自增,然后为自己投票 2 serverB接收到上述通知,然后进行投票PK 如果serverB收到的通知中的electionEpoch比自己的大,则serverB更新自己的electionEpoch为serverA的electionEpoch 如果该serverB收到的通知中的electionEpoch比自己的小,则serverB向serverA发送一个通知,将serverB自己的投票以及electionEpoch发送给serverA,serverA收到后就会更新自己的electionEpoch 在electionEpoch达成一致后,就开始进行投票之间的pk,优先比较proposedEpoch,然后优先比较proposedZxid,最后优先比较proposedLeader pk完毕后,如果本机器投票被pk掉,则更新投票信息为对方投票信息,同时重新发送该投票信息给所有的server。如果本机器投票没有被pk掉,如果是looking,过半更改状态,如果FOLLOWING/LEADING说明落后,加速收敛
- Recovery
略:https://my.oschina.net/pingpa... - follower读写过程图:
ectd
经常应用于配置共享和服务发现,相比于zk,简单。使用 Go 语言编写部署简单;使用 HTTP 作为接口使用简单;使用 `Raft 算法`保证强一致性让用户易于理解。无需安装客户端。提供接口K-V存储(storing up to a few GB of data with consistent ordering,提供线性读),watch,lease,lock,election。因为共识完全实现的raft所以只简单说下部署模式,节点组成,数据持久化等。
官方:https://coreos.com/etcd/docs/latest/
架构图:
单节点如下
store:为用户提供API
集群会区分proxy,leader,follower
- 启动
etcd 有三种集群化启动的配置方案,分别为静态配置启动、etcd 自身服务发现、通过 DNS 进行服务发现
自身服务发现:
首先就用自身单个的 url 构成一个集群,然后在启动的过程中根据参数进入discovery/discovery.go源码的JoinCluster函数。因为我们事先是知道启动时使用的 etcd 的 token 地址的,里面包含了集群大小 (size) 信息。在这个过程其实是个不断监测与等待的过程。启动的第一步就是在这个 etcd 的 token 目录下注册自身的信息,然后再监测 token 目录下所有节点的数量,如果数量没有达标,则循环等待。当数量达到要求时,才结束,进入正常的启动过程。 - 运行
proxy、leader\follower。proxy只负责转发,etcd proxy支持2种运行模式:readwrite和readonly,缺省的是readwrite,即proxy会将所有的读写请求都转发给etcd集群;readonly模式下,只转发读请求,写请求将会返回http 501错误。proxy保证参与投票数量有限的性能,所有follower都同步完数据才返回成功(因为异常不自动回来,可以全部,已经被管理员补齐了,否则只能读主,因此节点不能很多),在正常节点故障后,可以由管理员手动处理,一个备份的功能。
etcd 可以代理访问 leader 节点的请求,所以如果你可以访问任何一个 etcd 节点,那么你就可以无视网络的拓扑结构对整个集群进行读写操作,否则只能连接leader.
无故障自恢复(容易出错,考虑到已经高可用,管理员有时间自行恢复) - 数据持久:WAL+snapshot(删除WAL)
从 snapshot 中获得集群的配置信息,包括 token、其他节点的信息等等,然后载入 WAL 目录的内容,从小到大进行排序。根据 snapshot 中得到的 term 和 index,找到 WAL 紧接着 snapshot 下一条的记录,然后向后更新,直到所有 WAL 包的 entry 都已经遍历完毕,Entry 记录到 ents 变量中存储在内存里。此时 WAL 就进入 append(read和append互斥) 模式,为数据项添加进行准备。
当 WAL 文件中数据项内容过大达到设定值(默认为 10000)时,会进行 WAL 的切分,同时进行 snapshot 操作。这个过程可以在etcdserver/server.go的snapshot函数中看到。所以,实际上数据目录中有用的 snapshot 和 WAL 文件各只有一个,默认情况下 etcd 会各保留 5 个历史文件 - 数据KV
内存BTREE索引,物理B+树,存历史版本,没有引用后压缩删除历史版本。 - 应用:https://www.infoq.cn/article/...
chubby
GFS和Big Table等大型系统都用他来解决分布式协作、元数据存储和Master选择等一系列与分布式锁服务相关的问题
客户端发送到其他机器都会将master反馈,重新转到master,持续直到换。
数据组织方式和zk一样。
只有主节点提供读写(数据和日志有空洞),高可靠和可用,吞吐量不如zk。在换主阶段会阻塞。
etcd,chubby,zk的对比
因为zk,etcd都会补齐follower。因此主从都可以读。etcd的主是固定的,除非故障=》raft的换主。chubby(06年)用过的mutil-poxas主一般不变,不保证每轮主从数据一致,只有主有读写能力,吞吐量会差一些,一万台机器分布式锁还是可以的。
etcd(14年)是后来的,肯定更好啊,有http接口,一切都更轻量简单,缺点只是无故障自恢复吧,zk每次都会选主(但基于一个xid,基本也类似mutil-poxas会稳定),可自动恢复。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。