首先标题有点哗众取宠之嫌,可是暂时想不到更加合适的标题,就姑且这么使用吧。分布式共识算法一直是一个热门的研究话题,之因此要分布式共识,无外乎就是单点服务容易宕机,异常,出错,从而致使系统不可用,因而就有了备份容错的机制,那么一份数据多地(location)存储,若是不发生修改操做那就无需一致性协议的引入,可是这仅仅是理想状况,真实的应用中绝大多数都是须要执行更新操做的,这才有了分布式共识的需求。目前最为认同的共识算法就是lamport大神在98年发表的论文中说起的Paxos协议(然而因为太难以理解又在01年发表了paxos made simple),即便过了这么多年,Paxos依然难以理解和难以实现,工程实现大多都是精简版,最为出名的也有Raft,以及Zookeeper中的Zab。笔者以前读过paxos made simple,虽然能理解,可是总以为有点只知其一;不知其二(也写了一篇小博客理解paxos协议-分布式共识算法(consensus))。最近恰好看了一篇新的论文,结合Paxos来从新梳理下什么是分布式共识算法,怎么实现分布式共识算法。linux
Just Say NO to Paxos Overhead:Replacing Consensus with Network Ordering 这篇论文是2016年osdi上的一篇论文,第一做者是华盛顿大学计算机系统实验的一个PHD。标题看上去也很是的夺人眼球,拜读完以后,对于做者的想法还持有一点疑惑,可是这篇文章很好的阐述了怎么实现分布式共识算法,对于理解Paxos有难度的同窗不妨先去阅读下这篇论文,能更好的去理解,下面笔者就用纯大白话的形式来一步步说明分布式共识算法。git
lamport大神在其论文中也说起到,所谓的分布式共识要达成的目标:github
若是直接从上面的话来理解,那么就会陷入一个误区,或者说,看不明一致性的完整过程,换一个角度,咱们如何来保证多个replica一致性呢,很简单,目前最为流行的机制就是State Machine Replication(aka SMR)。SMR是这么定义的,(1).初始state要相同,(2).对于同一个state,给定相同的输入参数,执行相同的操做,输出结果是相同的。因此用SMR来保证一致性的要求就是,相同的状态,输入相同的参数,执行相同的操做。相同的状态属于上一步的结果,因此约束其实就是两个,相同的参数(argument),相同的操做(operation),说到底,paxos那些复杂的过程就是为了保证这两个约束条件。算法
相同的参数:看起来简单的约束,实际实现并不是易事,首先在异步网络的状况,消息乱序状况就严重干扰了这一约束,你想一想看,节点1先执行request n后执行m,节点2先执行m后执行n,结果能同样吗?那么paxos是如何保证这个有序性的呢?Paxos在其运行过程,一旦提案p被大多数的acceptors接受,那么后续提出的更高编号的提案都应该包含这个p,看明白了吗,就是一个提案未被确认执行前,全部的acceptors都不容许新的提案(这里的新的提案指的是value不一样的提案)发生,这就间接解决了乱序的问题,paxos保证全部的节点在每一次paxos提案期间只能执行一个提案(同一个value),从而来保证参数相同,消息有序。编程
相同的操做:客户端发起状态修改必然会带着一个operation的请求(实际工程中实现是经过调用不一样的接口如insert,update,delete等),那么当这个请求广播给全部的节点,那么执行天然是相同的操做。问题就在于异步网络没法保证可靠性,假如部分节点网络失效,有些没收到request,天然不会去执行。那么一部分节点执行,一部分节点不执行才是致使操做不一样的缘由(commit和do-nothing)。体如今paxos就是一旦通过大部分的acceptor赞成的提案到被learner学习的过程。工程上常见的实现方法是用leader来管理复制日志来实现操做相同的。segmentfault
有了上述的认知,再来看一致性,是否是就会以为明朗许多,本文标题中的另外一个名字是NOPaxos,天然重点就是讲解这篇论文了,下面就来看看论文的做者是如何实现分布式共识算法。网络
声明:这里不是纯翻译论文,若是想了解所有的过程,能够点阅前面的连接查看。 传统的一致性算法如paxos是把上述两个约束条件放在应用层去实现,这样的好处是不依赖于特定的网络结构,可是同时也有一些弊端,首先是一致性的时间比较长,性能较低,第二是实现难度比较大且复杂。做者另辟蹊径,若是网络层能保证消息有序,那么paxos前面整个投票过程就无需存在了,这样就把一致性的责任分摊在了网络层(并不是指TCP/IP协议栈中的网络层)和应用层。论文主要作了四方面的工做:session
按照论文自己的说法,最简单的实现,就是给每一个消息增长一个单调递增1的序列号(sequence number),这样,节点在接受到消息的时候,就能知道每一个消息的前后顺序了。除此以外,这一层无需再提供任何的保证,这样使得设计和实现都比较简单。简单的状况下,OUM为每一个request都添加一个sequence number,应用层经过libOUM调用接收到这个request以后,判断是不是当前想要接受的信息。只要sequence number是递增1,就能判断出是否乱序或者正常状况。若是出现了跳跃,好比当前须要的消息序列号是n,而后却接受到了一个序列号为m(m > n)的消息,那么上层应用就知道n-m中间的消息是丢失,进而执行其余操做,这样保证每一个replica接收到的消息是相同的顺序。下图是NOPaxos的一个总体架构:架构
每一个客户端都须要集成两个库,一个是用于处理网络消息的libOUM,一个是用于协调多个replica之间操做的NOPaxos。底层网络有一个sequencer,负责为每一个消息加上一个序列号,为了防止sequencer故障或者失效,须要一个controller来进行监控。总的来讲,这个架构总共有三种角色,controller,sequencer,client,其中client有两种协议OUM和NOPaxos。下面就一个个的来介绍这些角色分别承担的功能和用途。app
1. sequencer
正如前面所说,sequencer的功能很简单,就是为每一个消息添加一个序列号,这个序列号必须是单调递增1的。论文中,做者提供了三种不一样的实现方式,分别是,基于可编程的交换机内实现,基于middlebox原型的硬件实现,纯软件实现。在介绍这三种实现以前,先来考虑这样一个网络结构,
上图是一个三层胖树的结构3-level fat-tree,根据设计,全部的客户端发出来的信息都要通过sequencer,若是本来客户端与replica在同一个局域网内,这样的设计首先会致使消息路径增加,由于消息首先要走到sequencer,再从sequencer转发回来,明显消息走过的路径就变长了。因此这里对于sequencer最合理的位置,就是放在root switch(至于为啥是放在这个位置会使得性能最好,能够参考网络拓扑fat-tree的设计思路,笔者对于网络拓扑没有深刻研究,这里就不展开)。且论文做者在真实的环境下测试获得,对于这样一个三层胖树结构的网络,88%的状况下,添加一个序列号并不会增长额外的延迟开销,99%的状况下,只有5us的延迟。所以,增长这样一个sequencer并不会带来性能的降低(论文解释的缘由是,无论存不存在这个sequencer,大部分的package都是须要走到root-switch才能达到大多数的group)。sequencer的位置肯定了,那么接下来就是实现了:
2.controller
有了sequencer天然能实现消息有序性,可是同时也引进了一个问题,sequencer是一个单节点,一会儿就使得整个系统脆弱了不少,虽说发生故障的几率不高,可是一旦发生故障,整个系统不可用不说,还可能出错。这个时候就须要controller出场了,controller的主要目的是监视sequencer,一旦发现sequencer不可用或者不可达,则会选择一个替换的sequencer,并更新其路由表。这一过程引入一个新的概念,session。每当一个sequencer失效了,须要挑选新的sequencer的时候,首先从新选定一个session number,并将信息更新到新的sequencer上。这样用一个session的概念就能来维护跨sequencer的消息有序了(会在消息头上加上一个二元组 [session-number, sequencer-number] )。一旦libOUM接收到了一个更高编号的session number,则说明,旧的session已经失效了,可是此时,libOUM并不知道是否有丢失旧的session中的package,因此不能返回一个drop-notification,只能返回一个session-terminated,由上层应用去决定改如何处理。至于上层如何处理,后面会讲。这里session number能够采用本地时间戳,或者将session number持久化到磁盘而后递增。此外controller的可靠性能够由多个节点来保证,controller选举sequencer的算法甚至能够直接使用paxos或者raft等,毕竟节点失效并非一个常常发生的事。
NOPaxos从架构图中可知,属于一致性协议最上层的协议了,经过调用底层的libOUM保证消息有序,剩下的如何保证操做一致就是在这个协议中实现的。下面会分别介绍不一样的状况下,NOPaxos是如何执行的,首先讲明一些概念和变量:
系统运行只有,协议的运行过程,只会出现四种不一样的状况,正常的操做,出现消息丢失,发生视图转换,系统状态同步。下面就分别讲解这四种状况下接收到不一样消息时该如何处理,另外关于leader选举能够参考viewstamped-replication。
1. Normal Operation
正常的状况下,客户端广播(broadcast)一个request的消息(消息内容为,[request, op, request-id],其中request-id是用于response的时候,判断是哪一个消息,理解为消息的unique key)当replica接受到这样一个消息的时候,首先OUM带过来的一个session-msg-num判断是否是本身正在等待的消息,若是是,则递增session-msg-num并将op写入日志(注意,这里并不执行)。若是replica是leader的话,那么就执行这个操做,并写入日志。而后每一个replica会回复客户端一个消息(内容为,[reply, view-id, log-slot-num, request-id, result],这里的result只有leader才有回复,其余为null)。客户端会等待f+1个reply,能match上view-id和log-slot-nums的回复,其中必须有一个是leader,若是没有接收到足够的回复,则会超时甚至重试。上述是正常的状况下,正常的处理过程。【问题:若是leader提交了,然而client并无收到f+1个reply,这个时候怎么办,没有任何机制能反驳leader?raft的机制就是确认replica已经写入了日志才commit的,因此他这里没写明我也不明白是为啥】
2. Gap Agrement
假如此时libOUM原本在等待session-msg-num编号的请求,却来了一个更大的请求,说明,中间发生丢包了。那么此时,libOUM就会向上层返回一个drop-notification,告知session-msg-num丢失了(同一个session内)而且递增session-msg-num。若是是非leader接收到drop-notification,那么能够向相邻节点copy请求,或者不作任何事。若是leader节点接收到了这样一个返回值,则会在日志中追加一个NO-OP,而且执行下面的操做:
对于drop操做,客户端是不须要显式通知到的,由于能够等待客户端超时。固然这里在实际开发的时候能够进行一些优化,好比leader没有接收到的时候,也能够向其余的节点进行copy,减小NO-OP的数目。
3. view change
前文也说到了,NOPaxos这一层有一个leader的概念,且OUM有一个session的概念,若是这两个一旦有一个发生改变,就须要进行view change操做了。view change协议能保证新老视图中间的状态一致性,且能很好的从老的视图切换到新的视图。NOPaxos中的视图变换协议相似于Viewstamped Replication[42]。算法阐述以下:当一个replica怀疑当前的leader挂了,或者接收到了一个session-terminated,亦或者接收到一个view-change/view-change-req的消息。此时他就适当的增长leader-num或者session-num,而且将状态status设置为viewchange。一旦session-num发生改变,session-msg-num则重置为0。而后广播一个消息[view-change-req, view-id]给其余的replica,而且发送一个[view-change, view-id, v`, session-msg
-num, log]给新的leader,v`表示上一个状态status为normal的视图的view-id。当一个replica处于viewchange状态的时候,会忽略其余消息,除了start-view, view-change,view-change-req。若是超时,则从新广播和发送指定消息给新leader。当新leader接收到f+1个view-change的消息的时候,则会执行下列操做:从最近最新的view且status为normal的replica合并日志(每一个replica都会发送一个log信息给新的leader)。合并规则是,若是你们都是no-op,那就是no-op,若是有一个是request,那就是request。leader拿到新的view-id以后设置session-msg-num比合并日志大的数字,而后广播一个消息[start-view, view-id, session-msg-num, log]给全部的replica.当其余的replica收到了这个start-view的消息以后,会更新本身监听的信息,包括view-id,session-msg-num等,并要先同步下日志,若是发生了日志更新,则会发送reply信息给客户端。最后把本身的status设置为normal。
4. Synchronization
定时同步,这个属于优化范畴,前文也说到,在正常的状况下,leader进行commit操做,replica只进行日志append,可是随着系统的运行,会致使log愈来愈大,若是leader发生变动,那么就会有一个问题,新leader恢复时间很是长。为了在leader变动的时候,恢复时间缩短,NOPaxos决定周期性的进行数据同步。这一步骤的目的就是确保在sync-point前的全部数据,log状态都是一致的。小论文并无详细的指出如何解决这一问题,放在了大论文中去讲了。
首先定义正确性是:一个request被执行或者NO-OP这样的日志被写进f+1个节点中,且若是客户端确保一个request执行成功的标记是收到f+1个回复,而且咱们说一个view v的log是stable的,代表这个会成为全部高于view v的视图的前缀日志。
(1).stable log中的成功操做,在resulting log中也是一样正确的。
(2).replica老是开始于一个view中一个正确的session-msg-num
值得注意的是,一个操做(request或者no-op一旦被commit到日志中,将会永远呆着),由于若是在view change期间,同时发生了操做commit,意味着f+1个节点赞成了view change,而f+1个节点提交了日志,也就说明了,至少有一个节点同时执行了这两件事。并且新leader会跟其余的节点同步日志,因此若是是大多数的节点认可的日志会同步到leader中去。
后记:阅读完以后,总以为有点问题,github上有这个代码的开源实现,NOPaxos的源码,笔者还没来得及去看,后续若是看了会继续开更,但愿更多喜欢分布式共识和分布式一致性的朋友能一块儿谈论这个话题。