今年年初参加一家公司的面试,发生了一件有趣的事情。当我给面试官解释Ceph的运行原理时,提到了Ceph支持POSIX标准(Portable Operating System Interface),面试官突然问道:“那简述以下POSIX协议如何工作的,如何对数据一致性提供保证”。额~~我一时语塞,POSIX标准工作在OS层,只有IEEE官方标准文档中有关于它工作原理的详细论述。
我只有明确回答,我不清楚。还好面试官没有为难我,最终还是顺利拿到了这家公司的offer。不过这个坑不填上,我还是有点寝食不安啊。最纳闷的是,为什么会问和数据一致性的关系呢?过了几天我脑袋终于转过弯了,他当时问的不是POSIX,而是Paxos!!天啊,只有怪自己英语不好咯~~
经过一年多的整理二月份已经正式给出版社交稿了,后面还要经过多次审校,就可以出成果咯。笔者继续恢复系统存储专题的写作,这几篇从单机存储到分布式存储的过度文章就从这个Paxos算法说起吧,这个保证数据一致性的算法在当今的分布式系统中被大量实现和使用,可见它的重要性。
1. CAP与数据一致性
1-1. CAP概要
分布式系统的假设是,工作在网络环境下的系统拥有多个节点,而这些节点本身会由于各种原因而变得不稳定。这其中就有一个非常重要的概念——CAP原理。这个原理指导着大多数分布式系统的设计过程,CAP原理大致是说分布式系统中一定存在三个特性:一致性(Consistency)、分区容忍性(Partition)和可用性(Availability),且这三个特性在分布式系统的设计中不可能全部同时满足。
(上图摘自网络)
举个例子,一个分布式系统中有N个节点通过网络链接在一起协同工作。首先你不能将完整的数据X只存放在一个节点上,这是因为一旦这个节点由于各种原因停止工作了,数据X就不能被访问那肯定就不满足系统可用性了,并且一旦这个节点不能再被恢复,数据X就永远丢失了。所以数据X至少也应该在不同的节点上存储多份,存储的副本量越多越能保证数据X的安全,也更能保证即使在多个节点同时不可用的情况下,数据X也同样能够被访问。这就是分区性的要求,按照普遍经验,数据X的副本数至少应该有三份。在这种情况下,当数据X发生变化时如何对这些副本进行更新呢?最理想的效果是,当如果客户端要求发出数据X的更新请求后,从任何一个节点访问数据X都可以拿到它最新的状态,这就是一致性要求。当然这个最理想的效果太理论化了,要知道基于网络工作的分布式系统受很多外在因素影响:要是同步过程中发现某个副本节点无法连接了怎么办?要是同时又有一个客户要求再次更新数据X怎么办?如果真要达到这么理论的一致性要求,那就只能让所有需要读/写数据X的客户端等待,直到完成数据X的所有副本同步后,再进行响应,但显然这从可用性的角度出发又是不满足要求的。
再例如,关系型数据库的设计通常基于ACID原理,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),而且关系型数据库普遍采用事务技术实现ACID原理,其中每一个事务就是最小化的原子操作,还可以设置不同的事务级别,包括可读未提交、可重复读这样的事务级别;关系型数据库还规定了一旦事务正确提交就不能进行数据回滚,如果要继续修改数据就只能启动一个新的事务。 而这一切都是为了保证数据库系统是一个强一致性系统 。就连架设在各个关系型数据库实例之上的分布式事务机制,也是为了保证这个目标:只要有任何一个参与分布式事务过程的数据库实例出现异常,整个分布式事务就无法正常提交,当然也就无法完成数据写操作。当然老数据是可以进行读操作的,即使某些节点出现了问题其他数据库节点也可以承担这个读操作,因为这样的操作不存在一致性改变的风险。
任何分布式系统都不可能以CAP原理中三个特性同时作为主要设计目标 ,要到达非常高的强一致性和较高的分区容忍性,就必须以牺牲可用性作为代价(注意是牺牲而不是完全放弃)。而分区容忍性又是分布式系统成立的基础,没有任何分区容忍性的分布式系统甚至都不能称为分布式系统;高压力的环境下又不能过度牺牲分布式系统的可用性,要知道99.99%的可用性和99.999%的可用性完全就是两个档次的分布式系统。
1-2. 数据一致性
所以类似分布式事务机制那样过度强调数据一致性的设计思路就不太受主流分布式系统设计思想欢迎,至少从目前各种分布式系统公布的设计原理来看是这样的。例如HDFS这样的分布式文件系统,首先强调的是高可用性和一定的分区容忍性其次才是数据一致性,数据一致性通过副本保证。但并不是所有副本都完成写入动作后HDFS才认为数据一致,而是只要一部分数据副本完成了写入动作,HDFS就认为数据成功写入且客户端可以调用新数据,而没有完成同步的副本将会接着进行数据同步,达到数据的最终一致性。DNS也是一种需要首先考虑高可用性和分区容忍性的分布式系统,由于DNS服务跨不同网络,所以当一个DNS配置更改后新的域名解析请求要等全球所有DNS节点都更改生效了才继续工作,但DNS的组织结构能够保证多个DNS服务节点解析www.XXXX.com这个域名的结果最终是一致的。这种牺牲系统一致性保证系统可靠性和分区容忍性的设计思路,在分布式系统领域有一个特定的称呼:BASE。基本可用(Basically Availble)、软状态(Soft-state)和最终一致(Eventual Consistency)。
在上一小节简单说明CAP原理和示例的内容中,提到两个关于一致性的概念:强一致性和最终一致性。强一致性可以概括为 任何时刻客户在分布式系统中获取数据X,无论它在分布式系统的哪一个节点进行这个操作,其获取到的数据X都是一致的 。从这个定义来看,分布式事务机制就是一种强一致性的实现。有强一致性就有对应的弱一致性定义,弱一致性不是说不保持数据的一致性,而是说不保证数据每时每刻都一致,也不承诺什么时候才能保证分布式系统的任何节点都能读取到一致的数据。而 最终一致性是弱一致性的一种特定结果 ,既是承诺基于弱一致性,在经过一个数据不一致的时间窗口后,最终能保证数据一致。这个数据不一致的时间窗口在客户端看来非常短,而且分布式系统还可以通过多种方式向客户端屏蔽不一致的数据,例如主从副本方式。
2. 最终一致性的经典实现:Paxos算法
Paxos算法又被称为两阶段算法,请注意不是实现分布式事务的两阶段提交协议。Paxos算法的工作场景是基于CAP原理构建的分布式系统,是在这样的环境中如何高效率的达到数据的最终一致性;而两阶段/三阶段提交协议的工作场景是基于AICD原则的数据强一致性系统。关于这两种场景的设计思路已经在上文中详细介绍过,这里就不再赘述了。
Paxos算法中有四个角色Client(议题产生者)、Proposer(正式提议者)、Acceptor(投票决策者)和Learner(最终结果学习者),这四个角色中Client和Learner角色是两个广义的概念,而真正完成算法的是Proposer角色和Acceptor角色,所以很多技术文章中都是重点讲解后两个角色的工作过程。Proposer角色是正真的提议发起者,负责发起提议投票、总结提议投票结果以及变更后发起新的投票;Acceptor角色是决策者,对于某个提案发表自己的投票结果,并交由Proposer角色进行处理,各个Acceptor角色都有自己的都票结果,不受其它Acceptor角色的影响。
总的来说Paxos算法在处理这样一个场景:当分布式系统中的多个Proposer对于某件事的结果不能达成一致时,就发起一个提案,由多个Acceptor负责进行投票并让多个Proposer对于这件事的结果达成最终一致。“这件事”可以指代很多事情,例如“选主”,再例如K所代表的变量被同时赋予V1和V2两个值甚至更多的值,等等。这就是上文提到的最终一致性的一个具体实现算法——每个Proposer对某件事情的判断过程可能在某段时间内是不一致的,但最终在客户端Client上呈现的结果将是一致的。
需要说明的是,Paxos算法最早由莱斯利·兰伯特(Leslie Lamport)提出,在他的论文中《The Part-Time Parliament》本来就介绍了Paxos算法的多种变体,加上在实际应用中各技术人员又加入了自己的理解,所以才出现了Paxos算法许多不一样的实现版本,但实际上这些算法版本中各角色的工作都有其处理原则,只要掌握了这些处理原则,理解Paxos就不难了。
2-1. Basic-Paxos算法
虽然说Paxos算法和两阶段提交协议的设计思路完全不一样,但前者借鉴了后者的一些思想——即分为准备阶段(Prapare)和赋值阶段(Accept) 。为了顺利的完成数据最终一致性这个工作目标,在整个工作过程中Proposer角色和Acceptor角色要分别保持以下的的工作原则:
- Proposer和Acceptor一个负责发起提议,一个负责响应提议。后者要尽量回复前者的每一个请求,无论这次请求根据Acceptor的处理原则是成功了还是失败了。如果Proposer在规定的时间内没有获得Acceptor的响应,则往最坏的情况来进行考虑。
- 针对一个提议X的多轮投票,各个Proposer需要保证自己发起的后一轮投票轮次编号(vote)一定大于前一轮投票轮次编号。而各个Proposer不必保证自己的投票轮次是全局最大的,当然为了减少在第一阶段的授权冲突,也可以保证新的投票轮次全局最大(这个原因分析在后文中会详细讨论)。
- 什么情况下针对一个议题X,Proposer不再发起新一轮投票并认为得到了最终一致的数据呢? 当Proposer收集了各个Acceptor的最终投票值,并发现其中至少N/2 + 1个Acceptor的最终值都是V,则认为议题X的最终结果为V 。实际上这个工作规则属于Proposer角色,但很多时候Proposer、Proposer、Acceptor都是一个应用程序。
- 在Prapare阶段,Acceptor需要保证不接受投票轮次编号(vote)小于等于当前PrepareVote的投票轮次的授权(发起)申请。
- 在Accept阶段,Acceptor需要保证不接受投票轮次编号(vote)小于当前PrepareVote的投票轮次的赋值申请。以上两条工作原则是最重要,为什么Pasox算法会有若干种变体呢?其原因就是在算法应用实现阶段,具体的算法实现在保证这两个基本工作原则下,为了提高工作效率而对实现过程进行微调。
==========================
(接下文)