分布式系统的一致性问题
区块链作为一个分布式系统,首先碰到的问题就是一致性的保障。
在分布式系统中,一致性是指对于系统中的多个服务节点,给定一系列操作,在协议(往往通过某种共识算法)保障下,试图使得它们对处理结果达成某种程度的一致。
如果分布式系统能实现“一致”,对外就可以呈现是一个功能正常的,但性能和稳定性都要好很多的“虚处理节点”。
举个例子,某影视公司旗下有西单和中关村的两个电影院,都出售某电影票,票一共就一万张。那么,顾客到达某个电影院买票的时候,售票员该怎么决策是否该卖这张票,才能避免超售呢?当电影院个数更多的时候呢?
一致性并不代表结果正确与否,而是系统对外呈现的状态一致与否,例如,所有节点都达成失败状态也是一种一致。
挑战
在实际的计算机集群系统(看似强大的计算机系统,很多地方都比人类世界要脆弱的多)中,存在如下的问题:
- 节点之间的网络通讯是不可靠的,包括任意延迟和内容故障;
- 节点的处理可能是错误的,甚至节点自身随时可能宕机;
- 同步调用会让系统变得不具备可扩展性。
要解决这些挑战,愿意动脑筋的读者可能会很快想出一些不错的思路。
为了简化理解,仍然以两个电影院一起卖票的例子。可能有如下的解决思路:
每次要卖一张票前打电话给另外一家电影院,确认下当前票数并没超售;
两家电影院提前约好,奇数小时内一家可以卖票,偶数小时内另外一家可以卖;
成立一个第三方的存票机构,票都放到他那里,每次卖票找他询问;
更多……
这些思路大致都是可行的。实际上,这些方法背后的思想,将可能引发不一致的并行操作进行串行化,就是现在计算机系统里处理分布式一致性问题的基础思路和唯一秘诀。只是因为计算机系统比较傻,需要考虑得更全面一些;而人们又希望计算机系统能工作的更快更稳定,所以算法需要设计得再精巧一些。
强一致性
规范的说,理想的分布式系统一致性应该满足:
- 可终止性:一致的结果在有限时间内能完成;
- 共识性:不同节点最终完成决策的结果应该相同;
- 合法性:决策的结果必须是其它进程提出的提案。
第一点很容易理解,这是计算机系统可以被使用的前提。
第二点看似容易,但是隐藏了一些潜在信息。例如现在就剩一张票了,中关村和西单的电影院也分别刚确认过这张票的存在,然后两个电影院同时来了一个顾客要买票,从各自“观察”看来,自己的顾客都是第一个到的……怎么能达成结果的共识呢?记住我们的唯一秘诀:核心在于需要把两件事情进行排序,而且这个顺序还得是大家都认可的。
第三点看似绕口,但是其实比较容易理解,即达成的结果必须是节点执行操作的结果。仍以卖票为例,如果两个影院各自卖出去一千张,那么达成的结果就是还剩八千张,决不能认为票售光了。
带约束的一致性
做过分布式系统的读者应该能意识到,绝对理想的强一致性(Strong Consistency)代价很大。除非不发生任何故障,所有节点之间的通信无需任何时间,这个时候其实就等价于一台机器了。实际上,越强的一致性要求往往意味着越弱的性能。
很多时候,人们发现对一致性可以适当放宽一些要求,在一定约束下实现所谓最终一致性,即总会存在一个时刻,系统达到一致的状态。
从弱到强分别有如下几种:
- 顺序一致性(Sequential Consistency):Leslie Lamport 1978 年提出,是一种较弱的约束,保证所有进程自身执行的实际结果跟指定的指令顺序一致。例如,某进程先执行 A,后执行 B,则实际得到的结果就应该为 A, B,而不能是 B, A,所有其它进程也应该看到这个顺序,但不保证什么时候能看到。顺序一致性实际上只限制了各进程内指令的偏序关系,不在进程间进行排序。
- 线性一致性(Linearizability Consistency):Maurice P. Herlihy 与 Jeannette M. Wing 在 1990 年共同提出,在顺序一致性前提下加强了进程间的操作排序,形成唯一的全局顺序(系统等价于是顺序执行,所有进程看到的所有操作的序列顺序都一致),是很强的原子性保证。但是很难实现,基本上要么依赖于全局的时钟或锁(原子钟是个简单粗暴但有效的主意),要么性能比较差。
共识算法
实际上,要保障系统满足不同程度的一致性,往往需要通过共识算法来达成。
共识算法解决的是对某个提案,大家达成一致意见的过程。提案的含义在分布式系统中十分宽泛,如多个事件发生的顺序、某个键对应的值、谁是领导……等等,可以认为任何需要达成一致的信息都是一个提案。
问题挑战
实际上,如果分布式系统中各个节点都能保证以十分强大的性能(瞬间响应、高吞吐)无故障的运行,则实现共识过程并不复杂,简单通过多播过程投票即可。
很可惜的是,现实中这样“完美”的系统并不存在,如响应请求往往存在时延、网络会发生中断、节点会发生故障、甚至存在恶意节点故意要破坏系统。
一般地,把故障(不响应)的情况称为“非拜占庭错误”,恶意响应的情况称为“拜占庭错误”(对应节点为拜占庭节点)。
常见算法
针对非拜占庭错误的情况,一般包括 Paxos、Raft 及其变种。
对于要能容忍拜占庭错误的情况,一般包括 PBFT 系列、PoW 系列算法等。
从概率角度,PBFT 系列算法是确定的,一旦达成共识就不可逆转;而 PoW 系列算法则是不确定的,随着时间推移,被推翻的概率越来越小。
理论界限
搞学术的人都喜欢对问题先确定一个界限,那么,这个问题的最坏界限在哪里呢?很不幸,一般情况下,分布式系统的共识问题无解。
当节点之间的通信网络自身不可靠情况下,很显然,无法确保实现共识。但好在,一个设计得当的网络可以在大概率上实现可靠的通信。
然而,即便在网络通信可靠情况下,一个可扩展的分布式系统的共识问题的下限是无解。
这个结论,被称为FLP 不可能性
原理,可以看做分布式领域的“测不准原理”。
FLP 不可能性原理
FLP 不可能原理:在网络可靠,存在节点失效(即便只有一个)的最小化异步模型系统中,不存在一个可以解决一致性问题的确定性算法。
提出该定理的论文是由 Fischer, Lynch 和 Patterson 三位作者于 1985 年发表,该论文后来获得了 Dijkstra(就是发明最短路径算法的那位)奖。
FLP 不可能原理实际上告诉人们,不要浪费时间去为异步分布式系统设计在任意场景下都能实现共识的算法。
理解这一原理的一个不严谨的例子是:
三个人在不同房间,进行投票(投票结果是 0 或者 1)。三个人彼此可以通过电话进行沟通,但经常会有人时不时地睡着。比如某个时候,A 投票 0,B 投票 1,C 收到了两人的投票,然后 C 睡着了。A 和 B 则永远无法在有限时间内获知最终的结果。如果可以重新投票,则类似情形每次在取得结果前发生:(
FLP 原理实际上说明对于允许节点失效情况下,纯粹异步系统无法确保一致性在有限时间内完成。
这岂不是意味着研究一致性问题压根没有意义吗?
先别这么悲观,学术界做研究,考虑的是数学和物理意义上最极端的情形,很多时候现实生活要美好的多(感谢这个世界如此鲁棒!)。例如,上面例子中描述的最坏情形,总会发生的概率并没有那么大。工程实现上多试几次,很大可能就成功了。
科学告诉你什么是不可能的;工程则告诉你,付出一些代价,我可以把它变成可能。
这就是工程的魅力。
那么,退一步讲,在付出一些代价的情况下,我们能做到多少?
回答这一问题的是另一个很出名的原理:CAP 原理。
科学上告诉你去赌场赌博从概率上总会是输钱的;工程则告诉你,如果你愿意接受最终输钱的结果,中间说不定偶尔能小赢几笔呢!?
CAP 原理
CAP 原理最早由 Eric Brewer 在 2000 年,ACM 组织的一个研讨会上提出猜想,后来 Lynch 等人进行了证明。
该原理被认为是分布式系统领域的重要原理。
定义
分布式计算系统不可能同时确保一致性(Consistency)、可用性(Availablity)和分区容忍性(Partition),设计中往往需要弱化对某个特性的保证。
- 一致性(Consistency):任何操作应该都是原子的,发生在后面的事件能看到前面事件发生导致的结果,注意这里指的是强一致性;
- 可用性(Availablity):在有限时间内,任何非失败节点都能应答请求;
- 分区容忍性(Partition):网络可能发生分区,即节点之间的通信不可保障。
比较直观地理解,当网络可能出现分区时候,系统是无法同时保证一致性和可用性的。要么,节点收到请求后因为没有得到其他人的确认就不应答,要么节点只能应答非一致的结果。
应用场景
既然 CAP 不可同时满足,则设计系统时候必然要弱化对某个特性的支持。
弱化一致性
对结果一致性不敏感的应用,可以允许在新版本上线后过一段时间才更新成功,期间不保证一致性。
例如网站静态页面内容、实时性较弱的查询类数据库等,CouchDB、Cassandra 等为此设计。
弱化可用性
对结果一致性很敏感的应用,例如银行取款机,当系统故障时候会拒绝服务。MongoDB、Redis 等为此设计。
Paxos、Raft 等算法,主要处理这种情况。
弱化分区容忍性
现实中,网络分区出现概率减小,但较难避免。某些关系型数据库、ZooKeeper 即为此设计。
Paxos算法
此算法的详细描述参照此文
PBFT算法
此算法的详细描述参照此文
总结
拜占庭问题之所以难解,在于任何时候系统中都可能存在多个提案(因为提案成本很低),并且要完成最终的一致性确认过程十分困难,容易受干扰。但是一旦确认,即为最终确认。
比特币的区块链网络在设计时提出了创新的 PoW 算法思路。一个是限制一段时间内整个网络中出现提案的个数(增加提案成本),另外一个是放宽对最终一致性确认的需求,约定好大家都确认并沿着已知最长的链进行拓宽。系统的最终确认是概率意义上的存在。这样,即便有人试图恶意破坏,也会付出很大的经济代价(付出超过系统一半的算力)。