<1>pbft五阶段请求解释
Request pre-prepare prepare commit 执行并reply
(1)pre-prepare阶段:
主节点收到客户端请求,给请求编号,并发送pre-pre类型信息给其他从节点。
从1节点收到pre-pre类型信息,如果同意这个请求的编号,如果同意就进入prepare阶段
(2)Prepare阶段:
从1节点同意主节点请求的编号,将发送prepare类型消息给主节点和其他两个从节点。如果不发,表示不同意。
图:从1节点发prepare信息给其他节点
从1节点如果收到另外两个从节点都发出的同意主节点分配的编号的prepare类型的消息,则表示从1节点的状态为prepared,该节点会拥有一个prepared认证证书。
为了防止viewchange导致主节点给请求分配的编号失效,引入commit阶段。
(3)commit阶段
从1节点进入prepared状态后,将发送一条COMMIT类型信息给其它所有节点告诉他们它有一个prepared认证证书了。
图:从1节点发commit类型信息给其他节点
如果从1节点收到2f+1条commit信息,证明从1节点已经进入commited状态。
只通过这一个节点,我们就能认为客户端的请求在需要的节点中都到达了prepared状态,每一个需要的节点都同意了主节点分配的编号。当一个请求在某个节点中到达commited状态后,该请求就会被该节点执行。
问:为什么要有commit阶段?
答:commit阶段用来确保其他节点都已经收到足够多的信息来达成共识了。Commit阶段并没有像prepare阶段发送同意不同意请求,只检测有没有收到足够多的commit类型信息。
相当于是正常情况下,4个节点中,如果每个节点都同意了请求,并且收到另外三个节点的同意信息。
所以如果从1节点正常情况下达到了commited阶段,确保了实际收集的信息相当于是:
节点 收到同意编号数
主节点 4
从1 4
从2 4
从3 4
这时从1节点可以放心执行客户端的请求,写入新区块了,因为其他节点都已经收到足够多的信息来达成共识了。
如果从1节点达到了prepared阶段,实际每个节点收集到的信息可能是:
节点 收到同意编号数
主节点 2
从1 4
从2 1
从3 1
这时其他节点并不能确保已经收到足够多的同意信息,从数据验证的角度是不应该写入新区块的。这才是问题的关键。这时如果发生view change,会丢弃prepare阶段的请求。
问:如果在commit阶段view change,会导致达成不了共识吗?会导致之前的view下的请求编号丢失吗?
答:如果commit阶段viewchange,会保留之前commit阶段的请求,不会达成不了共识,也不会丢失请求编号。
解释:prepare阶段和commit阶段用来确保那些已经达到commit状态的请求即使在发生viewchange后在新的view里依然保持原有的序列不变,比如一开始在view 0中,共有req 0, req 1, req2三个请求依次进入了commit阶段,假设没有坏节点,那么这四个replicas即将要依次执行者三条请求并返回给Client。但这时主节点问题导致view change的发生,view 0 变成 view 1,在新的view里,原本的req 0, req1, req2三条请求的序列被保留,作数。那些处于pre-prepare和prepare阶段的请求在view change发生后,在新的view里都将被遗弃,不作数。
这个意思其实是:
如果每个节点都进入了commit阶段(这里要强调的是每个节点都进入这个commit阶段才算是整体进入了commit阶段),这时即使view change,也会保留之前的view里进入commit阶段的请求信息,view change会继续之前的commit阶段请求,不会再重新进入pre-prepare和prepare阶段。
问:为什么fabric0.6和1.0都要共识交易的顺序?0.6是pbft实现,1.0是order负责排序。每笔交易的顺序为什么需要共识?不能按照时间顺序存在一个队列里顺序执行就可以了吗?
答:考虑两种情况,有可能会导致每个节点收到的顺序不一样。
第一类:节点收到的顺序和单个客户端发起的顺序不一样
假如客户端发起请求的正确顺序:123
4个节点收到的顺序有可能是:
情况1
主节点:123
从节点1:123
从节点2:132
从节点3:123
情况2
主节点:132
从节点1:123
从节点2:123
从节点3:123
第二类:多个客户端同时发起请求,每个节点收到的顺序不一样的可能性增大
这样共识出来的结果,有可能未必是每个客户端的正确顺序。比如a发起请求1,b发起请求2. 1和2是有关系的。但是a到集群的时间很长,b到集群的时间很慢,导致集群收到的请求都是21,这样共识出来的结果是21,而不是实际应该的12,遇到这种情况,要有机制能判断是错误的,丢弃这样的请求,并返回错误信息。这种情况可以举个例子:
如果初始状态为a:10,b:0,c:0,a转b10元为请求1,b转c10元为请求2,正确请求顺序为12,如果集群达成共识的请求的为21,这样就会和正确请求顺序产生差异。丢弃2的请求。因为b这时并没有10元可以转给c。如果初始状态为a:10,b:10,c:0,集群达成共识的请求为21也可以正常执行。因为b最初已经有10元,可以转给c。请求之间有强依赖的另一例子就是特定物权的转移,因为那件物品只有一个,任意一方没有拥有这件物品时,是无法转给对方的。
而利用pbft算法,情况1和2都能共识出正确顺序结果应该为123。主节点为错误请求时,会导致view change变成情况1的情况,情况1能共识出123为大多数节点认可的结果。
扩展:
为什么不能直接用每个客户端的时间作为请求的排序,因为每个客户端的时间可以任意更改的,如果大家都更改到不一样的时间,这样的请求的顺序是错误的。如果去统一的时间戳服务去取时间,又会多了一个中心化节点。所以google spanner分布式数据库就在全球统一的时间戳服务这个点去入手解决一致性请求顺序的问题。
注:Google Spanner是个可扩展,多版本,全球分布式还支持同步复制的数据库。他是Google的第一个可以全球扩展并且支持外部一致的事务。Spanner能做到这些,离不开一个用GPS和原子钟实现的时间API。这个API能将数据中心之间的时间同步精确到10ms以内。因此有几个给力的功能:无锁读事务,原子schema修改,读历史数据无block。
<2>Checkpoint和高低水位:
分布式系统的复杂性在于要考虑到各种各样的意外和蓄意破坏,你要制定出一套有效的法律来约束这个系统里可能发生的一切行为,并没有完美的分布式系统存在。
当replica执行完请求时,需要把之前记录的该请求的信息清除掉。但是不能这样轻易的去清除,其中包含的prepared certificate可能会在后面用得上。所以要有种机制来保证何时可以清除。
其实很简单,每执行完一条请求,该节点会再一次发出广播,就是否可以清除信息在全网达成一致。更好的方案是,我们一连执行了K条请求,在第K条请求执行完时,向全网发起广播,告诉大家它已经将这K条执行完毕,要是大家反馈说这K条我们也执行完毕了,那就可以删除这K条的信息了,接下来再执行K条,完成后再发起一次广播,即每隔K条发起一次全网共识,这个概念叫checkpoint,每隔K条去征求一下大家的意见,要是获得了大多数的认同(a quorum certificate with 2 f + 1 CHECKPOINT messages (including itsown)),就形成了一个 stablecheckpoint(记录在第K条的编号),我们也说该replica拥有了一个stablecertificate就可以删除这K条请求的信息了(即删除k之前的请求)。
这是理想的情况,实际上当replica i向全网发出checkpoint共识后,其他节点可能并没有那么快也执行完这K条请求,所以replica i不会立即得到响应,它还要继续自己的步伐,那这个checkpoint在它那里就不是stable的。那这个步伐快的replica可能会越来越快,可能会把大家拉的越来越远,这时我们来看一下上面提到的高低水位的作用。对该replica来说,它的低水位h等于它上一个stable checkpoint的编号,高水位H=h+L,L是我们指定的数值,它一般是checkpoint周期K的常数倍(这个常数是比较小的,比如2倍),这样即使该replica步伐很快,它处理的请求编号达到高水位H后也得停一停自己的脚步,直到它的stable checkpoint发生变化,它才能继续向前。
注:主节点是坏的,它在给请求编号时故意选择了一个很大的编号,以至于超出了序号的范围,所以我们需要设置一个低水位(low water mark)h和高水位(high water mark)H,让主节点分配的编号在h和H之间,不能肆意分配。
例子:
假设低水位h为,0,高水位H为100,L为100.checkpoint为50,上个stablecheckpoint为150.表示集群每隔50个请求就全网共识下大家确认的请求的情况。
如果节点A已经确认到编号为250了,其他节点才确认到编号为180,则节点A需要等待。直到其他节点确认的编号大于200.于是使得stable checkpoint从150变成200后,节点A才能继续确认编号250以后的请求。
通信次数分析:
pbft算法:
本质需要两个大阶段,一个是准备,一个是确认。进入prepared阶段,需要n^2次 ,进入commited阶段也需要n^2次,所以总次数为2n^2,o(n)为 n^2。
优点:容错算法,如果主节点为作恶节点也能检查出来。
缺点:需要确认和通信的次数较多。
raft算法:
在选好主节点的情况下,其他从节点相当于是被动的复制,所以需要通信次数为n次即可。o(n)为 =n。
优点:容易理解,通信次数比较少。
缺点:如果主节点为作恶节点,无法检查出来。
注:n为节点数,一次通信包括请求和响应