Two-phase commit(http://en.wikipedia.org/wiki/Two-phase_commit_protocol)是分布式事务最基础的协议,Three-phase commit(http://en.wikipedia.org/wiki/Three-phase_commit_protocol)主要解决Two-phase commit中协调者宕机问题?/font>
Two-phase commit的算法实?(from <<Distributed System: Principles and Paradigms>>)Q?/font>
协调?Coordinator)Q?/font>
write START_2PC to local log;
multicast VOTE_REQUEST to all participants;
while not all votes have been collected {
wait for any incoming vote;
if timeout {
write GLOBAL_ABORT to local log;
multicast GLOBAL_ABORT to all participants;
exit;
}
record vote;
}
if all participants sent VOTE_COMMIT and coordinator votes COMMIT {
write GLOBAL_COMMIT to local log;
multicast GLOBAL_COMMIT to all participants;
} else {
write GLOBAL_ABORT to local log;
multicast GLOBAL_ABORT to all participants;
}
参与?Participants)
write INIT to local log;
wait for VOTE_REQUEST from coordinator;
if timeout {
write VOTE_ABORT to local log;
exit;
}
if participant votes COMMIT {
write VOTE_COMMIT to local log;
send VOTE_COMMIT to coordinator;
wait for DECISION from coordinator;
if timeout {
multicast DECISION_REQUEST to other participants;
wait until DECISION is received; /* remain blocked*/
write DECISION to local log;
}
if DECISION == GLOBAL_COMMIT
write GLOBAL_COMMIT to local log;
else if DECISION == GLOBAL_ABORT
write GLOBAL_ABORT to local log;
} else {
write VOTE_ABORT to local log;
send VOTE_ABORT to coordinator;
}
另外Q每个参与者维护一个线E专门处理其它参与者的DECISION_REQUESThQ处理线E流E如下:
while true {
wait until any incoming DECISION_REQUEST is received;
read most recently recorded STATE from the local log;
if STATE == GLOBAL_COMMIT
send GLOBAL_COMMIT to requesting participant;
else if STATE == INIT or STATE == GLOBAL_ABORT;
send GLOBAL_ABORT to requesting participant;
else
skip; /* participant remains blocked */
}
从上q的协调者与参与者的程可以看出Q如果所有参与者VOTE_COMMIT后协调者宕机,q个时候每个参与者都无法单独军_全局事务的最l结?GLOBAL_COMMITq是GLOBAL_ABORT)Q也无法从其它参与者获取,整个事务一直阻塞到协调者恢复;如果协调者出现类似磁盘坏q种怹性错误,该事务将成ؓ被永久遗弃的孤儿。问题的解决有如下思\Q?/p>
1. 协调者持久化数据定期备䆾。ؓ了防止协调者出现永久性错误,q是一U代h的解决ҎQ不Ҏ引入bugQ但是事务被d的时间可能特别长Q比较适合银行q种正确性高于一切的pȝ?/p>
2. Three-phase Commit。这是理Z的一U方法,实现h复杂且效率低。思\如下Q假讑֏与者机器不可能出现过一半同时宕机的情况Q如果协调者宕机,我们需要从zȝ的超q一半的参与者中得出事务的全局l果。由于不可能知道已经宕机的参与者的状态,所以引入一个新的参与者状态PRECOMMITQ参与者成功执行一个事务需要经qINIT, READY, PRECOMMITQ最后到COMMIT状态;如果臛_有一个参与者处于PRECOMMIT或者COMMITQ事务成功;如果臛_一个参与者处于INIT或者ABORTQ事务失败;如果所有的参与者都处于READY(臛_一半参与者活着)Q事务失败,即原先宕机的参与者恢复后处于PRECOMMIT状态,也会因ؓ有其它参与者处于ABORT状态而回滚。PRECOMMIT状态的引入l了宕机的参与者回滚机会,所以Three-phase commit在超q一半的参与者活着的时候是不阻塞的。不q,Three-phase Commit只能是是理Z的探索,效率低ƈ且没有解决网l分区问题?/p>
3. Paxos解决协调者单炚w题。Jim Gray和Lamport合作了一论文讲q个ҎQ很适合互联|公司的大规模集群QGoogle的Megastore事务是q样实现的,不过问题在于Paxos和Two-phase Commit都不单,需要有比较靠谱Q代码质量高Q的团队设计和~码才行。后l的blog详l阐q该Ҏ?/p>
MQ分布式事务只能是系l开发者的乌托邦式理想QTwo-phase commit的介入将D涉及多台机器的事务之间完全串行,没有代h的分布式事务是不存在的?/font>
前面我的一文?a >http://hi.baidu.com/knuthocean/blog/item/12bb9f3dea0e400abba1673c.html引用了对Google App Engine工程师关于Bigtable/Megastore replication的文章。当时留下了很多疑问Q比如:Z么Google Bigtable 是按照column familyU别而不是按行执行replication的?今天重新思考了Bigtable replication问题Q有如下体会Q?/p>
1. Bigtable/GFS的设计属于分层设计,和文件系l?数据库分层设计原理一_通过pȝ隔离解决工程上的问题。这U分层设计带来了两个问题Q一个是性能问题Q另外一个就是Replication问题。由于存储节点和服务节点可能不在一台机器,理论上L存在性能问题Q这p求我们在加蝲/q移Bigtable子表(Bigtable tablet)的时候考虑本地化因素;另外QGFS有自qreplication机制保证存储的可靠性,Bigtable通过分离服务节点和存储节点获得了很大的灵zL,且Bigtable的宕机恢复时间可以做到很短。对于很多对实时性要求不是特别高的应用Bigtable׃服务节点同时只有一个,既节U资源又避免了单炚w题。然后,Bigtable tablet服务q于灉|Dreplication做v来极其困难。比如,tablet的分裂和合ƈ机制D多个tablet(一个只写,其它只读)服务同一D范围的数据变得几乎不可能?/p>
2. Google replication分ؓ两种机制Q基于客L和基于Tablet Server。分q如下:
2-1). Z客户端的replication。这U机制比较简单,实现如下Q客L?写操作均为异步操作,每个写操作都试写两个Bigtable集群QQ何一个写成功p回用P客户端维护一个retry listQ不断重试失败的写操作。读操作发到两个集群QQ何一个集读取成功均可。然后,q样做有两个问题Q?/p>
a. 客户端不可靠Q可能因为各U问题,包括E序问题退出,retry list丢失D两个集群的数据不一_
b. 多个客户端ƈ发操作时无法保证序性。集A收到的写操作可能?DEL item; PUT item"Q集B的可能是"PUT item; DEL item"?/p>
2-2). ZTablet Server的replication。这U机制实现较为复杂,目的是ؓ了保证读服务Q写操作的g时仍然可能比较长。两个集,一个ؓ主集,提供?写服务;一个ؓslave集群Q提供只L务,两个集群l持最l一致性。对于一般的L作,量d主集,如果主集不可以讉K则读取slave集群Q对于写操作Q首先将写操作提交到主集的Tablet ServerQ主集群的Tablet Serverl护slave集群的元数据信息Qƈl护一个后台线E不断地积攒的用户表格写操作提交到slave集群q行日志回放(group commit)。对于一般的tabletq移Q操作逻辑和Bigtable论文中的完全一_主集如果发生了机器宕机Q则除了回放commit log外,q需要完成宕机的Tablet Server遗留的后台备份Q务。之所以要按照column familyU别而不是按行复Ӟ是ؓ了提高压~率从而提高备份效率。如果主集群写操作日志的压羃率大于备份数据的压羃率,则可能出现备份不及时Q待备䆾数据来多的问题?/p>
假设集群AZ集群Q集B是集A的备份,集群切换时先停止集群A的写服务Q将集群A余下的备份Q务备份到集群B后切换到集群BQ如果集A不可讉K的时间不可预知,可以选择直接切换到集BQ这样会带来一致性问题。且׃Bigtable是按列复制的Q最后写入的一些行的事务性无法保证。不q由于写操作数据q是保存在集A的,所以用户可以知道丢了哪些数据,很多应用可以通过重新执行A集群遗留的写操作q行N恢复。Google的App Engine也提供了q种查询及重做丢q写操作的工具?/p>
x不成熟,有问题联p:knuthocean@163.com
负蝲q{略
Dynamo的负载^衡取决于如何l每台机器分配虚拟节点号。由于集环境的异构性,每台物理机器包含多个虚拟节点。一般有如下两种分配节点LҎQ?/p>
1. 随机分配。每台物理节点加入时Ҏ光|情况随机分配S个Token(节点?。这U方法的负蝲q效果q是不错的,因ؓ自然界的数据大致是比较随机的Q虽然可能出现某D范围的数据特别多的情况Q如baidu, sina{域名下的网늉别多Q,但是只要切分_l,即S_大,负蝲q是比较均衡的。这个方法的问题是可控性较差,新节点加?dpȝӞ集群中的原有节点都需要扫描所有的数据从而找出属于新节点的数据,Merkle Tree也需要全部更斎ͼ另外Q增量归?备䆾变得几乎不可能?/p>
2. 数据范围{分+随机分配。ؓ了解x?的问题,首先数据的HashI间{分为Q = N * S?(N=机器个数QS=每台机器的虚拟节ҎQ,然后每台机器随机选择S个分割点作ؓToken。和Ҏ1一Pq种Ҏ的负载也比较均衡Q且每台机器都可以对属于每个范围的数据维护一个逻辑上的Merkle TreeQ新节点加入/d时只需扫描部分数据q行同步Qƈ更新q部分数据对应的逻辑Merkle TreeQ增量归档也变得单。该Ҏ的一个问题是Ҏ器规模需要做出比较合适的预估Q随着业务量的增长Q可能需要重新对数据q行划分?/p>
不管采用哪种ҎQDynamo的负载^衡效果还是值得担心的?/p>
客户端缓存及前后CQ务资源分?/strong>
客户端缓存机器信息可以减一ơ在DHT中定位目标机器的|络交互。由于客L数量不可控,q里~存采用客户端pull的方式更斎ͼDynamo中每?0s或者读/写操作发现缓存信息不一致时客户端更Cơ缓存信息?/p>
Dynamo中同步操作、写操作重试{后CQ务较多,Z不媄响正常的d服务Q需要对后台d能够使用的资源做出限制。Dynamo中维护一个资源授权系l。该pȝ整个机器的资源切分成多个片Q监?0s内的盘d响应旉Q事务超时时间及锁冲H情况,Ҏ监控信息出机器负蝲从而动态调整分配给后台d的资源片个数?/p>
Dynamo的优?/strong>
1. 设计单,l合利用P2P的各U成熟技术,模块划分好,代码复用E度高?/p>
2. 分布式逻辑与单机存储引擎逻辑基本隔离。很多公司有自己的单机存储引擎,可以借鉴Dynamo的思想加入分布式功能?/p>
3. NWR{略可以Ҏ应用自由调整Q这个思想已经被Google借鉴到其下一代存储基设施中?/p>
4. 设计上天然没有单点,且基本没有对pȝ旉一致性的依赖。而在Google的单Master设计中,Master是单点,需要引入复杂的分布式锁机制来解冻I且Lease机制需要对机器间时钟同步做出假设?/p>
Dynamo的缺?/strong>
1. 负蝲q相比单Master设计较不可控Q负载^衡策略一般需要预估机器规模,不能无缝地适应业务动态增ѝ?/p>
2. pȝ的扩展性较差。由于增加机器需要给机器分配DHT法所需的编P操作复杂度较高,且每台机器存储了整个集群的机器信息及数据文g的Merkle Tree信息Q机器最大规模只能到几千台?/p>
3. 数据一致性问题。多个客L的写操作有顺序问题,而在GFS中可以通过只允许Append操作得到一个比较好的一致性模型?/p>
4. 数据存储不是有序Q无法执行MapreduceQMapreduce是目前允许机器故障,h强扩展性的最好的q行计算模型Q且有开源的Hadoop可以直接使用QDynamo׃数据存储依赖Hash无法直接执行Mapreduced?/p>
异常处理
Dynamo中把异常分ؓ两种cdQ时性的异常和永久性异常。服务器E序q行时一般通过cMsupervise的监控daemon启动Q出现core dump{异常情冉|自动重启。这U异常是临时性的Q其它异常如盘报修或机器报废等׃其持l时间太长,UC为永久性的。回Dynamo的设计,一份数据被写到N, N+1, ... N+K-1qK台机器上Q如果机器N+i (0 <= i <= K-1)宕机Q原本写入该机器的数据{Ud机器N+KQ机器N+K定时ping机器N+iQ如果在指定的时间T内N+i重新提供服务Q机器N+K启动传输Q务将暂存的数据发送给机器N+iQ如果超q了旉T机器N+iq是处于宕机状态,q种异常被认为是怹性的Q这旉要借助Merkle Tree机制q行数据同步。这里的问题在于旉T的选择Q所以Dynamo的开发h员后来干脆把所有程序检出来的异常认ؓ是时性的Qƈ提供l管理员一个utility工具Q用来显C指定一台机器永久性下Uѝ由于数据被存储了K份,一台机器下U将D后箋的K台机器出现数据不一致的情况。这是因为原本属于机器N的数据由于机器下U可能被临时写入机器N+1, ... N+K。如果机器N出现怹性异常,后箋的K台机器都需要服务它的部分数据,q时它们都需要选择冗余机器中较为空闲的一台进行同步。Merkle Tree同步的原理很单,每个非叶子节点对应多个文Ӟ为其所有子节点值组合以后的Hash|叶子节点对应单个数据文gQؓ文g内容的Hash倹{这PM一个数据文件不匚w都将D从该文g对应的叶子节点到根节点的所有节点g同。每台机器维护KMerkle TreeQ机器同步时首先传输Merkle Tree信息Qƈ且只需要同步从根到叶子的所有节点值均不相同的文g?/p>
?写流E?/p>
客户端的?写请求首先传输到~存的一台机器,Ҏ预先配置的K、W和R|对于写请求,ҎDHT法计算出数据所属的节点后直接写入后l的K个节点,{到W个节点返回成功时q回客户端,如果写请求失败将加入retry_list不断重试。如果某台机器发生了临时性异常,数据写入后l的备用机器q在备用机器中记录时异常的机器信息。对于读hQ根据DHT法计算出数据所属节点后Ҏ负蝲{略选择R个节点,从中dR份数据,如果数据一_直接q回客户端;如果数据不一_采用vector clock的方法解军_H。Dynamopȝ默认的策略是选择最新的数据Q当然用户也可以自定义冲H处理方法。每个写入系l的<key, value>寚w记录一个vector lock信息Qvector lock是一pd<机器节点? 版本?旉?gt;对,记录每台机器对该数据的最新更新版本信息。如下图Q?/p>
d时进行冲H解冻I如果一台机器读到的数据的vector lock记录的所有版本信息都于另一台机器,直接q回vector lock较大的数据;如果二者是q版本Q根据时间戳选择最新的数据或者通过用户自定义策略解军_H。读h除了q回数据<key, value>g外还q回vector lock信息Q后l的写操作需要带上该信息?/p>
问题1Q垃圾数据如何回Ӟ
Dynamo的垃圑֛收机制主要依赖每个节点上的存储引擎,如Berkely db存储引擎Qmerge-dump存储引擎{。其它操作,如Merkle Tree同步产生的垃圾文件回收可以和底层存储引擎配合完成?/p>
问题2QDynamo有没有可能丢数据Q?/p>
关键在于K, W, R的设|。假设一个读敏感应用讄K=3, W=3, R=1Q待处理的数据原本属于节点A, B, CQ节点B出现临时性故障的q程中由节点D代替。在节点B出现故障到节点B同步完成节点D暂存的修改这D|间内Q如果读h落入节点B或者D都将出现丢数据的问题。这里需要适当处理下,对于B节点下线的情况,׃其它机器要么~存了B节点已下U信息,要么d时将发现B节点处于下线状态,q是只需要将h转发其它节点卛_Q对于B节点上线情况Q可以等到B节点完全同步以后才开始提供读服务。对于设|W<K的应用,Dynamod旉要解军_H,可能丢数据。MQDynamo中可以保证读取的机器都是有效的(处于正常服务状态)Q但W != K时不保证所有的有效机器均同步了所有更新操作?/p>
问题3QDynamo的写入数据有没有序问题Q?/p>
假设要写入两条数?add item"?delete item"Q如果写入的序不同Q将D完全不同的结果。如果设|W=KQ对于同一个客LQ由于写入所有的机器以后才返回,可以保证序Q而多个客L的写操作可能被不同的节点处理Q不能保证顺序性。如果设|W < KQDynamo不保证顺序性?/p>
问题4Q冲H解军_是否需要将l果值更新存储节点?
L作解军_H后不需要将l果值更新存储节炏V生冲H的情况一般有机器下线或者多个客LD的顺序问题。机器下U时retry_list中的操作丢失,某些节点不能获取所有的更新操作。对于机器暂时性或者永久性的异常QDynamo中内部都有同步机制进行处理,但是对于retry_list中的操作丢失或者多个客L引发的顺序问题,Dynamo内部Ҏ无法分L数据是否正确。唯一的冲H解x器在L作,Dynamo可以设计成读操作冲H解决结果值更新存储节点,但是q样会L作变得复杂和不高效。所以,比较好的做法是每个写操作都带上读操作q回的多个版本数据,写操作将冲突处理的结果更新存储节炏V?/p>