分布式transaction中lock的设计和实现

来源:互联网 发布:ubuntu怎么设置上网 编辑:程序博客网 时间:2024/06/05 15:50

锁是实现并发访问控制的必须机制。锁由两个对象组成:owner和resource。当想要访问resource时,必须先判断是否已经有其它的owner在使用,如果没有,则在resource中标志当前的owner正在使用锁。

 

在分布式系统中,锁的机制变得复杂,因为resource分布在各个机器中,但是锁的访问机制没有改变。比如有resources:R1,R2,R3分布在三台不同的机器上,现在有多个clients要想要在一个transaction中同时访问这三个resources,首先,client必须要全部获得 R1、R2和R3的lock后才能成功update这三个resources的状态。

 

1、分布式lock的owner-id的生成

 

在非分布式锁的实现中,owner一般使用thread-id作为lock的owner id,但是在分布式锁的实现中,lock的owner-id如何确定呢。owner-id的必须是唯一的,所以如果owner-id在client中指定,则必须有机制来确保多个clients中指定的owner-id没有冲突;如果参照非分布式锁的owner-id是由管理resource的系统指定(管理系统提供了access resource的方式,如OS中,通过thread的调度来并发access resource,所以可以使用thread-id作为lock的owner-id),可以把第一个access的分布式resource的owner-id作为分布式锁的owner-id,如client依次access R1、R2和R3,在访问R1时获得的lock的owner-id为ip:thread-id,则在acess R2和R3时指定用R1的owner-id来获得R2和R3的lock。

 

在Amazon S3的keymap的分布式lock机制中,client生成lock的owner-id。owner-id是64bit的,记作owner-id[64]。其中owner-id[63]设置为0,确保owner-id是unsigned。owner-id[32~62]表示从某时间点开始到现在时刻的秒数(如从格林尼治时间1970/1/1 00:00:00起到现在时刻流逝的秒数),owner-id[22~31]表示现在时刻的最后一秒中流逝的毫秒数,owner-id[0~21]是随机生成的bits。这样可保证任何一个client在任意时刻生成的owner-id都是唯一的。

 

2、如何避免分布式lock的死锁

 

在非分布式lock中,并发的访问多个resources会引起dead lock的问题,避免dead lock的最好方法是每个client都按照约定的同样顺序来获得resources的lock,比如对于resources R1、R2和R3,大家都按照R1-->R2-->R3这样的访问顺序来获得R1、R2和R3的锁。

 

同样在分布式lock中,按照一致约定的顺序来访问分布式resources是避免死锁的好方法。在Google的GFS的实现中,就用了这种方法。在GFS中,clients需要update一个文件的chunk,一个chunk在GFS中三个replicas。如何让clients都按照一致约定的顺序来访问chunk的3个replicas呢?Google并没有给clients指定访问的顺序(由于系统的动态性,采用动态指定coordinator的方法更好),而是在三个chunk的replicas中选出一个coordinator,由这个coordinator来协调clients的updates,这个coordinator在GFS称为chunk的Maste,GFS Master通过Lease机制指定拥有这个chunk replicas的其中一台chunk server作为这个chunk的master。所有的clients把update的请求发送给chunk master,chunk master把这些请求排序,依次在其它两台拥有chunk replica的chunk server和自身中按照次序执行这些排好序的请求。

 

按照预先约定好的顺序access resources可以避免死锁,但是也会带来其它的一些问题,比如GFS使用Lease来确定coordinator会带来额外的一些开销和复杂性。如果clients不按照预定的顺序访问分布式的resources,如何避免死锁呢?

 

我们可以使用“try lock”的方式来获得分布式resources的locks,如果“try lock”任意的resource失败,则abort transaction,否则可以commit transaction。这种方式的缺点就是如果resources的访问冲突很严重,transaction成功commit的概率较小,浪费了系统资源且降低了用户体验。

 

 

3、分布式lock的cleanup

 

在client请求分布式lock的过程中,client会发生crash的情况,这时候如何cleanup client请求的分布式lock呢。client是分布式lock的coordinator,cleanup过程应该由哪个对象来充当coordinator的角色呢。分布式lock的各个Servers和请求的client之间有heartbeat,是否可能在servers检查到client 失去连接的情况简单地abort或者commit client发起的transaction呢。答案是no。因为servers并不清楚这个transaction处在什么状态,transaction可能还没有到达commit-point的状态,也可能已经到达了commit-point状态但还没有完成整个commit 过程。如果transactin还没有到达commit-point状态,需要roll-back,如果transaction到达了commit point状态但没有完成commit过程,则需要roll-forward。

分布式系统可以指定一个模块充当这个cleanup过程的coordinator。这样的充当cleanup coordinator的模块会面临着HA的问题,所以另外一种在Google的Percolator中使用的方法是lazy-cleanup。

 

例如有两个分布式transaction A和transaction B在资源R1、R2和R3上有竞态的请求。请求Transaction B的client crash了,B的transactin在进行状态中,这时A如何cleanup transaction B获得的分布式locks呢。

 

首先,要有一个能够记录反映transaction是否到达commit-point状态的地方,因为commit-point状态是cleanup过程采取roll-back和roll-forward action的根据。

 

Google的Percolator系统选择在transaction访问的第一个resource中记录反映transaction的commit-point状态。例如transaction B访问的第一个resource是R1,则选择在R1中记录反映这个transaction的状态,我们可以把这个记录反映transaction状态的字段设置在resource的lock中,记为cps-commit point status,同时还要设置字段来记录这个cps状态存储在那个地方,记为primary--第一个访问的resorce为primary。

 

在transaction B获得R1、R2和R3的locks时,在locks中标记primary为R1。这样任何时候在请求resources的locks都可以很清楚的知道已经获得这个resource的lock的transaction的状态。比如transaction B在没有达到commit point状态时crash了,Transaction A在请求R1、R2、R3的locks时可以根据locks中的primary字段来找到R1,从R1的cps中可以知道这个transaction处在什么状态,如果还没有到达commit-point状态,进行roll-back,否则进行roll-forward操作来完成整个commit过程。

 

在Percolator中,把commit-point状态和存储位置都放在resource中,是因为Percolator的resource在Bigtable中,resource是高度HA的(尽管load resources的机器会crash),如果resources不是高度HA的,则需要把cps和primary存储在其它高度HA的entity中。

 

 

 

4、dirty read的支持

 

在分布式transaction的的实现中,往往都是支持dirty read的,因为transaction持续的时间会很长,支持dirty read可以不影响用户的体验。在支持dirty read的方法中,最常用的就是snapshot方法了。dirty read在read的时候看到是状态确定的snapshot,即是已经committed的transaction的状态。snapshot 本身是有timestamp属性的,当client在t1的timestamp请求read,系统会判断在【0~t1】时间内是否有transaction存在,如果没有,client可以read时间t1之前的snapshot;如果有transaction存在,client必须要等待transactin commit或者abort后才能read t1时间之前的snapshot。