海量存储检索原理系列文章
作者:WhisperXD
来源:http://qing.blog.sina.com.cn/whisperxd
今天玩微薄的时候有人问我有没有数据存储的相关资料,我想了想。。虽然在这个领域内也算有点积累,以前讲课的ppt有200多页,但毕竟ppt的信息量有限。所以在这里将这个系列的部分内容在这里进行重新编排
http://qing.weibo.com/1765738567/693f0847330005sk.html 海量存储系列之序言
http://qing.weibo.com/1765738567/693f0847330005sm.html 上一篇
上一篇 http://qing.weibo.com/1765738567/693f0847330005v7.html
首先是回答上次的问题。
假设有这么一组数据,性别有4种,user_id是一对多的关系,如果我想查询
select * from tabwhere user_id in (?,?,?,?) and 性别='不明'
如何进行索引构建能够获得比较好的效果呢?
我个人认为,应该建立的是以user_id作为前导列,性别作为辅助列的索引,在大量单值查询时会有优势。
理由如下
1. 假定总数据量为N,user_id的区分度为N/10000 而性别的区分度为N/4
那么如果以user_id作为前导列,性别作为后列,那么查询的复杂度为O(logN+log(N/10000))。也就是说,第一次二分查找之后,下一次是在第一次的二分查找的基础上再去查找。而如果以性别作为前导,user_id作为后列,那么复杂度为
O(logN+log(N/4));
效率略差。
然后进入本次正题。上次介绍了关系模型,那么这次我们来介绍一下事务。
在一切之前,我想先给自己解嘲一下。。事务我自己也没有办法完全融汇贯通,因为每一个小的选择,都会导致效果的完全不同,所以有错请在后面一起探讨。
那么我们在这里,主要以单机事务作为入手点,然后阐述一下多机事务相关的知识点。我在这里只是想做一个引导,让大家能够对整个的知识体系有一个基本的认识,对于细节问题,会给出一些资料,而不会直接去进行讲解,因为篇幅所限.
一般来说,我们一提起事务,就会想到数据库,确实,事务是数据库的最重要的一个属性。但这似乎不是事务的本源,那么,让我们从更深层次去对事务进行一次思考吧:
事务,本质来说就是一组由一个人(或机器)发起的连续的逻辑操作,共同的完成一件事情,在完成整个事情之前,其所有的改动,都不应该对其他人可见和影响。而在事务结束之后,其一切的改动,都必须“全部”“立刻”对其他的人(或机器)可见。
然后,人们为了描述这一运作,使用了四个词汇,这也是很多面试的同学们折戟沉沙之处。J 不过这个以前我也不会,后来发现,理解了以后,确实有点用,所以这里也费一些笔墨吧。
原子性(Atomicity):也就是说,一组操作,要不就都成功,要不就都失败。不存在中间状态。
一致性(Consistency):一致性,也就是说,这个事务在提交或回滚的时候,对其他人(或机器)来说,数据的状态是同一的,不会出现中间的状态。最理想的状态下,就是说,数据提交后,所有的更改立刻同时生效,可惜,在计算机领域,这个做不到。。因为cpu运算,磁盘写入,内存写入,都是要时间的,内部一定是个顺序化的过程,所以不可能做到绝对的立刻同时生效。
所以一致性一般来说指代的是逻辑上的同时生效,比如,我要改A,B两行数据,那么,最简单的一致性保证就是,对A,B加锁,改A,B,然后对A,B解锁。
这样下一个人读到的一定是A,B的最新值啦。
(但这块有很多种解释,一般来说这是个最不明确的词汇)。
隔离性(Isolation):隔离,这是面试最容易挂的一个问题,其实我认为不怪我们,而是因为本身这个隔离性,是依托锁来进行设计的。
我们所知道的锁,主要有以下几种,1.读写锁,2. 排他锁
那么这四种级别其实就和这两种锁的实现有关,之所以要定义四个级别,其实原因也是因为,锁的范围越大,并行效率越低。而范围越小,那么并行的效率就越高。
读未提交: 其实就是什么锁也没有,所以数据的中间状态,是可能被其他人读到的。
读已提交:就是读写锁实现,读锁在查询之后会被释放掉,所以这样其他人可能会更改那些被释放了读锁的数据,这样当前事务再去读取的时候,就可能读取到被别人修改过的数据了,所以一个人在事务中读取到的某个数据,可能下次读取就变成别的数据啦。这就是不可重复读的意思。。
可重复读:也是个读写锁实现,读锁会阻塞其他人(或机器)的写,于是,只要是事务中读取到得数据,都被加了锁,其他人没办法改他们,于是就实现了可重复读咯。
最后是序列化,就是所有都顺序,一个大锁全部锁住J
持久性(Durability):持久性就是,事务执行后,就丢不了了,就算是整个中国被淹了,机器都没了,数据也不应该丢掉(不过基本做不到这个,也就是一个机器挂了不会丢数据而已。。)所有机房没了那数据也就没了。。
对于这块,给大家一些参考资料:
http://zh.wikipedia.org/wiki/%E4%BA%8B%E5%8B%99%E9%9A%94%E9%9B%A2
http://www.cnblogs.com/wangiqngpei557/archive/2011/11/19/2255132.html
这些讲的不错,浅显易懂是我的最爱.
好啦,为了保证我写的东西不会被"qing"这个大怪兽再次吃掉。。我先发这些。
在下一个章节,我们继续在事务这个领域徜徉,给大家介绍一下,在单机上面,事务是如何进行的。
http://qing.weibo.com/1765738567/693f08473300067j.html 下一篇 单机事务
其实在上面介绍ACID的时候
我们已经提到了一种最简单的实现方式,就是锁的实现方式。
从原理来看,事务是个变态而复杂的事情。其实如果是序列化的话呢,那么实现起来一定是非常简单的。
但问题就在于,这样性能实在比较低,于是,就有了非常多的方案,为了能哪怕减少一个地方的锁,或者降低一个地方的锁的级别,就付出大量的时间和代码加以实现。
那么,让我们以崇敬的心情,去拜读一下他们的劳动成果吧~
--------------------------------------------------------------------------------
在上一篇中,我们谈了事务管理的四个核心要素,其中有两个要素是和性能紧密相关的,其实也就是需要涉及到锁的,一个是隔离性,一个是一致性。
一致性问题和隔离性问题,我们都可以归结为一个问题,他们都用于定义,什么时候数据可被共享,什么时候数据必须被独占。而这些决策,就最终决定了整个数据库系统的并行度,也就直接的决定了多线程并发时的性能指标。
如果要改一大批数据,又必须保证这些数据要么都出现,要么都不出现,这时候就有个难题了:因为这些数据不可能在同一个时间被选出,更不可能在同一个时间被更改。
于是就必须想个办法来假装达到这个状态,于是我们就需要一种方法,使得针对不同数据的更改,不同人(或机器)不打架。而如果出现对相同数据的更改,则要将更新进行排队。
这个排队可供选择的方法,就我知道的有:1,排他锁。2. 读写锁。3. Copy on write(MVCC) .4. 队列。5. 内存事务。这些方式。
从性能来说,排他锁最慢,而读写因为读可以并发,所以效率稍高,但写和读不能同时进行。3. Copy on write(MVCC) 则读取和写入之间可以互相不影响,所以效率更高。队列这种方式,内存时效果很好,省去中断上下文切换的时间。内存事务,目前还在研究阶段,具备很大潜力的东西。
排他锁,队列和内存事务,在目前的数据库中用的相对较少,我们就不在这里说了。
这里主要说两种实现,一种是读写锁,一种是MVCC.
先说读写锁,也是隔离性中“读已提交,可重复读”两种实现中最重要的底层实现方式。
简单来说,就是如果一个人在事务中,那么他所有写过的数据,所有读过的数据,都给他来个锁,让其他小样儿都只能等在外面,直到数据库能确定所有更改已经全部完成了,没有剩下什么半拉子状态的时候,就解开所有的锁,让其他人可以读取和写入。Hoho,就是这个了。
那么MVCC呢,其实是对读写锁的一个改进,有一批大牛们,说你们这读写锁,写的时候不能读,读的时候不能写,并行度太低了,我要做个更牛B的,写不阻塞读,读不阻塞写的东西来超越你们。
于是他们想起了copy-on-write.鼓捣了个MVCC数据库出来。。。
题外话,现在的甲骨文,之所以能在数据库领域保持优势地位,有个很重要的原因也是因为他们是很早就在商业数据库系统中实现了MVCC的数据写入引擎。
所以他们的Thomas Kyte 技术副总裁也就有了在他们的最牛逼的oracle专家编程里面有了吹嘘的资本 XD .
这里我们要着重的介绍一下MVCC,因为这东西看起来非常的精妙而美丽。。。现在大量的分布式类存储中,也都在借鉴这套模式中的很多部分来增加自己的并行度,以提升性能。比如megaStore.比如percolator。
我们在读写锁的实现中,提到了写读的相互阻塞问题,MVCC则使用copy-on-write来解决这个问题。
如果一个人在事务中,会先申请一个事务ID,这个ID是自增的,每个事务都有他自己的唯一的ID,那么他写过的数据,都会被转变为一次带有当前事务ID的新数据,在读取的时候,则只会读取小于等于自己事务ID的数据。这样实现的东东,语义上来说,与可重复读就一样了。而如果读小于等于全局ID的数据,那么这样的实现,就是读已提交了。
一般来说,MVCC只实现了四个级别中的第二级和第三级,其他的就没有啦,不过这两个是我们最常见的级别。所以也就大家同乐,同乐了~
有了这个东西,我们的一致性也就很容易保证了,因为一个事物和他对应的版本号对应,又有更改后的数据和更改前的数据,如果要提交,那么就只需要很简单的让更改后的数据生效可见即可,这样我们可以将大量的更新中要做的事情,都在事务过程中进行,这样,比原有的基于读写锁的必须在commit时候一起做掉来说,commit这个操作就轻量化了很多,于是,就可以支持更多的人(或机器)持有事务状态了。
很美妙吧?
我一致认为这是oracle当年的核心竞争力,不过现在基本上是个数据库就用了这一套,我们就不在多嘴啦~
解决了一致性和隔离性,剩下的是原子性和持久性,原子性么,一般来说就是要么都成功,也就是新版本数据都让他生效,要么就都失败,也就是让和自己事务ID对应的所有修改都无效即可。也很好就解决掉了。持久性。这个就是后面我们要在写入模型里面介绍的东西了,基本上来说就是写磁盘策略的事情。
到这里,我们单机ACID的实现大概思路,就给大家介绍过了。下一个章节,我们还要用很多的文字,来向大家介绍在分布式场景中我们面临的事务的难题,以及“我所知道的”百花齐放的解决方法。
http://qing.weibo.com/1765738567/693f0847330006ao.html?rnd=0.6134993201121688
http://qing.weibo.com/1765738567/693f08473300067j.html 上一篇
需要说明的一点是,这里涉及到的权衡点非常的多。就我短短的工作经验里面,也只是能够简单的涉猎一部分,因为在事务这个领域,目前大家都在尝试提出各种各样的不同的方法,而在taobao,我们目前也没有完美的解决这个问题,更多的是在权衡,在金钱和开发成本之间,做出选择。
那么,我们就先从问题开始,来看一下原来的事务出了什么问题。
在事务中,有ACID四种属性。(见上篇文章)
在分布式场景中,我们看引入了什么因素,导致了什么样的新问题:
1. 延迟因素:光是我们所知最快的信息载体了,各位可能都会从潜意识里面认为光传输信息不就是一眨眼的事情而已。那我们做个简单的计算吧(感谢@淘宝叔度,第一次在分享中让我对这个问题有了个数值化的印象。):
北京到杭州,往返距离2600km ,光在真空中的传输速度是30wkm/s。在玻璃中的速度是真空的2/3。算下来,最小的请求和响应,之间的延迟就有13ms。并且,因为光在管子里走的不是直线,又有信号干扰等问题,一般来说要乘以2~3倍的因子值。
所以一次最小的请求和响应,时间就差不多有30ms左右了。
再想想TCP的时间窗口的移动策略,相信大家都能意识到,实际上延迟是不可忽略的,尤其在传输较多数据的时候,延迟是个重要的因素,不能不加以考虑。
并且,延迟 不是 带宽,带宽可以随便增加,千兆网卡换成万兆,但延迟却很难降低。而我们最需要的,是带宽,更是延迟的降低。因为他直接决定了我们的可用性。
2. 灾备因素:单机的情况下,人们一般不会去追求说一个机器物理上被水冲走了的时候,我的数据要保证不丢(因为没办法的嘛。。)。但在分布式场景下,这种追求就成为了可能,而互联网行业,对这类需求更是非常看重,恨不能所有的机器都必须是冗余的,可随意替换的。这样才能保证7*24小时的正常服务。这无疑增加了复杂度的因素。
3. Scale out的问题: 单机总是有瓶颈的,于是,人们的追求就一定是:不管任何一种角色的机器,都应该可以通过简单的增加新机器的方式来提升整个集群中任何一个角色的性能,容量等指标。这也是互联网行业的不懈追求。
4. 性能:更快的响应速度,更低的延迟,就是更好的用户体验。(所以google用了个“可怜”到家的简单input框来提升用户体验,笑)。
说道这里,大概大家都应该对在分布式场景下的广大人民群众的目标有了一个粗略的认识了。
那么我们来看一下原有ACID的问题吧。
在上次的章节中,我们也提到了ACID中,A和D相对的,比较容易达到。但C和I都涉及到锁实现,也就和性能紧密的相关了。
然后,人们就开始了纠结,发掘这个C和I,似乎不是那么容易了。
上次,我们谈到,目前主流的实现一次更新大量数据的时候,不同人(或机器)修改数据相互之间不会打架的方法有以下几种:
1. 排他锁
2. 读写锁
3. Copy-on-write
4. 队列
5. 内存事务
排他锁和读写锁,本身都是锁的实现,单机的锁实现,相对而言是非常简单的事情,但如果涉及到分布式锁,那么消耗就很高了,原因是,锁要在两边都达到一致,需要多次机器之间的交互过程,这个交互的过程,再考虑到延迟的因素,基本上一次加锁请求就要100~200+毫秒的时间了,那么去锁又要这样的时间。而要知道,我们在单机做内存锁操作,最慢也不过10毫秒。。
于是,有一批人就说了,既然这么难,我们不做了!~来个理论证明他很难就行了~。于是就有了CAP和BASE.
所谓CAP,我个人的理解是描述了一种: 在数据存了多份的前提下,一致性和响应时间,读写可用性不可兼得的“现象”而已。
在我这里来看CAP的证明过程就是个扯淡的玩意儿,他只是描述了一种现象而已。原因还是网络延迟,因为延迟,所以如果要做到数据同时出现或消失,那么按照锁的方式原来可能只需要10ms以内完成的操作,现在要200~400ms才能完成,那自然不能接受了。所谓CAP就是这个现象的英文简称,笑。
BASE呢,这个理论似乎更老,其实也是个现象,就是基本可用,软状态,最终一致的简称,也没个证明,其实就是告诉咱:要权衡一下,原来的ACID不太容易实现啦,我们得适当放弃一些啦。但请各位注意,ACID实际上是能够指导我们在什么情况下做什么样的事情能够获取什么样的结果的。而BASE则不行,这也说明BASE不是个经典的理论。
好啦。废话了这么多,其实就是想说,分布式场景没有银弹啦,你们自己权衡去吧。我们大牛们救不了你们啦的意思。。
既然大牛救不了咱,咱就只能自救了。。。
好,好的文章就要在关键的地方恰然而止,留下悬念,我们也就在这里留下点悬念吧。
在这篇中,主要是想给大家介绍一下,目前在分布式场景中,事务碰到了什么问题,出现这些问题的原因是什么。
在下一篇中,我将尝试从原理的角度,去分析目前的几类常见的在分布式场景中完成原有事务需求的方法。敬请期待 : )
在我开始做淘宝数据层的时候,被问得最多的无非也就是:如何做事务,如何做join.至今仍然如此,我一般都会简单而明确的跟对方说:没有高效的实现方法。
虽然没有高效的实现,但实现还是有的。作为引子,我们先来介绍一下这种实现的方式。
我们仍然以上一次讲到的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也就被默认的存在于系统中,所有的工程师都需要认识到这个问题的客观存在,并采取方法去解决之。
在基于网络的数据同步过程中,如果需要最大化性能,那么,一致性是第一个被放弃的。然后数据和消息不会出现重复,是第二个被放弃指标。
使用这种模型,我们可以放弃原来快得等慢的的模式,让整体的吞吐量和性能不会受制于锁的限制,所以淘宝和支付宝才能够支持如此大的交易量。完成大量交易订单。
http://qing.weibo.com/1765738567/693f0847330007ay.html 上一篇
在上一个章节,我们阐述了分布式场景下,事务的问题和一些可能的处理方式后,我们来到了下一章节
Key-value存储
这一章,我们将进入k-v场景,其实,在大部分场景下,如果某个产品宣称自己的写读tps超过其他存储n倍,一般来说都是从k-v这个角度入手进行优化的,主要入手的点是树的数据结构优化和锁的细化,一般都能在一些特定的场景获得5-10倍的性能提升。由此可见key-value存储对于整个数据存储模型是多么的重要。
好吧,那么我们来进入这个章节,用最简单和浅显的话语,阐述这些看起来很高深的理论吧 : )
在未来的几篇中,我们将大概的介绍和分析如下几种比较有特点的数据结构,并探讨其优势劣势以及适用的场景。
让我们先从映射入手吧,所谓映射,就是按照key找到value的过程,这个过程几乎就是我们处理数据的最核心数据结构了。
如何能够根据一个key找到对应的value呢?
一类是hash map.最简单的实现就是算一个key数据的hashCode.然后按照桶的大小取mod.塞到其中的一个桶里面去。如果出现冲突怎么办呢?append到这个桶内链表的尾部就行了。
还有一类呢,我们可以抽象的认为是一个有序结构。之所以把它归类到有序结构原因也很简单,因为只有有序才能做二分查找。。。举些有序结构的例子吧: 1. 数组 2. 各类平衡二叉树 3. B-树类族 4. 链表
这些数据结构如果想进行快速查找,都需要先让他们有序。然后再去做log2N的二分查找找到对应的key。
从原教旨上来说,这就是我们要用的key-value的主要结构了。
那么,hash和有序结构,他们之间有什么样的差别呢?我们来进行一下简单的比较
基本上来说,核心区别就是上面的这点,hash单次查询效率较高,但为了保证O(1)效率,对空间也有一定要求。而有序结构,查询效率基本是O(log2N)这个级别。但有序结构可以支持范围查找,而hash则很难支持。
所以,一般来说我们主要在使用的是有序结构来进行索引构建,因为经常需要查询范围。
不过,所有数据库几乎都支持hash索引,如果你的查询基本都是单值的,那么可以找一找稳定的hash索引,他们能从一定程度上提升查询的效率。
在这里,我们主要讨论有序结构,对于数据库或nosql来说,有序结构主要就是指b-tree或b-tree变种。那么我们先来介绍一下什么叫b-tree作为讨论磁盘结构的入门吧。
先上图(copy的,这是个b-tree。版权方请找我)
首先进行词汇科普:b-tree只有两类,一类叫b-tree,就是btree,还有一类是b+tree,但b-tree不是b”减”树的意思。这个大家不要再跟当年的我犯同样的错误哟 :__0
那么b树的核心是几个关键词
1. 树高:一般来说,树的高度比较低。三到五层
2. 数组:每一个node,都是一个“数组”,数组是很关键的决定性因素,我们后面写入和读取分析的时候会讲到。
没了呵呵
然后我们进行一下读取和写入的模拟。
读取来说:如果我要查找28这个数据对应的value是多少,路径大概是:首先走root节点,取出root node后,对该数组进行二分查找,发现35>28>17,所以进入branch节点中的第二个节点,取出该节点后再进行二分查找。发现30>28>26,所以进入branch节点的p2 value,取出该节点,对该三个值的数组进行二分查找,从而定位到28这个数据的对应value。
而写入删除则涉及到分裂和合并这两个btree最重要的操作,比如,要写入37,那么会先找到36所应该被插入的数组[36,60]这个数组,然后判断其是否有空,如果有空,则对该数组进行重新排序。而如果没有空,则必须要进行分裂。分裂的缘由是因为组成b-tree的每一个node,都是一个数组,数组最大的特性是,数组内元素个数是固定的。因此必须要把原有已经满掉的数组里面的一半的数据拿出来,放到新的一个新建立的空数组中,然后把要写入的数据写入到老或新的这两个数组里面的一个里面去。
【这里要留个问题给大家了,我想问一下,为什么b-tree要使用数组来存储数据呢?为什么不选择链表等结构呢?】
对于上面的这个小的b-tree sample里面呢,因为数组[35,60],数组已经满了,所以要进行分裂。于是数组在插入了新值以后,变成了两个[35,36] 和[60] ,然后再改变父节点的指针并依次传导上去即可。
当出现删除的时候,会可能需要进行合并的工作,也就是写入这个操作的反向过程。在一些场景中,因为不断地插入新的id,删除老的id,会造成b-tree的右倾,这时候需要有后台进程对这种倾向进行不断地调整。
基本上,这就是b-tree的运转过程了。
B+tree
B+tree 其实就是在原有b-tree的基础上。增加两条新的规则
1. Branch节点不能直接查到数据后返回,所有数据必须读穿或写穿到leaf节点后才能返回成功
2. 子叶节点的最后一个元素是到下一个leaf节点的指针。
这样做的原因是,更方便做范围查询,在b+树种,如果要查询20~56.只需要找到20这个起始节点,然后顺序遍历,不再需要不断重复的访问branch和root节点了。
发现每一种数据结构都需要去进行简介才能够比较方便的了解到他们的特性,所以在后续的章节还会介绍几种有代表性的树的结构都会针对性的加以介绍。
首先来回答一个问题:为什么在磁盘中要使用b+树来进行文件存储呢?
原因还是因为树的高度低得缘故,磁盘本身是一个顺序读写快,随机读写慢的系统,那么如果想高效的从磁盘中找到数据,势必需要满足一个最重要的条件:减少寻道次数。
我们以平衡树为例进行对比,就会发现问题所在了:
先上个图
这是个平衡树,可以看到基本上一个元素下只有两个子叶节点
抽象的来看,树想要达成有效查找,势必需要维持如下一种结构:
树的子叶节点中,左子树一定小于等于当前节点,而当前节点的右子树则一定大于当前节点。只有这样,才能够维持全局有序,才能够进行查询。
这也就决定了只有取得某一个子叶节点后,才能够根据这个节点知道他的子树的具体的值情况。这点非常之重要,因为二叉平衡树,只有两个子叶节点,所以如果想找到某个数据,他必须重复更多次“拿到一个节点的两个子节点,判断大小,再从其中一个子节点取出他的两个子节点,判断大小。”这一过程。
这个过程重复的次数,就是树的高度。那么既然每个子树只有两个节点,那么N个数据的树的高度也就很容易可以算出了。
平衡二叉树这种结构的好处是,没有空间浪费,不会存在空余的空间,但坏处是需要取出多个节点,且无法预测下一个节点的位置。这种取出的操作,在内存内进行的时候,速度很快,但如果到磁盘,那么就意味着大量随机寻道。基本磁盘就被查死了。
而b树,因为其构建过程中引入了有序数组,从而有效的降低了树的高度,一次取出一个连续的数组,这个操作在磁盘上比取出与数组相同数量的离散数据,要便宜的多。因此磁盘上基本都是b树结构。
不过,b树结构也不是完美的,与二叉树相比,他会耗费更多的空间。在最恶劣的情况下,要有几乎是元数据两倍的格子才能装得下整个数据集(当树的所有节点都进行了分裂后)。
以上,我们就对二叉树和b树进行了简要的分析,当然里面还有非常多的知识我这里没有提到,我希望我的这个系列能够成为让大家入门的材料,如果感兴趣可以知道从哪里着手即可。如果您通过我的文章发现对这些原来枯燥的数据结构有了兴趣,那么我的目标就达到了: )
在这章中,我们还将对b数的问题进行一下剖析,然后给出几个解决的方向
其实toku DB的网站上有个非常不错的对b树问题的说明,我在这里就再次侵权一下,将他们的图作为说明b树问题的图谱吧,因为真的非常清晰。
http://tokutek.com/downloads/mysqluc-2010-fractal-trees.pdf
B树在插入的时候,如果是最后一个node,那么速度非常快,因为是顺序写。
但如果有更新插入删除等综合写入,最后因为需要循环利用磁盘块,所以会出现较多的随机io.大量时间消耗在磁盘寻道时间上。
如果是一个运行时间很长的b树,那么几乎所有的请求,都是随机io。因为磁盘块本身已经不再连续,很难保证可以顺序读取。
以上就是b树在磁盘结构中最大的问题了。
那么如何能够解决这个问题呢?
目前主流的思路有以下几种
1. 放弃部分读性能,使用更加面向顺序写的树的结构来提升写性能。
这个类别里面,从数据结构来说,就我所知并比较流行的是两类,
一类是COLA(Cache-Oblivious Look ahead Array)(代表应用自然是tokuDB)。
一类是LSM tree(Log-structured merge Tree)或SSTABLE
(代表的数据集是cassandra,hbase,bdb java editon,levelDB etc.).
2. 使用ssd,让寻道成为往事。
我们在这个系列里,主要还是讲LSM tree吧,因为这个东西几乎要一桶浆糊了。几乎所有的nosql都在使用,然后利用这个宣称自己比mysql的innodb快多少多少倍。。我对此表示比较无语。因为nosql本身似乎应该是以省去解析和事务锁的方式来提升效能。怎么最后却改了底层数据结构,然后宣称这是nosql比mysql快的原因呢?
毕竟Mysql又不是不能挂接LSM tree的引擎。。。
好吧,牢骚我不多说,毕竟还是要感谢nosql运动,让数据库团队都重新审视了一下数据库这个产品的本身。
那么下面,我们就来介绍一下LSM Tree的核心思想吧。
首先来分析一下为什么b+树会慢。
从原理来说,b+树在查询过程中应该是不会慢的,但如果数据插入比较无序的时候,比如先插入5 然后10000然后3然后800 这样跨度很大的数据的时候,就需要先“找到这个数据应该被插入的位置”,然后插入数据。这个查找到位置的过程,如果非常离散,那么就意味着每次查找的时候,他的子叶节点都不在内存中,这时候就必须使用磁盘寻道时间来进行查找了。更新基本与插入是相同的
那么,LSM Tree采取了什么样的方式来优化这个问题呢?
简单来说,就是放弃磁盘读性能来换取写的顺序性。
乍一看,似乎会认为读应该是大部分系统最应该保证的特性,所以用读换写似乎不是个好买卖。但别急,听我分析之。
1. 内存的速度远超磁盘,1000倍以上。而读取的性能提升,主要还是依靠内存命中率而非磁盘读的次数
2. 写入不占用磁盘的io,读取就能获取更长时间的磁盘io使用权,从而也可以提升读取效率。
因此,虽然SSTable降低了了读的性能,但如果数据的读取命中率有保障的前提下,因为读取能够获得更多的磁盘io机会,因此读取性能基本没有降低,甚至还会有提升。
而写入的性能则会获得较大幅度的提升,基本上是5~10倍左右。
下面来看一下细节
其实从本质来说,k-v存储要解决的问题就是这么一个:尽可能快得写入,以及尽可能快的读取。
让我从写入最快的极端开始说起,阐述一下k-v存储的核心之一—树这个组件吧。
我们假设要写入一个1000个节点的key是随机数的数据。
对磁盘来说,最快的写入方式一定是顺序的将每一次写入都直接写入到磁盘中即可。
但这样带来的问题是,我没办法查询,因为每次查询一个值都需要遍历整个数据才能找到,这个读性能就太悲剧了。。
那么如果我想获取磁盘读性能最高,应该怎么做呢?把数据全部排序就行了,b树就是这样的结构。
那么,b树的写太烂了,我需要提升写,可以放弃部分磁盘读性能,怎么办呢?
简单,那就弄很多个小的有序结构,比如每m个数据,在内存里排序一次,下面100个数据,再排序一次……这样依次做下去,我就可以获得N/m个有序的小的有序结构。
在查询的时候,因为不知道这个数据到底是在哪里,所以就从最新的一个小的有序结构里做二分查找,找得到就返回,找不到就继续找下一个小有序结构,一直到找到为止。
很容易可以看出,这样的模式,读取的时间复杂度是(N/m)*log2N 。读取效率是会下降的。
这就是最本来意义上的LSM tree的思路。
那么这样做,性能还是比较慢的,于是需要再做些事情来提升,怎么做才好呢?
于是引入了以下的几个东西来改进它
1. Bloom filter : 就是个带随即概率的bitmap,可以快速的告诉你,某一个小的有序结构里有没有指定的那个数据的。于是我就可以不用二分查找,而只需简单的计算几次就能知道数据是否在某个小集合里啦。效率得到了提升,但付出的是空间代价。
2. 小树合并为大树: 也就是大家经常看到的compact的过程,因为小树他性能有问题,所以要有个进程不断地将小树合并到大树上,这样大部分的老数据查询也可以直接使用log2N的方式找到,不需要再进行(N/m)*log2n的查询了。
这就是LSMTree的核心思路和优化方式。
不过,LSMTree也有个隐含的条件,就是他实现数据库的insert语义时性能不会很高,原因是,insert的含义是: 事务中,先查找该插入的数据,如果存在,则抛出异常,如果不存在则写入。这个“查找”的过程,会拖慢整个写入。
这样,我们就又介绍了一种k-v写入的模型啦。在下一次,我们将再去看看另外一种使用了类似思路,但方法完全不同的b树优化方式 COLA树系。敬请期待 ~
http://qing.weibo.com/1765738567/693f0847330008ii.html 上一篇
从性能来说,tokuDB的写入性能很高,但更新似乎不是很给力,查询较好,占用较少的内存。
http://www.mysqlperformanceblog.com/2009/04/28/detailed-review-of-tokutek-storage-engine/
这里有一些性能上的指标和分析性文字。确实看起来很心动,不过这东西只适合磁盘结构,到了SSD似乎就挂了。原因不详,因为没有实际的看过他们的代码,所以一切都是推测,如果有问题,请告知我。
先说原理,上ppt http://tokutek.com/presentations/bender-Scalperf-9-09.pdf,简单来说,就是一帮MIT的小子们,分析了一下为什么磁盘写性能这么慢,读的性能也这么慢,然后一拍脑袋,说:“哎呀,我知道了,对于两级的存储(比如磁盘对应内存,或内存对于缓存,有两个属性是会对整个查询和写入造成影响的,一个是容量空间小但速度更快的存储的size,另外一个则是一次传输的block的size.而我们要做的事情,就是尽可能让每次的操作传输尽可能少的数据块。
传输的越少,那么查询的性能就越好。
进而,有人提出了更多种的解决方案。
•B-tree [Bayer, McCreight 72]
• cache-oblivious B-tree [Bender, Demaine, Farach-Colton 00]
• buffer tree [Arge 95]
• buffered-repositorytree[Buchsbaum,Goldwasser,Venkatasubramanian,Westbrook 00]
• Bε
tree[Brodal, Fagerberg 03]
• log-structured merge tree [O'Neil, Cheng, Gawlick, O'Neil 96]
• string B-tree [Ferragina, Grossi 99]
这些结构都是用于解决这样一个问题,在磁盘上能够创建动态的有序查询结构。
在今天,主要想介绍的就是COLA,所谓cache-oblivious 就是说,他不需要知道具体的内存大小和一个块的大小,或者说,无论内存多大,块有多大,都可以使用同一套逻辑进行处理,这无疑是具有优势的,因为内存大小虽然可以知道,但内存是随时可能被临时的占用去做其他事情的,这时候,CO就非常有用了。
其他我就不多说了,看一下细节吧~再说这个我自己都快绕进去了。
众所周知的,磁盘需要的是顺序写入,下一个问题就是,怎么能够保证数据的顺序写。
我们假定有这样一个空的数据集合
可以认为树的高度是log2N。
每行要么就是空的,要么就是满的,每行数据都是排序后的数据。
如果再写一个值的时候,会写在第一行,比如写了3。
再写一个值11的时候,因为第一行已经写满了,所以将3取出来,和11做排序,尝试写第二行。又因为第二行也满了,所以将第二行的5和10也取出,对3,11,5,10 进行排序。写入第四行
这就是COLA的写入过程。
可以很清楚的看出,COLA的核心其实和LSM类似,每次“将数据从上一层取出,与外部数据进行归并排序后写入新的array”的这个操作,对sas磁盘非常友好。因此,写入性能就会有非常大的提升。
并且因为数据结构简单,没有维持太多额外的指针,所以相对的比较节省空间。
但这样查询会需要针对每个array都进行一次二分查找。
性能似乎还不是很高,所以,他们想到了下面这种方式,把它的命名为fractal tree,分形树。
用更简单的方法来说的话呢,就是在merge的时候,上层持有下层数据的一个额外的指针。
来协助进行二分查找。
这样,利用空间换时间,他的查询速度就又回到了log2N这个级别了。
到此,又一个有序结构被我囫囵吞枣了。
嘿嘿
在下一篇,我们将进入大家期待的分布式k-V场景,也就是noSQL的范畴了,让我们拨开nosql的神秘面纱,看看这东西到底意味着什么。
http://qing.weibo.com/1765738567/693f0847330008x6.html 上一篇
在分布式key-value中,很多原来的知识是可以继续复用的。因为k-v解决的问题实在是非常的简单,只不过是根据一个key找到value的过程,所以原来的知识,现在也继续的可以用。
但有两个额外的因素需要考虑
网络延迟
TCP/IP –公用网络,ip跳转慢,tcp包头大
可能出现不可达问题
这其实是状态机同步中最难的一个问题,也就是,A给B消息,B可能给A的反馈可能是:1. 成功 2. 失败 3. 无响应。最难处理的是这个无响应的问题。以前的文章中我们讨论过这个问题,以后还会碰到。这里暂且hold住。
先上图一张,在未来的几周内,我们都会依托这张图来解释分布式K-V系统
可以看到,在客户机到服务器端,有这么几个东西
一,规则引擎
二,数据节点间的同步
抽象的来看,分布式K-V系统和传统的单机k-v系统的差别,也就只在于上面的两个地方的选择。
今天先来谈规则引擎
抽象的来看,规则引擎面向的场景应该被这样的描述:对于有状态的数据,需要一套机制以保证其针对同一个数据的多次请求,应该物理上被发送到同一个逻辑区块内的同一个数据上。
举例子来说,一个人进行了三笔交易,每笔交易都是这个人给其他人100元。那么,这三比交易的更新(update set money = money – 100 where userid = ?) 必须被发到同一台机器上执行,才能拿到正确的结果。【不考虑读性能的gossip模型除外】
这种根据一个userid 找到其对应的机器的过程,就是规则引擎所要处理的事情。
我们对于规则引擎的需求,一般来说也就是要查的快,第二个是要能尽可能的将数据平均的分配到所有的节点中去。第三个,如果新的节点加入进去,希望能够只移动那些需要移动的数据,不需要移动的数据则不要去移动他。
那么一想到“根据xxx找到xxx”相信大家第一个想到的一定又是以前说过的K-V了。所以我们就再复习一遍: )
Hash
O(1)效率
不支持范围查询(时间这样的查询条件不要考虑了)
不需要频繁调整数据分布
顺序
主要是B-Tree
O(logN)效率
支持范围查询
需要频繁调整节点指针以适应数据分布
这也就是我们最常用的两种分布式k-value所使用的数据结构了
首先来看HASH的方法。Hash其实很容易理解,但我跟不少人交流,发现大家可能对一致性hash的理解有一定的误解。下面请允许我给大家做个简单的介绍。
简单取模:
最简单的HASH 就是取mod,user_id % 3 。这样,会将数据平均的分配到0,1,2 这三台机器中。
这也是我们目前最常用的,最好用的方案。但这套方案也会有一个问题,就是如果id % 3 -> id % 4 总共会有75%的数据需要变动hash桶,想一想,只增加了一台机器,但80的数据需要从一个机器移动到另外一个机器,这无疑是不够经济的,也是对迁移不友好的方案。
不过,虽然增加一台机器,会发生无谓的数据移动,但取模的方案在一些特殊的场景下,也能很好的满足实际的需要,如id % 3 -> id % 6,这种情况下,只需要有50%的数据移动到新的机器上就可以了。这也就是正常的hash取模最合适的扩容方式-----> 倍分扩容。我们一般把这种扩容的方式叫做”N到2N”的扩容方案。
取模Hash还有个无法解决的问题,就是无法处理热点的问题,假设有一个卖家有N个商品。如果按照卖家ID进行切分,那么就有可能会造成数据不均匀的问题。有些卖家可能有10000000个商品,而有些卖家只会有10个。这种情况下如果有大量商品的卖家针对他的商品做了某种操作,那这样无疑会产生数据热点。如何解决这类问题,也是分布式场景中面临的一个重要的问题。
既然简单取模有这么多的问题,那有没有办法解决这些问题呢?
首先,我们来介绍第一种解决这个问题的尝试。
一致性Hash.
先来个图,这套图估计几乎所有对Nosql稍有了解的人都应该看过,在这里我会用另外的方式让大家更容易理解
上面这个图,用代码来表示,可以认为是这样一套伪码
Def idmod = id % 1000 ;
If(id >= 0 and id < 250)
returndb1;
Else if (id >= 250 and id < 500)
returndb2;
Else if (id >= 500 and id < 750)
returndb3;
Else
returndb4;
这个return db1 db2 db3 db4 就对应上面图中的四个浅蓝色的点儿。
而如果要加一个node5 ,那么伪码会转变为
Def idmod = id % 1000 ;
If(id >= 0 and id < 250)
returndb1;
Else if (id >= 250 and id < 500)
returndb2;
Else if (id >= 500 and id < 625)
returndb5;
else if(id >= 625 and id <750)
returndb3
Else
returndb4;
从这种结构的变化中,其实就可以解决我们在普通hash时候的面临的两个问题了。
1. 可以解决热点问题,只需要对热点的数据,单独的给他更多的计算和存储资源,就能部分的解决问题(但不是全部,因为迁移数据不是无成本的,相反,成本往往比较高昂)
2. 部分的能够解决扩容的问题,如果某个点需要加机器,他只会影响一个节点内的数据,只需要将那个节点的数据移动到新节点就可以了。
但一致性hash也会带来问题,如果数据原本分布就非常均匀,那么加一台机器,只能解决临近的一个节点上的热点问题,不会影响其他节点,这样,热点扩容在数据分布均匀的情况下基本等于n->2n方案。因为要在每个环上都加一台机器,才能保证所有节点的数据的一部分迁移到新加入的机器上。
这无疑对也会浪费机器。
于是,我们又引入了第三套机制:
虚拟节点hash
Def hashid = Id % 65536
可以很容易的看出,上面这套虚拟节点的方案,其实与id % 4的结果等价。
可以认为一致性hash和普通节点hash,都是虚拟节点hash的特例而已。
使用虚拟节点hash,我们就可以很容易的解决几乎所有在扩容上的问题了。
碰到热点?只需要调整虚拟节点map中的映射关系就行了
碰到扩容?只需要移动一部分节点的映射关系,让其进入新的机器即可
可以说是一套非常灵活的方案,但带来的问题是方案有点复杂了。
所以,我们一般在使用的方式是,首先使用简单的取模方案,如id % 4。在扩容的时候也是用N->2N的方案进行扩容。但如果碰到需求复杂的场景,我们会“无缝”的将业务方原来的简单取模方案,直接变为使用虚拟节点hash的方案,这样就可以支持更复杂的扩容和切分规则,又不会对业务造成任何影响了。
好,到这,我基本上就给大家介绍了如何使用Hash来完成分布式k-value系统的规则引擎构建了。
下一期我们来看一下使用树的方案,当然,主要也就是hbase这个东西了,可能会再介绍一下mongodb的自动扩容方案。睡觉睡觉: )
http://qing.weibo.com/1765738567/693f084733000963.html 上一篇
上一期我们主要在介绍hash相关的切分方式,那么这次我们来看一下有序结构的切分
有序结构的拆分,目前主要就是使用树或类似树的结构进行拆分,这里主要就是指HBase和MongoDB.
使用树结构切分,带来的好处就如hbase和mongoDB的宣传标语一样,可以无缝的实现自由扩展。但反过来,带来的问题其实也不少,下面我们一起来看一看吧。
首先复习B树知识http://qing.weibo.com/1765738567/693f0847330008ii.html
在B树中,最关键的处理逻辑是如果单个节点数据满的时候,应该进行节点分裂和节点合并。
那么,其实在HBase中也有类似这样的过程。
对于巨大量的数据来说,整个树的Branch节点都有可能超过单机的内存大小上限,甚至超过单机的硬盘大小上限。
这时候就需要把BTree进行拆分,这种拆分的最标准实现映射,就是HBase.
(图片版权方在:http://blog.csdn.net/HEYUTAO007/article/details/5766951)
看这个图可能会比较晕,没关系,听我分析之。
首先,整个Hbase就是为了解决一个B树非常巨大,以至于单机无法承载其branch and root节点之后,使用分布式存储的方式来提升整个树的容灾量的一种尝试。
抽象的来看,每一个HRegion都是一个Btree的Node,这个Node会挂在在某个Region server上面,RangeServer内可以存放多个Hregion ,其实就是Btree的branch节点了,但因为Branch也很多,以至于单机无法存放所有branch节点,因此就还需要一层结构来处理这个问题。这就是HMaster 。
上图
虽然可能有点抽象,不过本质来说就是这样一个东西。
当然,细节有点变化:
HMaster ,在上面的图中是单个点,实际的实现是一个btree,三层结构的。
因为HMaster的数据不经常发生变化,同时,每次请求都去访问HMaster,那么HMaster所承担的读写压力就过大了。所以,HBase增加了一个客户端的Cache.来存HMaster中的这几层BTree.
于是,可怜的Hbase又得考虑如何能够将HClient和HMaster中的数据进行同步的问题。
针对这个问题,Hbase提出的解决思路是,既然变动不大,那就允许他错吧,只要咱知道出错了,改正了就行了。
也即,允许HClient根据错误的Btree选择到错误的Region Server,但一旦发现自己所选的数据在那台Region server上无法找到,则立刻重新更新自己的HMaster表。已达到同步。
这基本上就是BTree的分布式实践中做的最好的HBase的一些过程了。
然后然后,私货时间开始: )
借助HDFS,Hbase几乎实现了无限的扩展性,但整体结构过于复杂和庞大了,最终,他只解决了一个K-V写入的问题,同时又希望对所有用户屏蔽底层的所有数据节点的具体位置。
这套思路有其优势之处(也就是Btree的优势):
1. 纯粹log场景,btree管理起来非常方便
2. 支持范围查询
但可能的劣势其实也很多
1. 结构繁杂,在各种角色中进行数据同步,这件事本身听起来就已经很吓人了。然而,最终,他只是解决了一个按照K找到V的过程。。Hash一样可以做到
2. Region server ,维护难度较高,核心数据结构点,虽然该机器可以认为是个接近无状态的机器,但如果想拿一台空机器恢复到可以承担某个Region server的指责,这个过程需要的时间会很长,导致的问题就是,系统的一部分数据不可用,甚至发生雪崩。
3. BTree 在不断追加append的时候,其实是有热点的,目前没有很好地办法能在按照时间序或按照自增id序列的时候保证所有的数据存储机都能够比较均衡的写入数据。会存在热点问题,这个问题的源头在BTree需要有序并连续,这意味着连续的数据只会被写在一个region块内,这个问题在单机btree其实也是存在的,但有raid技术,以及有二级索引,所以问题没有那么明显。(感谢@bluedavy)
综上,HBase其实从一开始是一个面向后端处理的数据引擎,在数据一致性上是可以期待的,但对于线上系统来说,他违背了重要的一个原则:简单。所以我“个人”对这一点持保留态度。
不过,这么多大牛在努力的经营HBase这个产品,那么我也乐观其成,毕竟能把这么复杂的东西整的能在这么多台机器上用,也是个巨大成就了。
MongoDB其实也是在学Hbase的这种有序的BTree结构,不过它的实现就简单的多了。
就是把数据拆分成一段一段的数据,用一个公用的配置角色存储这段数据所在的分片。查询时进行二分查找找到。
思路类似。
从角色来看
他的规则引擎实现就是个有序数据的实现,可以认为是个两层有序结构查找.第一层决定数据的具体机器(Mongos+config server),第二层决定数据在该机的具体位置MongoServer。
好了,画个图用了20分钟,今天的介绍就到这里,下期我们来探讨分布式场景下一个必要的过程。数据的迁移方式讨论。
http://qing.weibo.com/1765738567/693f084733000a5w.html 上一篇
时间隔了比较久了,因为最近在过年临近,所以都在准备这方面的事情。这里提前祝大家新年快乐。
然后还是回到我们的正题儿吧:)
本章,我们主要来讨论数据的管理和扩容中最重要的一个部分,数据迁移。
数据迁移是数据运维中最为重要的一个部分,在前面的文章中已经提到过,作为有状态的数据节点,在互联网行业的主要追求就是,无限的水平扩展能力,这种水平扩展,主要用于解决两类问题,一类是磁盘空间不足的问题,一类是性能不足的问题。
为了达到这种能力,一般来说主要也就是这样一个思路,尽可能的让数据不动,只通过规则变动的方式来完成扩容,如果这种方式无法满足要求,那么再通过移动数据的方式,来满足其他的一些需求。
下面来进行下分析。
只通过变动规则的方式扩容,举个最简单的例子,就是一组按照时间或自增id的数据。那么最简单的切分方式,就是按照时间或id的范围,将一组数据直接映射到某个具体的机器上,类似
if(gmt> = 2010 and gmt < 2011)
returndataNode1;
elseif( gmt >= 2011 and gmt < 2012)
returndataNode2;
elseif(gmt >= 2012 and gmt < 20121223)
returndataNode3;
…
使用这种方式的好处,显而易见,就是不用动数据,方法简单。
但带来的坏处也明显,就是不移动数据,那么如果一组数据已经成为热点,那么永远也没有机会将热点数据分开到不同的机器里用以减轻热点的损耗了。而,这种情况是非常有可能的,对于一对多的模型,如果按照一去存储数据,那么因为多的数据量的不断扩展,会最终导致单个机器的数据量和io超限。
为了解决上述矛盾,就需要引入数据的迁移的方法了,简单来说,就是按照规则将数据从原来的一组机器上,迁移到新的一组机器上去,这样规则和数据一起变动,就可以有效的解决上面所说的热点问题,尽可能让所有的机器均匀的发挥效用。
思路很简单,但工程实践就复杂多了,下面来描述几种扩容的模式,希望大家能针对这几种场景以及我的分析,对如何解决这个问题有个更深入的认识。
所有有状态的数据,其实都需要有扩容的策略,在这些扩容的模式中,最简单的莫过于对cache节点的扩容了。因为cache本身其实只是一个一致的数据的一个快照,快照的意义就在于:如果你对快照的数据是否正确有异议,可以直接去从数据的源头再查一次写回快照中,即可保证数据的最新。
那么对于缓存数据,一般来说缓存的更新逻辑有两种,一种是写的时候同步更新缓存。一种是先读缓存,缓存没有的时候读数据库读出最新值后更新缓存,一般来说是两种缓存模式组合使用,因为没有副作用。对于这种情况的缓存节点扩容,最简单的做法是,只需要直接改变规则即可。
如,假设原来的数据是按照id% 4进行切分的,那么如果规则换为id% 8.那么有一半的数据就无法被访问到。但没关系,因为业务的实际逻辑是,如果读不到,就读穿缓存去数据库里面取数据再更新回缓存,所以很快,数据会按照新的id% 8 进行填充,扩容就完成了。
当然,实际的扩容比这个要复杂一点,因为,要考虑规则变动后,读穿的次数增多,导致数据库压力上升的问题,所以要尽可能的避免过多的数据读穿缓存,这时候会使用我们在以前的文章中讨论过的一致性hash或虚拟节点hash,使用缓慢更新映射关系的方式,来降低扩容对数据库带来的压力。
以上是最简单的规则和数据一起移动的例子,从上述例子可以分析出,其实规则迁移的最主要问题在于如何保证规则变更时,数据能够在规则发生变动的时候对外部保证数据是最新的读取,在缓存扩容的case中,这个数据保证最新的任务,是由数据库这个组件来完成的。所以缓存扩容是相对最为简单的。
那么,自然的就会产生另外一个疑问:对于数据库,怎么保证这个一致性的读取呢?这也是我们这一章要阐明的最重要的问题。
数据的一致性读,一般来说就只有两种做法。第一种是共享内存指针,说白了就是数据只有一份,但指向该数据的指针可能是多个。还有一种就是数据复制,数据的复制,保证一致性的难度会很大。更多的情况是按照实际的需求,取两种模式的折衷。
对数据节点的扩容而言,其实核心就是数据的复制,既然复制,那么一致性就非常难以保证,于是我们也就只能尽可能巧妙地利用手头的工具,取折衷,用以尽可能的减少不一致的影响。
为了解决这个一致性的问题,我们需要在规则上引入版本,这个概念,主要是用于规定什么时候数据应该以什么规则进行访问。这样,就可以避免数据复制过程中所带来的不一致的问题了。
假设,我们原来的规则,版本号为0,新的规则,版本号为1.那么,开始的时候,客户端所持有的数据的切分规则是版本0,所有数据在老的一组机器上进行读取和写入,不会出现问题。当我给定v0和v1两个版本同时存在时,从客户端就可以意识到,目前的规则是两份并存,数据可能是不一致的,这时候最简单的处理策略是,阻止一切读取和写入,这样数据的不一致就不会发生了(哈哈,因为本身不允许读写了嘛。。),而当规则变为只有v1的时候,那么客户端就可以知道,目前只有一个规则了,按照这个规则,进行数据访问就可以了。
使用版本号,就可以让客户端能够有机会意识到数据在某个时间段可能存在着不一致,应该加以针对性的处理,这样就可以规避数据读写的不一致的问题了。
解决了不一致的问题,下面紧接着要解决的问题有两个:
我如何知道应该让哪些数据移动到哪台机器上?
我如何尽可能的减小规则并存时的停写的数据范围?
针对这个问题,外面开源的社区,最常用的解决方法是一致性hash。
在一致性hash中,在某个地方加一组机器,可以很容易的预测应该将哪个节点的数据移动到新的节点上。同时,又可以预测,哪些节点不会受到影响,哪些不受到影响的节点,完全可以开放读取,而受到影响的节点,则阻止访问即可。
如上图中,
node4和node2中间,加了一个node5,那么很容易的可以知道,只需要将node4中的一部分数据,写入新的node5即可。而node2,node1,node3的数据不受到影响,可以继续允许访问。
这样就可以比较成功的解决上面提到的两个问题了。
但从http://qing.weibo.com/1765738567/693f084733000963.html这篇文章的讨论中,我们也很容易可以看到,一致性hash也有他自己的问题。
于是,自然就有人要问,有没有其他的做法呢?
自然是有啦,下面来介绍一下淘宝TDDL在这方面的工程实践吧。以下是纯粹干货,目前暂时没见过业内使用类似方式,这种模式在淘宝也经历了较多次的自动扩容考验,能够满足我们的需求,相信也一定能满足您的需求,因为它什么都没做,也什么都做了:).
首先是需求描述:分析淘宝的需求,简单概括就是一句话,业务方的规则需求,复杂到无以复加,绝非简单一致性hash或简单btree可以满足,为了不同的业务需求,会有种类很多的切分规则。
需求分析:
需求分析其实就是挖掘需求的含义,找到哪些是真实的需求,哪些不是,将不是的砍掉,看看剩下的能不能满足的过程:)
扩容系统的技术特点:
规则系统要自定义,因为这是业务核心,只有业务知道他们的数据怎么分配会获得比较均匀的访问模型。
扩容“不是”常态,一般来说扩容的周期是3个月~6个月,甚至更长。如果一个业务,每6天要扩容一次,那采购人员绝对会抄家伙找他们team干架去了
扩容本身不是不能做,但难度较大,一般来说需要几个人一起参与,最少有数据运维人员,系统运维人员以及开发人员参与,一帮苦13程序员夜里3点多闹钟叫起来,睡眼朦胧的进行机械的操作。难度可想而知。
基于这些技术特点,可以作如下分析
业务的变化要求数据扩容的规则要尽可能的自定义,可以有些预先定义好的规则模型,但不能强制要求业务必须走定义好的模型。
自动扩容,意义不大,如果只是让业务人员根据数据点个确定,是最容易被接受的扩容模式
要尽可能的避免扩容本身对业务本身带来的影响,同时要尽可能减轻开发人员的熬夜次数。
所以我们设计了如下的系统,他满足以下特性
规则完全自定义,你可以随便写任何的ifelse等脚本代码。
只对扩容需求提供决策支持和方案生成,但决策由人进行。
除了决策,其余全部自动化。
这套系统就是我们的自动扩容系统。
因为我们将要在Q1~Q2完全开源目前淘宝在300~400个系统中所使用的所有中间件,包括rpc调用框架,消息系统以及数据库切分中间件,所以我的介绍本身将是对实现思路与细节的描述,无保留。
不过在这里,让我悬念留给下一篇(喂喂喂,难道不是文章太长导致的么?嘿嘿),在下一篇,我们将仔细介绍一下TDDL的规则引擎系统。
http://qing.weibo.com/1765738567/693f084733000bxj.html 上一篇
在上一章中,我们主要介绍了规则引擎中最重要的一个部分,自动扩容,在今天的章节,我们主要还是介绍一下我们在淘宝TDDL中的工程实践吧。
首先从原理开始吧。
先来一张图
这张图以前也出现过,我们在里面着重介绍了规则引擎
规则引擎是什么呢?
对应在上述例子里面,其实就是DBNum = pk % 3 这个规则。
他的变化可能很多,比如对于一致性hash则变为一个if - else 的表达式(见前面)
也可能有其他的变化。
所以,我们要回归本源,问一个问题,什么是规则引擎?
抽象来看,规则引擎在做的事情是,根据一组输入条件(例如主键id,或者用户id+时间,或者一个rowKey),进行了一种计算,然后返回在某个机器某个表上执行的结果。这种计算要保证,在规则本身不发生变动的情况下,同一组输入条件,返回的永远是相同的结果。
想想这种描述像什么?:-) 我个人认为很像函数的定义,那么让我们换一下表述方式吧:
假设输入数据为x(主键id,用户id_时间,或者rowKey) ,经过运算F,返回了该数据在某台机器上这个结果y.那么表达式就是
y = F(x)
这是第一层抽象,为了方便表述,我们后面都以这种方式进行表述。
这种规则引擎,在几乎所有“有状态”的数据存储中都会用到,在我们的工程实践中,我们发现这套引擎需要非常灵活的表现能力,才能适应不同用户的不同需求,比如有些场景中,业务方会给出一批经过数据分析以后的大卖家,他们固定的就拥有大量数据,会对其他人造成影响,这时候规则引擎必须能够对各种不同的场景进行适应。
因为规则能够决定数据的分布是否均匀,因此规则是整套系统中最重要的核心组件。
有了规则引擎,我们要追寻的下一个目标就是,如何能够在尽量少的影响业务的正常使用的前提下,改变规则,以达到均衡访问或扩容的目标。
要达到这个规则,第一个需要做的事情就是要能够分辨,哪些数据应该被移动,以及从哪个源头移动到哪个目标去。
要解决这个问题,在当时能够想到的方法有两个,一个是定死的规则,比如一致性hash,一致性hash,因为规则本身的入参是定死的,输出也是定死的,所以可以知道从哪里移动到哪里。但这也会带来问题,因为有些业务根本不是使用一致性hash来完成的,他们可能有自定义的函数(如:如果卖家id=2000,那么走特殊的机器) 。
一旦有这样的自定义函数,那么就很难通过分析规则来获取需要迁移的数据是哪些以及应该从哪里移动到哪里这些属性了。
于是,就必须有另外的方法。
我们采取的方案,是完全放开F,采取多版本的方式来获得“哪些数据应该被移动,以及从哪个源头移动到哪个目标去”,这两个信息。
原理如下:
我们假设有老规则 F0 ,以及新规则F1.
对于相同的输入X,我们能得到两个y,也即
y0 = F0(x) 以及y1 = F1(x)
对两个y进行比较(compare) ,能够获取两种结果: 结果1 : y0 == y1. 结果2 : y0 != y1.
思考这两种结果的含义,不难明白其中的含义:
如果y0 == y1,那么意味着,对于相同的数据x,在老规则和新规则中,数据都在同一个库的同一张表上(y相同),这条数据在老规则换为新规则的时候是不需要移动的。
而,如果y0 != y1,那么意味着,这条数据,如果将规则从F0换为F1,则数据需要被移动,移动的方向应该是从y0到y1.
这样,我们就很轻松的使用多版本的方式,获得了“哪些数据应该被移动,以及从哪个源头移动到哪个目标去”,这两个信息。
最后,在知道了上面的两个关键的信息后,还需要一套东西来帮用户把数据尽可能平滑的从一个源机器中移动到目标机器中。
这就是我们在平衡迁移中进行的思考,如果有想探讨的欢迎一起参与。
下面,我们进入工程实践,来看一下我们的规则引擎在做的事情吧。
角色介绍
对于规则引擎,它实现了如下特性:
多版本支持
只有支持多版本,才能够方便的知道哪些数据应该从哪里移动到哪里去。
枚举支持
用来支持用户按照日期进行切分,但需要注意的是,这里的日期切分不是传统意义上B树模型的那种切分方式,原因见后续分析。
内建多种切分函数支持
允许方便的直接使用内置定义的一致性hash,虚拟节点hash等函数方法,减少代码量。
与规则引擎配套的,还有一套我们目前叫做“大禹”的项目工程,他主要完成了以下几件事:
切分数据收集
能够协助收集用户切分后的数据状态,如访问热点情况,硬件情况等。
决策支持
能够帮助用户定义新的扩容策略,但我们不做“自动化扩容”,因为扩容本身不是常态。
自动迁移
能够根据用户的多版本规则,协助用户自动化的进行规则迁移,最终能够将数据迁移导致的不可用时间降低到深夜1分钟内,基本不造成影响。
工程实践描述
在我们的工程实践中,我们选择了groovy来实现java的规则引擎,使用javaScript来实现跨平台的规则引擎。
从规则引擎来看,他只需要一个引擎,能够运行一个函数就可以了,所以上述平台都可以满足我们的需求,从速度角度考虑,我们选择了可静态编译的groovy和js v8引擎。
在这个引擎之上,我们对引擎进行了包装,针对淘宝的特殊需求进行了二次开发:
在淘宝,有很大一批数据是需要按照多个条件进行切分的,如,按照用户切库后,按照时间切表等,针对这种需求,我们要扩展原来的函数定义,允许用户使用类似table+"_"+ #userid# % 1024 +"_" + dayofmonth(#gmt#);
这样的方式来拼装类似table_0001_23这样的表后缀.实现多维度的切分。
同时,还需要满足用户的范围查询需求,如,返回一个用户在某个时间段内的所有数据。这往往意味着可能要遍历多个分表的需求,针对这种需求,我们允许用户使用表达式的方式填入y = F(x)中的'x' ,如 'x' = (gmt <= now() ) and (gmt >'2012-01-01' ) 这样的输入参数。
针对这样的参数,传统的解决方案是使用排序后的树形结构来满足查询(如hbase),我们认为,因为数据节点的个数本身是有限的,我们没有必要维持复杂的数据结构,只需要使用枚举的方式就可以达到类似的效果,因为颗粒度可控。
对于大禹工程
排开数据收集以及分析后展现之外,最重要的部分无疑是能够根据多版本的规则进行自动化的扩容和迁移这一块了。
他的主要流程如下:
举个例子来说明这个流程
从整体来看,大禹在做的事情就是,全量迁移所有需要移动的数据,然后将在全量过程中产生的增量数据append到新节点上,然后部分停写1分钟,推送规则的新版本。完成迁移。
我们假定原来有一台机器,里面有两条记录:
row a : id = 0 ,name = "a"
row b : id = 1 ,name = "b"
切分的规则为 id % 1 ,
那么我们根据表达式 y0 = id % 1 ,分别将id(row a) = 0 ;id(row b) = 1代入表达式,得到y0(row a) = 0; y0(row b) = 0;
这两个结果。
然后,我们要将机器扩容为两台,
这时候规则变为 y1 = id % 2,分别将id(row a) = 0 ;id(row b) = 1代入表达式,得到y0(row a) = 0; y0(row b) = 1;
这时候,用户新写入了一条数据row c : id = 3 , name="c"
因为用户在使用老规则写入,所以使用老规则后,数据应该通过老规则计算出结果y0(row c) = id % 1 = 0;
在按老规则写入后,数据就已经可见了,这时候,大禹会读取这条记录,按照新规则进行计算,y1(row c) = id % 2 = 1; 因为1 != 0,所以row c 需要进行迁移,迁移目标是从0机器--> 1机器。
这时候大禹会将这条数据保存在本地磁盘中。
而如果假定row d 通过新老规则计算出的结果y0 (row c) == y1 ( row c) 则该数据会被大禹增量复制组件丢弃,因为数据在规则变动后不需要移动位置。
在增量开启后,会进行全量的迁移。
全量的过程与增量类似,是按照选择条件,将老机器内的指定数据遍历一次,对每一条记录,进行老规则和新规则的计算,如果计算结果相同则丢弃,计算结果不同,则将数据写入新规则算出后的结果。
当全量结束后,大禹增量复制组件会将记录在本地磁盘中的增量数据覆盖到全量后的数据上,并且继续随着新的数据产生,将数据双写在老规则和新规则所对应的机器上。并发出catch up的状态指令。
在catch up后,我们可以认为,老规则内的数据和新规则内的数据,是异步一致的,中间的数据延迟是异步复制的延迟,一般来说在几百个毫秒内。
这时候,就可以选择一个合适的时机,比如夜里5点,进行部分停写,等待新老数据绝对一致以后,发布新规则。完成迁移。
整个迁移过程,只有最后的“部分停写,等待新老数据绝对一致以后,发布新规则。完成迁移“ 是会影响业务应用的,这之前的所有过程都是个外加过程,对业务完全没有影响,就算异常失败了,也可以全部放弃掉以后重新来过,这就保证了整套逻辑的尽可能简单清晰。
好的软件就是少做不该做的事情的软件嘛 :)
以上是好的地方,下面来自暴家丑,说说不足。
规则引擎所面向的目标,其实是有状态数据的节点管理,对于节点管理来说,大家的追求一般都是有共识的,也就是说,可以按照需求,随便的增加或减少节点。但遗憾的是,目前在我们的工程实践中,目前还没能很好的解决“随便”这个需求。
所谓随便,就是指可以达到这样一个效果,某天某个监控人员,发现某些数据突然的成为热点了,那么它可以快速反应,点个按钮,上线100台机器,立刻load下降,保证了系统稳定。然后呢,发现某个集群load很低,就点个按钮,下线100台机器作战略储备。
可惜,这样的事情在有状态的机器中是很难做到的,原因很简单,有状态节点的数据迁移是需要成本的,而且成本不小,这也是为什么foursquare会挂的原因。
以上,就是我对淘宝TDDL 数据库切分tool kits中规则引擎和配套的自动扩容组件的介绍了。
目前淘宝的TDDL组件被广泛的使用在淘宝300多个不同的业务系统中,并且没有使用过强制命令进行推广。
在未来的一个Q内,我们会逐渐的开源我们目前的这套工程实践产品,希望有更多的人能够受益。
这一次,我们来讲讲数据安全和读写高可用
好吧,那么我们也就进入这个领域,来看看这数据安全所代表的一切。
在20年以前,数据安全对于大部分用户来说,只意味着数据库ACID中的”D”,数据写入到数据库,并返回成功后,这个数据也就是安全的了,在老师教给我们的计算机原理课上,似乎最多也就讲到,数据库有冷备份,也有热备份,因此写入数据库内的数据是安全的。
然而,真的如此么?
最简单的问题,就是,如果这台机器的硬盘挂掉,那应该怎么办呢?
于是,有些人就想到,那我们用多块硬盘来备份不就好了?于是,RAID技术就应运而生了。Raid技术的核心,就是利用磁盘阵列的手段,提升数据的写入效率和安全性。
那么,raid也不是完美的,首先,磁盘放在机器上,这机器的磁盘就不可能无限增加的。单机的磁盘容量受到磁盘架个数的限制。
于是,有一些人就想到:那我们就专门的设计一种可以挂无数磁盘的柜子来好了。这就是盘柜技术的产生,关键词 SAN,不过这东西不是我们要讨论的东西,所以我们就不在这里细说了。
但因为盘柜技术本身有其优势也有一定的局限性,所以目前这套东西不大为人所知了。
似乎所有人谈起存储必谈GFS,HDFS…但其实个人认为在原教旨主义的文件系统上,更多的依靠硬件的盘柜,也不失为一套非常好的解决方案。
正所谓分久必合合久必分,目前Oracle携ExtraData 一体机技术在市场上杀的风生水起,不也是盘柜技术的新时代体现么?呵呵。。
好了,废话不多扯,盘柜在目前不大容易成为主流,主要原因是
1. 冗余容错性不好
2. 价格较贵
3. 核心技术把持在大公司手里
Etc.
那么,既然盘柜技术不大容易成为主流,那么目前的主流是什么呢?
这就是多机的同步技术,利用廉价的tcp/ip网络,将多台pc server联系到一起,使用软件逻辑而非硬件逻辑来进行数据的多磁盘备份。
这其实就已经涉及到了问题的核心:什么是数据安全和数据高可用?我们将数据安全的级别从低到高,做成表格列在下面。
可见,从目前来看,解决数据安全的唯一办法是将数据同步的写到多个不同的地方(可以是用raid5的方式写,也可以用raid10的方式,不过核心都是一个--- 冗余。
而且不能简单的就用单机的冗余,必须要多机冗余才靠得住。
如果要最安全,那么数据就要同步的复制多机。
下面,我们就专注于这同步复制多机的case,来看看目前我所知的几种常见的,以解决高可用为基础的数据冗余方案吧。
需要强调的是,这些方案本身,没有绝对的一家独大之说,每一种方案,都有其自己的特性和适用场景,所以不存在某一种方案一定比其他方案好的这种说法,每一种模式都有其自己的优势和劣势。
所谓可用性,就是尽可能的保证,无论发生什么变故,数据库都能够正常的提供读写访问的这种方式。
而所谓安全性,就是尽可能的保证,无论发生什么变故,数据库内,持久化的数据都不会丢失。
这里的数据丢失,其实是个很宽泛的词汇,为了表达的更为清楚,我们需要仔细的描述一个最关键的问题:什么时候能够叫做“数据写入成功”,换句话说,也就是,数据存储系统,与前端的无状态调用者之间的承诺关系是什么呢?
认清这个问题,对于我们定义数据安全,至关重要。
从一个请求走到网络,提交给存储系统的过程,细化下来可以认为是以下几个动作的分解:
可以认为,在时间线上来说,所有的这些操作都可能出现!异常!,导致写失败。这里又涉及到以前我们提到的网络三种反馈状态问题了,让我分阶段来进行讨论:
A. 客户端发起请求出现失败 -> 客户端明确的知道自己失败,所以所有操作都未进行,对整个系统的一致性没有影响。
B. 发起请求后,因为网络因素,导致请求未被server接受 -> 客户端等待,直到超时,但Server未接受请求。客户端应认为失败。
C. Server端接受到写入请求,但内部操作失败-> Server端应该保证操作的原子性,并反馈client操作失败。
D. Server端一系列操作成功,反馈Client,但因为网络异常导致Client无法接收请求 ->Server端成功,但Client端等待超时,则client端对操作有疑问。
E. Client端收到Server端反馈的成功 -> 认为成功。
可以认为,只有最后一步成功的时候,才算做成功,而其他操作,因为网络因素导致的异常,都认为是失败的。
从上面的分析中可以看出,存储对于完整性的保证,只存在于E步骤成功时。
而client端能明确的知道操作失败的,是A,,C场景。
Client端不能明确知道操作成功或失败的,是B,D,场景,需要人工验证,或默认丢超时异常,并被认为是失败。
而我们所定义的数据安全性,就是指,当E完成时,也就是Server对client端反馈成功时,则数据在各种变故出现时,所能保证的完整性的一种体现( T_T ..真绕。。。)
而我们要在后面,花费大量篇幅来讨论的,就是“进行一系列操作处理”这个过程,如何能保证数据的完整性的问题。
因为篇幅的关系,今次就介绍第一种,也是最简单的一种,使用异步复制队列的方式来提升可用性和安全性吧。
这是所有数据存储中基本都提供的一种模式,而大部分的其他方案的核心和基础都是这个异步的复制的模型的权衡版本。所以,我们就从这里开始。
要讲清这个问题,我们先来看一张图
所谓异步传输,简单来说就是数据写入主机后,不等待其他机器反馈结果请求,直接反馈用户写入成功的一种策略。
好处:
数据写入速度快(因为只需要保证一台机器写成功,那么就算成功了)。
副作用:
如果主机写了位置102,但备机还没来得及收102的数据到备机,这时候主机down机。数据实际上还未写入备机呢。于是就出现了数据丢失。
好,就到这里。。下次我们来讨论,在这种情况下,如何能够保证可用性。
罪过啊。。竟然发现没有写行列存的关系。。
行存/基于行的存储
一个数据块内的数据类似上面图除了蓝色以外的部分的顺序排列,一般来说会在block头记录一个block有几行,每行的偏移量是多少,如果超过一个block,需要用extend结构扔到块的外面去。
优势是一次写全部数据,一次取一行全部的数据。在事务操作中,访问控制相对的比较容易做一些。另外因为大部分情况下对编号列不做压缩处理,所以更新效率较高。
列存/基于列的存储
一个block内存放的数据是
这样的顺序数据,因为这样存储数据,所有数据的类型是一样的,所以对数字类型可以做差量压缩,比一般压缩效率要好很多,在block内一般会额外描述在当前这个block内的列的一些信息,比如sum / count/最大/最小。用以方便在不解压缩的情况下进行快速计算。
优势其实很明显,对数字可以做差值压缩,取单个列的count sum的时候速度很快。
但如果做了压缩,那么不适于做频繁的update操作。代价较大。
加锁可能比行的要复杂一些。不过也没有本质区别。
这种存储方式就是json/pb/常用的方式,数据以key-value对的方式存放在里面。所有的数据都是以extends的方式进行记录。优势是可以随意增加,减少行的数量(如果你用行存,意味着所有行都必须有值,哪怕1000000个数据里只有一个有这个列。也需要占用较多空间,如果用这种方式,就不会有问题。
代价是所有的数据都要存schema.一种常见的优化是额外的有个map,映射schama->bit。这样可以通过两次映射的方式(schema->bit 。 bit->数据) 来减少在一行内的重复的冗余schema信息。文档类数据库的常用组织形式。mongodb是在这个的基础上额外的做了索引。所以其实就是个行存储。只是能schema free.对于初期建模有一定帮助。
还有一种常见的,针对行存的优化方式,也很巧妙。oracle exadata在使用
因为数据是按照block取的,所以oracle允许你按照block取数据,但block内的数据按照列存的方式组织。
这样可以在一个block内存放更多的压缩后的数据,提升空间利用率。是个很好的想法。。
很久木有和大家见面了,因为博主也需要时间来沉淀。。博主也需要学习和思考。。
好吧,不多废话,进入正题,今天我们谈的东西是一致性和安全性。
一致性这个问题,非常绕,想用语言表述,难度很大,我给别人去讲的时候,一般都是白板,因为白板有类似“动画”的效果,能够帮助别人理解,但使用文字,就没有办法了,只好要求各位有一定的抽象思维能力,能在自己的脑袋里模拟这种动画吧:)
主要会聊到: 简单的双机两阶段提交,三阶段提交,vector clock ,paxos思路,paxos改进思路,既然要阐述问题,那我们就需要先给自己画个框框,首先来对这个问题做一个定义。
============
写在前面,发现不少读者都会用自己以前的2pc知识来套用文章里面提到的2pc改。
我想说的是,在这里的两段提交,与大家所理解的两段提交,有一定区别。
他是为了满足文中提到的C问题而改过的2pc.所以能够解决一些问题,但也会产生一些新的问题。各位一定要放弃过去对2pc的理解,一步一步的跟着文中的思路走下来,你就会发现他其实不是真正事务中所用到的2pc,而是专门为了同步和高可用改过的2pc协议。
============
问题描述
人们在计算机上面,经常会碰到这样一个需求:如何能够保证一个数据写入到某台或某组机器上,并且计算机返回成功,那么无论机器是否掉电,都能够保证数据不会丢失,并且能够保证数据按照我写入的顺序排列呢?
面对这个问题,一般人的最常见思路就是:每次写都必须保证磁盘写成功才算成功就好了嘛。
没错,这就是单机一致性的最好诠释,每次写入,都落一次磁盘,就可以保证在单机的数据安全了。
那么有人问了,硬盘坏了怎么办?不是还丢了么?没错啊,所以又引入了一种技术,叫做磁盘阵列(这东西,在我们目前的这个一致性里面不去表述,感兴趣的同学可以自己去研究一下)
好像说到这,一致性就说完了嘛。。无非也就是保证每次成功的写入,数据不会丢失,并且按照写入的顺序排列,就可以保证数据的一致性了。
别急,我们这才要进入正题:
如果给你更多的机器,你能做到更安全么?
那么,我们来看看,有了更多机器,我们能做到什么?
以前,单机的时候,这台机器挂了,服务也就终止了,没有任何方式能够保证在这台机器断电或挂了的时候,他还能服务不是?但如果有更多的机器,那么你就会忽然发现,一台机器挂了,不是还有其他机器么?一个机房里面的所有机器都挂了,不是还有其他机房么?美国被核武器爆菊了以后,不是还有中国的机房么?地球被火星来客毁灭了,不是还有火星机房么?
哈,排比了这么多,其实就是想说明,在机器多了以后,人们就可以额外的追求更多的东西了,这东西就是服务的可用性,无论如何,只要有钱,有网络,服务就可用。怎么样?吸引人吧?(不过,可用性这个词在CAP理论里面,不只是指服务可以被访问,还有个很扯淡的属性是延迟,因为延迟这个属性很难被量化定义,所以我一般认为CAP是比较扯淡的。。。)
好,我们现在就来重新定义一下我们要研究的问题:
寻求一种能够保证,在给定多台计算机,并且他们相互之间由网络相互连通,中间的数据没有拜占庭将军问题(数据不会被伪造)的前提下,能够做到以下两个特性的方法:
1)数据每次成功的写入,数据不会丢失,并且按照写入的顺序排列
2)给定安全级别(美国被爆菊?火星人入侵?),保证服务可用性,并尽可能减少机器的消耗。
我们把这个问题简写为C问题,里面有两个子问题C1,C2.
为了阐述一下C问题,我们需要先准备一个基础知识,这知识如此重要而简单,以至于将伴随着每一个分布式问题而出现(以前,也说过这个问题的哦..:) )
假定有两个人,李雷和韩梅梅,假定,李雷让韩梅梅去把隔壁班的电灯关掉,这时候,韩梅梅可能有以下几种反馈:
1)"好了,关了"(成功)
2)"开关坏了,没法关"(失败)
3)
呵呵,3是什么?韩梅梅被外星人劫持了,消失了。。于是,反馈也没有了。。(无反馈)
这是一切网络传递问题的核心,请好好理解哈。。。
--------------准备结束,进入正题---------------------
两段提交改:
首先,我们来看一种最容易想到的方式,2pc变种协议。
如果我有两台机器,那么如何能够保证两台机器C问题呢?
我们假定A是协调者,那么A将某个事件通知给B,B会有以下几种反馈:
1.成功,这个可以不表。正常状态
2.失败,这个是第二概率出现的事件,比如硬盘满了?内存满了?不符合某些条件?为了解决这个情况,所以我们必须让A多一个步骤,准备,准备意味着如果B失败,那么A也自然不应该继续进行,应该将A的所有已经做得修改回滚,然后通知客户端:错误啦。
因此,我们为了能做到能够让A应付B失败的这个情况,需要将同步协议设计为:
PrepareA -> Commit B -> Commit A.
使用这个协议,就可以保证B就算出现了某些异常情况,数据还能够回滚。
我们再看一些异常情况,因为总共就三个步骤,所以很容易可以枚举所有可能出现的问题:
我们将最恶心的一种情况排除掉,因为网络无反馈导致的问题,看看其他问题。
PA ->C B(b机器挂掉): 也就是说,如果在Commit B这个步骤失败,这时候可以很容易的通过直接回滚在A的修改,并返回前端异常,来满足一致性问题,但可用性有所丧失,因为这次写入是失败的。
在这时的可用性呢? B机器挂掉,对A来说,应该允许提交继续进行。这样才能保证服务可用,否则,只要有任意的一个机器挂掉,整个集群就不可用,这肯定是不符合预期的嘛。
PA -> C B -> C A(A机器挂掉) :这种情况下,Commit A步骤失败,应该做的事情是,在A这个机器重新恢复后,因为自己的状态是P A,所以他必须询问B机器,你提交了没有啊?如果B机器回答:我提交成功了,那么A机器也必须将自己的数据也做提交操作,就能达到一致。
在可用性上面,一台机器挂掉,另外一台还是可以用的,那么,自然而然的想法是,去另外一台机器上做尝试。
从上面可以看到,因为B机器已经提交了这条记录,所以数据已经是最新了,可以基于最新数据做新的提交和其他操作,是安全的。
怎么样?觉得绕不绕?不过还没完呢,我们来看看2pc改的死穴在哪里。。
还记得刚开始的时候,我们提到了排除掉了一种最恶心的情况,这就是网络上最臭名昭著的问题,无反馈啊无反馈。。
无反馈这个情况,在2pc改中只会在一个地方出现,因为只有一次网络传输过程:
A把自己的状态设置为prepare,然后传递消息给B机器,让B机器做提交操作,然后B反馈A结果。这是唯一的一次网络调用。
那么,这无反馈意味着什么呢?
1.B成功提交
2.B 失败(机器挂掉应该被归类于此)
3.网络断开
更准确的来说,其实从A机器的角度来看这件事,有两类事情是无法区分出来的:
1)B机器是挂掉了呢?还是只是网络断掉了?
2)要求B做的操作,是成功了呢?还是失败了呢?
不要小看这两种情况。。。他意味着两个悲剧的产生。
首先,一致性上就出现了问题,无反馈的情况下,无法区分成功还是失败了,于是最安全和保险的方式,就是等着。。。没错,你没看错,就是死等。等到B给个反馈。。。这种在可用性上基本上是0分了。。无论你有多少机器,死等总不是个办法。。
然后,可用性也出现了个问题,我们来看看这个著名的“脑裂”问题吧:
A得不到B的反馈,又为了保证自己的可用性,唯一的选择就只好像【P A ->C B(b机器挂掉):】这里面所提到的方法一样:等待一段时间,超时以后,认为B机器挂掉了。于是自己继续接收新的请求,而不再尝试同步给B。又因为可用性指标是如此重要,所以这基本成为了在这种情况下的必然选择,然而,这个选择会带来更大的问题,左脑和右脑被分开了!
为什么?我们假定A所在的机房有一组client,叫做client in A. B 机房有一组client 叫做client in B。开始,A是主机,整个结构worked well.
一旦发生断网
在这种情况下,A无法给B传递信息,为了可用性,只好认为B挂掉了。允许所有client in A 提交请求到自己,不再尝试同步给B.而B与A的心跳也因为断网而中断,他也无法知道,A到底是挂掉了呢?还是只是网络断了,但为了可用性,只好也把自己设置为主机,允许所有client in B写入数据。于是。。出现了两个主机。。。脑裂。
这就是两段提交问题解决了什么,以及面临了什么困境。
碰到问题,就要去解决,所以,针对一致性问题上的那个“死等”的萌呆属性,有人提出了三段提交协议,使用增加的一段提交来减少这种死等的情况。不过3PC基本上没有人在用,因为有其他协议可以做到更多的特性的同时又解决了死等的问题,所以3pc我们在这里就不表了。3pc是无法解决脑裂问题的,所以更多的人把3pc当做发展过程中的一颗路旁的小石头。。
而针对脑裂,最简单的解决问题的方法,就是引入第三视点,observer。
既然两个人之间,直接通过网络无法区分出对方是不是挂掉了,那么,放另外一台机器在第三个机房,如果真的碰到无响应的时候,就去问问observer:对方活着没有啊?就可以防止脑裂问题了。但这种方法是无法解决一致性问题中的死等问题的。。。
所以,最容易想到的方式就是,3pc+observer,完美解决双机一致性和安全性问题。
后记3317字。nnd我本来以为可以5篇儿纸说完这个问题的。。现在发现刚阐述了很小一部分。。果然一致性和可用性真不是个简单的问题。今天到这,这个做个专题吧。
-------三段提交改-----------
回顾上文,我们已经提到了,在两段提交协议里面有个“死等”的过程,那么我们来看看三段提交协议是怎么解决这个问题的,需要注意的是,3pc只是解决了死等问题,对脑裂没有贡献。用的也不多,我们只把它当做路边的小石头,理解了作为模型的一种,参考一下就行了。
首先分析原因,死等的关键,在于B机器挂了,A机器没有收到B机器的反馈。这时候A不知道应该怎么办,所以只能死等。否则都可能造成不一致。
在这里,我要着重强调一下,3pc的假定是,你能确切的知道B是机器挂掉了,不是网络断开了(虽然在A看来,它无法区分这两种情况,我们在开始已经提到)。
3pc解决B在commit B这个阶段挂掉方法,是做两次通信。
增加了一次通信,叫pre commit,【我们这里,因为B是跟随A的,不会在B出现写读冲突或者写写冲突,所以我们也可以减少B的一个prepare(canCommit)状态,我们把它叫做3pc!改!】
下面,让我来说明一下这个问题:
为了解决死等的问题,我们只有一种选择,就是让每次请求都有一个超时时间。也就意味着,每次请求都要有个计时器,计算多少时间以后,这个请求就算是超时了。
但是,光有超时是不够的,在上一篇文章里面也提到,在2pc改中,请求超时可能意味着B提交成功了,或者B提交失败了。这时候A是无所适从的,提交也不是,不提交也不是。
最简单的一种思路,其实大家经常用到:
大家还记得在有些影片里面,某大boss要参加一场鸿门宴,于是跟下面的将军说:“我现在要去参加鸿门宴,不一定能回来,我们约定一下,如果今天晚上10点我还没回来,你就给我带军队平了他们,给我报仇。”
这其实就是三段提交核心思想的真实写照,问题的关键就是约定延迟某时间后,最终双方就按照某种"先期约定"进行后面的操作。(在这个case,先期约定是对方出老千,我们出兵跟他拼命。在3pc内的先期约定是双方都算作提交成功)。
那么,在这延迟的时间内,其实是数据状态是混沌的(或者说量子态的?笑),10点前,这boss是死了啊?还是活着?没人知道,这种混沌只有被揭开盖子的那个时刻(发送doCommit(),doAbrot(),boss派人你双方和解了,boss派人告诉你快来救我),才会变成决定论的。
好,我们回到3pc上来。
在第一次通信(pre commit)的时候,A和B的先期约定是如果某个时间后两方都没有后续反馈,那么算作提交成功。这里需要注意的是,没有后续反馈的原因,在3pc理论里只是指B挂掉这种情况,【而不是】指网络出现问题这个情况。
这样,如果A机器发送pre commit这条信息,能够拿到这么几个反馈:
1.成功
2.失败
3.超时【超时的原因是B挂掉,不是网络断开】
而B呢?在收到precommit 这条信息之后,能够发送给A几种反馈
1.成功
2.失败
【这里要非常注意,是"没有"超时这个实际会发生的选项的,因为3pc协议是排除了网络超时这种情况的】
所以B的策略很简单,如果pre commit我反馈了成功给A,那么如果我发现在等待超时之后仍然没有获得A的提交或终止请求,那么我就提交。反之我就终止。
这里的等待超时,我们来看看可能由哪些问题引起:
1.因为A机器挂了,无法发送最终的提交或回滚命令给B
2.因为B机器挂了,没有收到A机器的提交或回滚命令。
再来看A这台协调机:
如果preCommit 得到了失败或超时的结果,那么我们可以立刻发送abort命令给所有人,这个命令可能有两种结果:
abort成功,那么整个事物回滚
abort失败,意味着B机器超时,已经提交。那么A机器也只能提交。
如果preCommit成功,那么为了加速提交过程,可以再发送一条commit命令给所有人,加速提交过程。
以上的过程就是3pc的核心思路,各位看官可以根据这个思路,去推断和补充这套协议的其他内容,我就不再细说了,因为其他推论如果不是真正要去实现,意义不大,关键是核心思路。
然后,我们来看看3pc的问题,其实,从我们讨论的过程中多次出现的东西,大家就能很容易的看出问题所在,他的假定实际上不是真实的双机或多机一致性场景,在这种场景里面,网络无响应也可能意味着网络隔离断开这种情况。但这种情况在3pc内是没有被考虑的。
所以也因为这样,我们只是需要了解这东西的一个简单思路就行了,不过,所有的思路在你未来的生活中,都可能会成为你解决问题的利器,所以,我还是把这种模型的核心思路写出来。给大家做个参考。
-----统一思想,做出统一决策---------解决脑裂问题
我们刚才也提到,网络上经常会因为不可抗力造成思维上的隔阂,比如A机房和B机房之间的主干路由器挂掉了啊等等情况。那么,在这种情况下,有什么办法能够在这种时候,通过一种机制来选择应该听哪一组集群的呢?
问题在一致性问题的第一篇已经分析过了,所以我就不再重复了,只重复我们要做到的事情:"给定安全级别(美国被爆菊?火星人入侵?),保证服务可用性,并尽可能减少机器的消耗。"
我们也给出了一种最简单的模型,观察者模式:
在这种模式中,A和B机房的网络如果断掉,只要他们到C机房的网络不同时断掉,那么就可以利用在机房C的观察者来协助判断谁是正确的。
这样似乎问题不就解决了嘛?你或许会这样想?呵呵,那你就错过最好玩的东西了。。
我们来看看,这是三台机器的情况,任意一台挂掉或者网络断掉,都可以保证结构不乱。
那么,如果我有10台机器,有更多机器可能“挂掉”更多网段可能断开,这时候你会怎么安排机器的角色和网络结构呢?
仔细想想,无论给你多少台机器,用observer模式,那么只要observer挂掉并且在{A}机房{B}机房的网络断开的情况下,系统会退化到脑裂问题上了。
那么,简单的思路是,给Observer加机器不就好了?问题来了,Observer如果有两台,你到底应该听谁的?这些Observer部署到哪些机房?哪个Observer是真正管事儿的?比如如果有两个Observer, {Observer C和Observer D},各自为政,那么A机房正好问了Observer c,C说你是主机吧。然后B机房问了Observer D ,D说,你是主机吧。最后还是个脑裂的结局。。
怎么样?再给你来几台机器?晕不晕?
呵呵,我也不是难为大家,而是这些情况会实际的发生在一致性和高可用的整个过程里面。
然后,我们能做的事情是什么?只好去找现实中的解决方法了。。
让我们来吐槽一下Lamport.. 不得不说,这大神是个很好的数学家,但写的Paxos论文你妹怎么就这么晦涩呢?本来很简单的一个思路,让他描述一下我是看了很久没看懂啊- -。。最后还是得看in simple才勉强弄明白。。
不过,paxos的论文文章其实也正披露了他思路的核心来源:
信使就是消息传递,也就是通过网络将消息传递给其他人的机制。类似的机制还有人通过空气传递语言给其他人,作家通过文字将情感传递给读者,这些都是消息传递。所以也会有像我们李雷,韩梅梅关电灯一样的几种反馈。
然后,Lamport大神还提到了一个重要的概念:议会。一看parliament,估计中国人就都晕了:这是嘛?我们伟大光荣正确的领路人的所有决议不都全票通过了么,也没讨论什么的。和paxos有神马关系?。
其实这就是个悲哀了。。。在古希腊城邦时代,很多决议就是议会讨论并作出决议的。
这种作出决议的方式,叫做“少数服从多数”。
当然当然,你也可以说,不是说有多数派暴政么?不是说多数派更多的是《乌合之众》么?其实这也没办法的事情,所以丘吉尔才会说:“民主并不是什么好东西,但它是我们迄今为止所能找到的最不坏的一种。”
我们就来看看,我们为什么需要少数服从多数这个原则
我之后的讨论,纯粹的是在机器和机器之间做出决策所需,在实际的zz过程中,环境远比大家想想的复杂得多,变数很大,但作为计算机,所有的东西其实都是可预期的,所以我们还是回到计算机科学领域。
假定有两个机房,A机房和B机房,A机房有5台机器,B机房有3台机器。他们的网络被物理隔离了。我们来看看我们有哪些选择:
1.A,B机房独立的都可以提供服务。
这种方式明显是不靠谱的,会出现不可逆转的不一致问题。咔嚓掉。
2.A,B机房都不可以提供服务
合理的方式,有些场景的时候会选择这种情况。但明显在高可用上面是0分,只保持了一致性而已。
3.让A机房的机器服务。
好吧。你真的认为剩下三台机器提供服务的安全性比5台的高?。。
4.让B机房的机器服务。
“民主并不是什么好东西,但它是我们迄今为止所能找到的最不坏的一种。”
这就是qurom产生的核心原因。
今天到这,晚上我们会讨论paxos的一些细节,散会~
我们已经在上面的分析中,我们已经看到observer模型在多机场景下的问题,所以,paxos模型的目标就是解决这个问题,他解决这个问题的方法就是quorum模型。
我的目标是让大家能弄明白,掌握这些复杂的概念,所以我也会将以前我在淘宝java中间件团队内分享时候,大家经常犯的一些错误,也写到【】里面,尽可能让大家少走弯路,如果有什么感想,疑问,后面可以留言。
PS 广告插播 : 淘宝java中间件团队,你值得拥有:)
----PAXOS------
好,我们回顾一下上下文,我们在上篇文章中谈到,当机器变得更多的时候Observer不能只有一个。必须有更多个Observer,但Observer多了,到底听谁的又成了问题。你一言我一语,大家都觉得自己是老大,谁也不服谁。咋办捏?
这时候就得有人站出来,说:那我们少数服从多数吧!制定一套策略,在各种情况下都能够选出一个决议不就行了!
这其实就是paxos协议的核心想法之一,我们来看一下他是怎么做到的。在这里,我不想去做那个繁琐的证明过程,那个过程如果你感兴趣,可以去看paxos made simple这篇文章,有中文,这里给出http://blog.csdn.net/sparkliang/article/details/5740882 ,数星星同学也翻译过。可以直接google.
我在这里只说结论,因为结论更容易理解一些。
我们假定有A,B,C,D,E五台机器。kv系统需要put一个数据[key=Whisper -> val=3306]到我们这5台机器上,要保证只要反馈为真,任意两台机器挂掉都不会丢失数据,并且可以保证高可用。怎么做:
1.首先,客户端随机选择一个节点,进行写入提交,这里我们随机选择了C这个节点,这时候C节点就是这次提议的发起人【也叫proposer,在老的2pc协议里也叫做coodinator】,当C收到这个提议的时候,C首先要做的事情是根据当前节点的最新全局global id,做一次自增操作,我们假定,在当时全局id,Global ID是0,所以,这个议案就被对应了一个编号,1--->[key=Whisper -> val=3306]。
【【这里有两个我们经常犯的错误,下面做一个解说:
1.global id问题,在老的论文里,Lamport没有描述这个自增id是怎么生成的,所以大家的第一个疑问一般是问id怎么生成,从我目前能够看到的所有实现里面,基本上就是选择哪一台机器,就是以那台机器当前所保持的全局id(snapshot,可能不是全局来看的最高值,但没关系,只要是自己这台机器的最高值就行了),然后做一下自增就行了。我们后面会看到协议如何保证非全局最高值的globalID提议会被拒绝以至于不能够形成决议。
2.global id --->[key=Whisper -> val=3306] . 这也是个会让人困惑的问题,在原文中,他被表示为一个key-value的形式,比如proposal[0->value] 。这会让人自然的联想到与数据库的kv相对应,key是0,value是value。然后就会困惑,这个数据是怎么和数据库对应起来的呢?这是我当时的困惑,现在也把他列在这里。其实很简单,这里的global id对应value.global id只是对paxos协议有意义,对于数据库,其实只需要关心value里面的数据即可,也即将global id --->[key=Whisper -> val=3306]里面的value: [key=Whisper-> val=3306] 作为数据库构建映射时所需要的redoLog就行了,global id的作用只是告诉你这些数据的顺序是按照global id来排列的,其他无意义。 】】
我们回到文中,我们已经将这个新的议案标记了从C这台机器看起来最大的global id : 1--->[key=Whisper -> val=3306]。然后,他会尝试将这个信息发送给其余的A,B,D,E这几台机器。
我们来看这些机器的操作流程。 在这个过程中,Paxos将A,B,D,E叫做accepter【老的协议里没有区分,管这些都叫做参与者,cohorts】,他们的行为模式如下:
如果A,B,D,E这几台机器的globalID 小于C给出的决议的GID(1--->[key=Whisper -> val=3306]),那么就告诉C,这个决议被批准了。而如果A,B,D,E这几台机器的GlobalID 大于或等于C给出决议的GID.那么就告知C 这个决议不能够被批准。
我们假定A,B两台机器当时的Max(GID)是0 ,而D,E的Max(GID)是1.那么,A,B两台机器会反馈给C说协议被接受,这时候我们算算,C的议案有几票了?A+B+!C!,一定要算自己哦:) 。所以,这个议案有三票,5台机器的半数是3.超过法定人数,于是决议就被同意了。
我们保持这个上下文,来看看D,E这边的情况。首先,要思考的问题是,为什么D,E的Max(GID)是1呢?
其实很简单,D可能在C发起决议的同时,也发起了一个决议,我们假定这个决议是由D发起的,决议是 1--->[key=taobao ->val=1234]。既然D,E的Max(GID)是1,那么意味着E已经告知D,它同意了他的决议,但D马上会发现,A,B,C里面的任意一个都返回了D不同意。他的议案只拿到两票,没有通过,它虽然有点不爽,但也是没办法的事情啊。。
这时候C的决议已经被多数派接受,所以他需要告知所有人,我的议案1--->[key=Whisper -> val=3306]已经被接受,你们去学习吧。
这时候还有一个问题是需要被考虑的,如果在C已经得知决议已经达到法定人数,在告知所有人接受之前,C挂了,应该怎么办呢?
我之所以没有将这个放到开始的描述里,主要原因是觉得这是个独立因素,不应该影响议案被接受时候的清晰度。
为了解决这个问题,需要要求所有的accepter在接受某个人提出的议案之后,额外的记录一个信息:当前accepter接受了哪个提议者的议案。
为什么要记录这个?很简单,我们看一下上面出现这个情况时候的判断标准。
A 机器:角色-accepter 。 批准的议案 1--->[key=Whisper-> val=3306] 。提议人:C
B 机器:角色-accepter 。 批准的议案 1--->[key=Whisper-> val=3306] 。提议人:C
C机器:角色-proposer 。 挂了。。不知道他的情况。
D 机器:角色-accepter 。 批准的议案 1--->[key=taobao->val=1234] 。提议人:自己
E 机器:角色-proposer。 “提议的”议案 1--->[key=taobao->val=1234] 。提议人:D。
因为有了提议人这个记录,所以在超时后很容易可以判断,议案1--->[key=Whisper -> val=3306] 是取得了多数派的议案,因为虽然D,E两台机器也是可以达成一致的议案的。但因为有个人本身是提议者,所以可以算出这个议案是少数派。
就可以知道哪一个议案应该是被接受的了。
在这之后,提议者还需要做一件事,就是告知D,E,被决定的决议已经是什么了。即可。
这个过程在文章中叫Learn. D,E被称为Learner.
别看写的简单,这个过程也是变数最大的过程,有不少方法可以减少网络传输的量,不过不在这里讨论了。
下面,我们讨论一下我们在2pc/3pc中面临的问题,在paxos里面是怎么被解决的。
2pc最主要的问题是脑裂,死等。两个问题。
对于脑裂,paxos给出的解决方案是,少数服从多数,决议发给所有人,尽一切努力送达,总有一个决议会得到多数派肯定,所以,不在纠结于某一台机器的反馈,网络无响应?没有就没有吧,其他人有反馈就行了。
所以,如果出现了机房隔离的情况,比如A,B,C在机房1,D,E在机房2,机房1和机房2物理隔离了,那么你会发现,D,E永远也不可能提出能够得到多数派同意的提案。
所以,少数派的利益被牺牲了。。换来了多数派的可用性。我们分析过,这是唯一能够既保证数据的一致性,又尽可能提高可用性的唯一方法。
而对于死等问题,解决的方法也是一样的,对于某一台机器的无响应,完全不用去管,其他机器有相应就行了,只要能拿到多数,就不怕一小撮别有用心的反对派的反攻倒算~。
---------------------------------paxos就是这样一个协议----------
休息一下
----------------------------------------------------------------------------------------
那么Paxos有没有什么值得改进的地方?有的,很简单,你会发现,如果在一个决议提议的过程中,其他决议会被否决,否决本身意味着更多的网络io,意味着更多的冲突,这些冲突都是需要额外的开销的,代价很大很大。
为了解决类似的问题,所以才会有zoo keeper对paxos协议的改进。zk的协议叫zab协议,你可以说zab协议不是paxos,但又可以说是paxos.但将paxos和zab协议之间做直接的等同关系,无疑是【错误】的。
其实,这也是在我们的现实生活中经常能够发现的,如果每个议案都要经过议会的讨论和表决,那么这个国家的决策无疑是低效的,怎么解决这个问题呢?弄个总统就行了。zab协议就是本着这个思路来改进paxos协议的。
---------paxos 改进----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。。
然后是我的个人评述,写zab协议的人否认自己的协议是paxos.变种 其实我也是有些认同的。不过,他们是针对一个问题的两种解决方法:
因为他们解决的问题的领域相同
解决网络传输无响应这个问题的方法也一样:也即不在乎一城一池的得失,尽一切努力传递给其他人,然后用少数服从多数的方式,要求网络隔离或自己挂掉的机器,在恢复可用以后,从其他主机那里学习和领会先进经验。
并且也都使用了quorum方式来防止脑裂的情况。
核心思路是类似的,但解决问题的方法完全是两套。 paxos在其他公司的实现里面也对paxos进行了这样,那样的改进。不过核心思路都是这个。
我们对paxos协议的讲解,就到这里。
也留下一个问题,zab协议,如果我们用在google 全球数据库spanner上,会不会有什么问题呢?请大家思考哈 :)
后记,抱歉,这篇文章一个图都没有。。我已经尽可能用简单的方式来描述paxos和他的变种协议了(当然有一个作为了问题)。如果有哪个地方不明白,也还请在后面留言吧。友情提示这篇文章不适于跳跃性阅读,想要理解,必须从第一行开始读到最后。。。。
google的工程师说,所有的一致性协议都是paxos的特例,我表示不置可否吧。。。。下一篇我们要讨论另外一系的实现,gossip模型的实现。我个人感觉:把gossip归类到paxos模型,似乎也不是很合适。gossip协议的两个主要的实现方式,是dynamo和cassandra.我们在下一篇里面进行讨论
--------Dynamo and Cassandra ----------
这两套系统,其实是同源的,我其实不是很愿意来说这两套系统,因为他们用的技术比较学术化,所以比较复杂一些。。Anyway ,I'll try my best !
提到这两个系统,他们在核心思路上是非常类似的,但有一些细节性的东西又有所偏重,在分布式系统中也算是独树一帜了,很有代表性的一个系列,这些不一致的地方,最明显的地方就在于一致性上。可见,哪怕是从追求简单为上的工程化实现来说,各种不同的方式实现一致性也都有很大的不同,不过他们也有一些共性和一些独树一帜的概念,下面来做一下分别解说。
先说共性:
W+R>N
相信这个大家都耳熟能详了吧?呵呵,我从其他角度来说明这件事吧。
N表示这个复制集群的总数【很多地方解释的不是很准确造成了不少误解】。
W表示写入份数
R表示一次一致性读所需要的份数(这里要注意,是随机从N中选择的机器数量哦)
这个公式表示为:如果满足W+R>N(W,R,N 属于不为负数的整数且R,W<N),那么集群的读写是强一致的。如果不满足,那么这个复制集群的读写是非强一致的。
这里我强调一下,这里的“集群”,是指数据完全相同的多份拷贝。不涉及数据切分哦:)
这个公式的最常用的用法是求R,也就是说公式应该写成N-W<R(W,R,N 属于不为负数的整数且R,W<N)。
我们举几个常见的例子:
1. 简单的主备强一致复制。
因为是强一致,所以数据一定是写够两份的,W=2.
这个集群的节点数呢?只有两台,所以N=2.
那么R>(N-W = 2 - 2 = 0).
R> 0 所以R可以取 1, 2。 不能取三,因为机器数只有2.。取不来3 :).
2. 假定有三台机器,使用quorum的方式强一致的写入数据。
因为有三台机器,所以N=3.
因为是进行quorum写入,只要写两台就算成功了,所以W=2
这时候R>(N-W=3-2=1)
所以R的取值只可能是2,3
R的取值意味着什么?意味着你必须从N=3台机器中,最少随机选择两台进行读取,才有可能读取到数据的最新值。
3. 假定有三台机器,写一台就算成功。
因为有三台机器,所以N=3.
因为是进行quorum写入,只要写一台就算成功,所以W=1.
这时候R>(N-W=3-1=2)
R的取值只可能是3。
这意味着什么?意味着如果你只写一台机器就算做成功,那么你在读取的时候需要读取3台机器,才能取得数据的最新值。
具体证明我就不列了,感兴趣自己去看一下:) . 枚举一下很容易可以得出结论的。
gossip协议
gossip协议是这两套存储的基础之一,说复杂也复杂,说不复杂也不复杂。。其实gossip就是p2p协议。他主要要做的事情是,去中心化。
怎么做到的呢?我只希望在这篇文章里给大家留下几个印象:gossip是干什么的?怎么做到的?优势劣势是什么?即可。对协议的细节感兴趣的,可以自己去深入研究。
gossip的核心目标就是去中心。
做到的方式:
根据种子文件,按照某种规则连接到一些机器,与他们建立联系,不追求全局一致性,只是将对方机器中自己没有的数据同步过来。这里就设计到如何能够快速的知道自己的数据和其他人的数据在哪些部分不一致呢?这时候就要用到Merkle tree了,它能够快速感知自己所持有的数据中哪里发生了变更。
优势:
去中心化,看看伟大的tor,只要能连到一个seed,有一个口子能出长城,那么你最终就能跳出长城。。
劣势:
一致性比较难以维持,
(这里我们介绍很简单,因为我也没有实际的写过。。。如果谁有这方面经验欢迎补充)
不同选择:
Dynamo : vector clock vs. Cassandratimestamp.
这两个协议的目标都是解决一致性的。也是我们要说的重点。
我们先来说说Vector clock:
提到vector clock ,不能不提Lamport的另一篇论文Time Clocks and the Ordering of Events in a Distributed System(中文翻译http://t.cn/zlEwziN)
这篇文章核心讲的是多进程之间的互斥和排队问题。不过这不是我们主要的要吸收的,在这篇文章中,更多的能够让你意识到一个问题:原来你跑到了一个相对论的世界里。也即,在进程之间没有消息相互通知的时候,他们就是各自为政的,遵循着自己的时钟的。只有在当他们之间有了相互之间的消息传递的时候,才有可能创造一个全局时间序出来。
vector clock 给我的感觉,就正是沿着这条路子思考下去得出来的一种方式。如果要我一定有一个现实生活的类比的话,我想说,vector clock给我的感觉更像git .
让我们从他与paxos的比较上面开始吧。
在paxos里面,我们使用quorum和类三阶段提交的方式来保证数据提议是顺序的,一次只会有一个提议被接受。
这样在一个场景下效率不是最高的:如果我们假定,大部分场景更新的数据都不重复,那么效率就不会高了。
比如,如果我们不断地往一个kv里面进行以下操作:
{查看A够不够100块?如果够,减少100块}
{查看A够不够100块?如果够,减少100块}
{查看A够不够100块?如果够,减少100块}
...
如果不断地塞入这种数据,那么实际上每次的写入都依托了上一次数据的值。这种操作是必须排队才会高效的,否则会出现超减的情况的。
但如果我们的操作只是我们不断地往一个kv里面进行以下操作:
A登陆了
B登陆了
C登陆了
D登陆了
E登陆了
F登陆了
那么可以认为,所有的数据之间都是“相互没有关系的”, 这时候,再让这些写入全部排队一次,代价明显就高了。
我理解的Dynamo和Cassandra,他们的场景主要是适合后面的这种方式,也即数据之间冲突很小的情况。在这种情况下,维持一个全局有序的队列的效率太低了,不如这种分散式的方式。
但冲突的概率小,并不意味着没有冲突,所以,还是需要有一套机制,能够帮你感知哪些数据出现了冲突,允许你进行冲突处理的。
而在这个问题上dynamo和Cassandra选择了不同的道路。
dynamo选择了vector clock .
他的主要方式是:在数据传递的信息中,额外带上这数据是从哪里来的版本是多少。
我们来看看,用这种方式,如何能够知道什么时候发生了冲突:
我们假定有A,B,C三台机器。
初始的时候,A,B,C三台机器的数据sequence都是100.
这时候,我随机挑了一台机器,B写了一行记录[key=Whisper , Val=0]
这时候B生成的数据是议案[101 from B->[key=Whisper , Val=0]].
然后,又有另外一个人选择C写了另外一条记录,比如[key=Whisper , Val=10000]
这时候C生成的数据议案是[101 from C->[key=Whisper , Val=10000]]
这时候,B的数据传递给C. 因为C也有个101的议案,所以【他会保持两份议案】(请注意,这是和paxos不一致的地方哦)
所以C接受的议案是
[101号议案
{fromB [key=Whisper , Val=0]}
{fromC [key=Whisper , Val=10000]}
]
然后怎么办?然后。。。。然后你在get("Whisper")这个数据的时候,vector clock会把这两条记录都反馈给你,告诉你,冲突,你自己选择一条吧:)
那么,这样我们有几种选择,对于{count ++ 类}操作,应该将所有决议合并加到一起。
而对于其他数据,则应该按照时间戳,取时间戳最大的数据,这是最新的。
这就是vector clock的工作流程,在合并这段,让我很容易就想到svn或git中的冲突解决。。
怎么样?是否觉得思路更开阔了?欢迎大家基于paxos和vector clock再进行其他思考,一致性的研究远远没有结束。
就我个人嘛。。我更喜欢能保证顺序的一致性模型,不喜欢这种各自为政按需合并的模型。
好,说完了vector clock 我们来说说Cassandra 的Timestamp.
其实,TimeStamp模型是vector clock的劣化和简化版本。在vector clock里面,冲突是由用户处理的,系统只是帮你检查冲突,但Cassandra做的更粗暴和简单一些。他不检测冲突,所有数据只保留时间戳最大的那个。
这种模型可以应对80%场景了,模型得到了极大简化,不过我估计应该是不能做count++操作了吧?我没实际使用过。
好,回顾一下,我们在刚才讲了三个新概念: W+R>N .用来决定一致性级别。gossip协议和Merkle tree,用来进行去中心化的节点间数据同步。vector clock或timestamp.
现在是拼装时间。
dynamo : 数据同步使用了gossip+Merkletree, 使用vector clock来标记冲突数据,冲突数据会交给用户做出处理。允许通过配置,指定一组小集群有几个相同数据的Equalty Group(N).以及你要写入并保证同时成功的份数(W),以及你要读的份数(R)。
Cassanra :与dynamo类似(因为同源), 在选择上,放弃了vectorclock,使用Timestamp来进行冲突合并。其余的类似。
然后因为qingblog我看sina自己都不怎么维护了,所以转战新浪博客。http://blog.sina.com.cn/u/1765738567
看到mvcc时 http://qing.blog.sina.com.cn/1765738567/693f08473300067j.html,有一些不理解,还望详解。
A:
如果要提到MVCC,就应从他最原始的需求出发来看。
对同一个数据的访问来说,所谓的一致性和隔离性,其实就是针对以下四种情况的不同处理方式。
写写
写读
读写
读读
你可以用这四个冲突顺序,拼装出针对同一个数据的全部访问顺序。
那么如何能够限制这些请求的先后顺序呢? 一个最简单的方式就是加锁。
第一个人访问或写入数据的时候,其他人不能够写入数据。
但这样并发度明显是上不去的,于是自然要想到,有没有更快的方法呢?
那么在java里面,比加排他锁更快的方法就是加入读写锁咯。
它能够保证”读读“ 这个冲突不会相互阻塞。
然而,读写和写写这两个case,却还是要全部锁住的。
为了解决这个问题,才会有MVCC这个概念产生,对应java就是copyOnWrite .
本质是为了让”读写“和”写读“冲突而出现的一种技术,每次针对一个数据的写入,都会把原有数据复制一份出来,然后写道新的地方去。这样,这样,如果访问顺序是写读写,对于读写锁来说等于加锁三次,而对于MVCC来说只需要针对写写加锁就可以了,甚至写写都可以不加锁。对吧?
怎么做到呢? 我们假定有个全局时间戳,每次事务都加1 。 那么对于”写读写“这个顺序,第一个写的时候申请事务id 0, 第二个读因为不修改数据所以事务id 不增加,第三个写时事务id加1. 那么数据的版本是0,1. 而读数据的时候的id是0.
这样,读取的数据是0这个版本,因为已经有1这个版本了,我们就可以推断0这个版本的数据一定是完整的,而不需要关心1这个版本是否完整的写入到了节点中,从而就不需要等待版本1的完整写入了。于是,写不阻塞读,读不阻塞写。
从而,写写,写读,读写,都可以相互不阻塞,提升了系统的并发度。
理解了copyOnWrite,再来看真实的场景 中还需要解决什么问题。
为了说明这个问题,我们就需要理解什么是事务, 所谓事务,并不是指更新或读取一条记录的。 事务是在事务开始后,你可能会更新的数据的集合。比如,update table set a = 1 where a > 10 . 那么A > 10的所有数据(绝对不止一条),都需要“同时”被更改。
如果有一些计算机的基本知识,你就应该知道,计算机从本质来说还是个图灵机实现,在目前是不可能“同时”更改所有数据的,但从需求来说又要有这样的需求,于是只能通过其他方式模拟,这个模拟的方法,就只能是加锁,将所有A>10的记录都加锁,然后再更新掉。
那么这样做的时候,就不大可能使用乐观方式来做写写更新了,以我个人的理解,乐观锁的前提是,在争用不明显的场景下,因为减少了上下文切换的开销,从而可以获得性能的提升,但如果假定我们要针对一组记录做多次频繁的更新时,就要权衡,到底是上下文切换的开销大呢,还是频繁rollback并且额外消耗大量cpu空转的开销大了。
于是你就知道了,为什么mysql 目前的实现里面,只做到了读不阻塞写,写不阻塞读。写和写是相互阻塞的,原因就是因为必须加锁保证数据完整性。
----------------
针对mysql的实现方式的介绍。
为了简化模型,我们只讨论插入的情况,实际上还有个删除要考虑,不过原理类似。
在每行记录上维持一个trx_id.
每一个事务开始的时候,trx_id都会增加,事务可以是显示的setAutoCommit(false),也可以是个普通的update insert 等。
然后,他还有个当时正在进行的事务的trx_id的列表,维持在系统信息里,所有正在运行的事务都会将自己的id记录在这个列表中,等到事务提交后则从这个列表中删除掉。
而每个事务又维持了”自己的“一个正在进行的事务trx_id的列表,这个列表是系统列表的一个snapshot.
在实际运行过程中,读的时候其实mysql是用事务trx_id列表中最小的trx_id去与数据列中的记录做比较的,只有比这个在当前正在运行的snapshot中最小的trx_id还要小的数据,才允许被读取出来。
如,
全局事务id 列表是: 100,101,120,150
当前事务的id列表snapshot是 : 120,150
那么,记录中那些小于120 ,或者自己事务id所更新的记录,并且符合未被删除这个条件的的(这个原理类似,我不在这里说是减少复杂度)。 才能够被当前事务读取。
然后,就来看看MVCC能做到的两个隔离级别,读已提交RC (其他事务提交后的数据立刻能够被当前事务看到), 和可重复读RR(其他事务提交后的事务在当前事务内不可见)。
了解了原理,不难推断如何做到RC和RR
RC , 在事务内,在开始事务时更新一次当前事务的id列表snapshot,并且在每次运行了一个更新的sql后,都更新当前事务的id列表snapshot .
RR , 只在事务开始时更新当前事务的id列表snapshot 。
这就是mysql的mvcc实现方式。
我说的那个是以oracle的实现模式为模板的。。所以不大一样。 PG的实现也不一样。。不过核心思路就是我上面提到的那种。