一致性

一致性的解决方法:

分布式场景中由于各种因素导致ACID的困难(从而就有了BASE和CAP):

  1. 延迟因素: 北京到杭州,往返距离2600km ,光在真空中的传输速度是30wkm/s。在玻璃中的速度是真空的2/3。算下来,最小的请求和响应,之间的延迟就有13ms。并且,因为光在管子里走的不是直线,又有信号干扰等问题,一般来说要乘以2~3倍的因子值。所以一次最小的请求和响应,时间就差不多有30ms左右了。再想想TCP的时间窗口的移动策略,相信大家都能意识到,实际上延迟是不可忽略的,尤其在传输较多数据的时候,延迟是个重要的因素,不能不加以考虑。并且,延迟 不是带宽,带宽可以随便增加,千兆网卡换成万兆,但延迟却很难降低。而我们最需要的,是带宽,更是延迟的降低。因为他直接决定了我们的可用性。
  2. 灾备因素:单机的情况下,人们一般不会去追求说一个机器物理上被水冲走了的时候,我的数据要保证不丢。但在分布式场景下,这种追求就成为了可能,而互联网行业,对这类需求更是非常看重,恨不能所有的机器都必须是冗余的,可随意替换的。这样才能保证7*24小时的正常服务。这无疑增加了复杂度的因素。
  3. Scale out的问题: 单机总是有瓶颈的,于是,人们的追求就一定是:不管任何一种角色的机器,都应该可以通过简单的增加新机器的方式来提升整个集群中任何一个角色的性能,容量等指标。这也是互联网行业的不懈追求。
  4. 性能:更快的响应速度,更低的延迟,就是更好的用户体验。

最简单的上述问题的方法是两阶段提交(核心是在prepare的阶段,会对所有该操作所影响的数据加锁,这样就可以阻止其他人对其访问),但它存在一些严重的问题,用的并不多: 1. 需要记录事务进行的过程,log要保证安全和可信,性能非常低。 2. 锁的利用率和并行性较低。 3. 网络开销较大 4. 可见性要求实际上就等于让快的操作等慢的 5. 死等问题

三阶段提交只是解决了两阶段提交的死等问题,对脑裂没有贡献,用的也不多。

实际上业务系统一般并不需要强一致性,因而可以通过一定程度上牺牲一致性来保证可用和可靠:

我们仍然以上一次讲到的bob和smith为例子来说明好了。
开始的时候。Bob要给smith100块,那么实际上事务中要做的事情是
事务开始时查询bob有多少钱。如果有足够多的钱让bob的账户 -100 ,然后给smith 的账户+100 。最后事务结束。

如果这个事情在单机,那么事情可以使用锁的方式加以解决。
但如果bob在一台机器,smith在另外一台机器,我们应该怎么做呢?

第一种最常被人想起的方法,就是两段提交协议。
两段提交协议从原理上来说是非常简单的一套协议。
Prepare(bob-100) at 机器A->prepare (smith+100) at 机器b ->commit(bob) ->commit(smith)
事务结束。

两段提交的核心,是在prepare的阶段,会对所有该操作所影响的数据加锁,这样就可以阻止其他人(或机器)对他的访问。题外话,问个问题: )如果这时有其他节点,用相反的方向,进行更新,也就是先更新smith,然后更新bob.会有可能发生什么事情呢?
两段提交协议是被我们在大部分场景下放弃的一个模型,原因主要是因为
1.      Tm本身需要记录事务进行的过程,log要保证安全和可信,性能非常低。
2.      锁的利用率和并行性较低。
3.      网络开销较大
4.      可见性要求实际上就等于让快的操作等慢的。

所以从性能角度来说,这类需求不多也不常见。
既然这样的模型不行,有没有其他模型可以使用呢?

有的。

在事务的过程中,细心的读者不难发现,实际上事务中并不需要这么强的一致可见性。
Bob是需要强一致的,因为他的操作仰赖于他有多少钱,如果他的钱不够100,那么是不能让他的账户变为负数的。但smith却不需要,smith不需要判断他的账户有多少钱,只需要把钱加到他的账户里,不少给他,到账时间尽可能短就可以。
Smith不需要chech账户的钱数,这个前提非常重要,这也是我们能使用最终一致性的关键因素。

下面,我们来看一下另外的选择吧。
Bob的账号在机器A上,smith的账号在机器b上。
首先,我们在机器A上做以下操作:
1.      本地事务开始
2.      读取bob的账户
3.      判断是否有充足余额
4.      更新bob的账号,将bob的钱减少100
5.      将需要给smith加100块这个操作,以事务的形式插入到同机(A)的一张log表中,并自动生成一个唯一的transactionID。
6.      事务关闭

然后,异步的发送一个通知,给一个消费者。
消费者接到通知后,从bob的机器上读取到需要给smith+100这个操作,以及该操作所对应的transactionID。
然后,按照如下方法进行运作
1.      查看在去重表内是否有对应的transactionID.如果没有,则
2.      开启本地事务
3.      将smith的账户+100
4.      将transactionID 插入去重表
5.      事务结束

这样,我们也可以完成一个交易的核心流程了。在交易类过程中的大量事务操作,都是以这样的方式完成的。

下面,我们针对上面的这个流程的一些抉择的点进行一些探讨。

首先,是bob这个机器,这里涉及第一个抉择点。
如果bob是个消费大户,短时间内进行了大量购买,那么可能会造成的问题是,bob所在的那个机器会成为热点,如果在某个突发的情况下,某个账户突然成为热点,那么这些有状态的数据很难快速的反应并加以处理,会造成事务数在某个单节点大量堆积。造成挂掉。

可能的解决方法是:
1.      利用两段提交协议来让原来的” 将需要给smith加100块这个操作,以事务的形式插入到同机(A)的一张log表中,并自动生成一个唯一的transactionID”这步操作放在另外的一台机器上进行。
这样做的的好处是,无论bob怎么是热点,都可以通过水平的加log机器的方式来防止这种热点的产生。
坏处则有:
         1方案复杂度高
         2额外的网络开销
         3消息基于网络发送后,会可能得到三个可能的反馈:1. 成功 2. 失败 3. 无反馈。最麻烦的就是这个无反馈,他可能成功,也可能失败。所以是不确定的状态,需要进行事务的两边进行第二次确认,来确保这个事务的参与方是否都做了该做的事情,如果有一方做了类似commit的操作,那么另外的一方应该commit.如果两方都没做commit操作,那么应该回滚。
2.      让bob的库余量更高,并按照访问压力进行数据的切分,按照热度进行数据划分,放弃原有的简单取mod的策略。来兼容这种不均匀特性。

其次,如果有80个系统都关注着smith加了100这个操作的log,要做对应的处理(比如一些人要针对这个加钱操作做个打款短信推送,有些要做个数据分析等等),那么这里就有另外一个问题,这些系统对bob所在的库的读取就会让该机器成为悲剧的存在。
所以,可以考虑的方式是,增加一个队列,使用,推,拉,或推拉结合的方式将smith加100这个操作加以分发。这样就可以减轻主机的压力。
         坏处则是:
         1方案进一步复杂
         2如何保证log到数据分发服务器之间的数据同步是安全的和准确的?
         3如何保证分发服务器的可靠和冗余?
         4如何保证写入分发服务器的数据的安全和可靠?

再次,smith这边也有问题,为什么要使用一张去重表呢?其实是因为,在发送端,也就是队列将数据发送到目标机器后,也可能从目标机获取到三种不同的反馈,一类是成功(这个占了大多数)。一类是失败。还有一类是。。。没反馈。

当然,最麻烦的还是这个没反馈的情况,没人知道这时候到底对方是做成功了呢?还是没做成功,为了保证最大的吞吐量,又不能其他人都不做事儿了,就等对方的反馈。所以这里就有另外的权衡了。

一般的模型有两类,一类是用分布式事务来完成。
一类是使用努力送达的模型,说叫努力送达,顾名思义,就是只有得到成功的反馈,才停止投递,而其他时候则重复投递消息,直到对方反馈成功为止。

两种模型比较,显然应该追求速度而放弃方便性,于是我们主要来说说这个努力送达以后所带来的影响。
影响一 : 会有重复的投递,也就是说,这个消息可能会投多次,这对于update set version=version+1 这类的操作来说,是个比较毁灭性的打击。
影响二:如果需要重复投递的消息过多,会导致log分发的机器消耗大量资源来进行重复投递。这会影响server的稳定性
影响三:如果大量堆积消息,那么会造成消息的严重delay。smith发现自己在1个月后收到了bob的钱,你说他会不会去K咱一顿: ) .

最后,额外记的这两次log其实在某些场景下也是可以省去的。

以上,就是我在尝试还原淘宝的消息和事务系统时所能大概想到的一些非常需要权衡和注意的问题点。

小小总结一下,整个问题的核心其实是幂等,说白了就是要能够理解数据基于网络的同步过程中,无反馈是一个经常发生的现象,在这种现象中,重复投递比傻傻等待要有效率的多。所以,重复作为一个side affect也就被默认的存在于系统中,所有的工程师都需要认识到这个问题的客观存在,并采取方法去解决之。

在基于网络的数据同步过程中,如果需要最大化性能,那么,一致性是第一个被放弃的。然后数据和消息不会出现重复,是第二个被放弃指标。


使用这种模型,我们可以放弃原来快得等慢的的模式,让整体的吞吐量和性能不会受制于锁的限制,所以淘宝和支付宝才能够支持如此大的交易量。完成大量交易订单。

paxos协议

简单说就是少数服从多数

zab协议

那么Paxos有没有什么值得改进的地方?有的,很简单,你会发现,如果在一个决议提议的过程中,其他决议会被否决,否决本身意味着更多的网络io,意味着更多的冲突,这些冲突都是需要额外的开销的,代价很大很大。

为了解决类似的问题,所以才会有zoo keeper对paxos协议的改进。zk的协议叫zab协议.

zab协议把整个过程分为两个部分,第一个部分叫选总统,第二个部分叫进行决议。
选总统的过程比较特殊,这种模式,相对的给人感觉思路来源于lamport的面包房算法,这个我们后面讲。,选择的主要依据是:
         1.如果有gid最大的机器,那么他是主机。
         2.如果好几台主机的gid相同,那么按照序号选择最小的那个。

 所以,在开始的时候,给A,B,C,D,E进行编号,0,1,2,3,4。 第一轮的时候,因为大家的Max(gid)都是0,所以自然而然按照第二个规则,选择A作为主机。
 然后,所有人都知道A是主机以后,无论谁收到的请求,都直接转发给A,由A机器去做后续的分发,这个分发的过程,我们叫进行决议。
 进行决议的规则就简单很多了,对其他机器进行3pc 提交,但与3pc不同的是,因为是群发议案给所有其他机器,所以一个机器无反馈对大局是没有影响的,只有当在一段时间以后,超过半数没有反馈,才是有问题的时候,这时候要做的事情是,重新选择总统。
具体过程是,A会将决议precommit给B,C,D,E。然后等待,当B,C,D,E里面的任意两个返回收到后,就可以进行doCommit().否则进行doAbort().
 为什么要任意两个?原因其实也是一样的,为了防止脑裂,原则上只能大于半数,不能少于半数,因为一旦决议成立的投票数少于半数,那么就存在另立中央的可能,两个总统可不是闹着玩的。
 定两个,就能够保证,任意“两台”机器挂掉,数据不丢:),能够做到quorum。。

gossip协议

gossip协议是Dynamo和Cassanra这两套存储的基础之一,说复杂也复杂,说不复杂也不复杂。。其实gossip就是p2p协议。他主要要做的事情是,去中心化。
怎么做到的呢?我只希望在这篇文章里给大家留下几个印象:gossip是干什么的?怎么做到的?优势劣势是什么?即可。对协议的细节感兴趣的,可以自己去深入研究。

gossip的核心目标就是去中心,做到的方式:
       根据种子文件,按照某种规则连接到一些机器,与他们建立联系,不追求全局一致性,只是将对方机器中自己没有的数据同步过来。这里就设计到如何能够快速的知道自己的数据和其他人的数据在哪些部分不一致呢?这时候就要用到Merkle tree了,它能够快速感知自己所持有的数据中哪里发生了变更。
优势:
       去中心化,看看伟大的tor,只要能连到一个seed,有一个口子能出长城,那么你最终就能跳出长城。。
劣势:
       一致性比较难以维持,

引用来源:WhisperXD

Comments

comments powered by Disqus