Percolator与分布式事务思考(二)
这一篇会更加细节的来了解以下Percolator分布式事务,首先看下事务协议。Figure 6 显示了Percolator事务的伪代码,事务构建器请求时间戳预报服务一个开始的时间戳(第6行),通过Get()操作决定可见的一致性快照。被暂时缓冲起来(buffered)(第7行)直到提交的时候调用Set()操作。基本的提交多个缓冲写的方法是2段提交,这个通过客户端协调。不同机器的事务交互通过BigTable tablet 服务器上的行事务进行。(译者:这个和XA对比比较类似,TM只做事务日志和协调,但是并不关心隔离性,由各个RM来进行各自的隔离性实现,无论是XA还是Percolator的实现都会产生死锁-不同事务在不同机器上都等待对方释放其持有的资源,不同的是,XA实现通常会有一个死锁检测,而Percolator有延迟释放锁的协议,后面会讲到)
下图Figure 4 展示了Percolator在执行事务中数据和元数据的格式与分布,各种元数据columns被系统使用,如同Figure 5所描述。
在commit的第一个阶段(“prewrite”),我们试着锁定所有将被写的cell.(为了处理客户端失败,我们任意指定一个锁定为主锁定;我们在下面将会介绍这个机制)事务读取元数据来检查每个将要被写的cell是否是冲突的。这里有2种冲突元数据:如果事务发现另外一个操作在它申请的时间戳后写数据,那么它中止(第32行);这是一个写-写冲突,快照隔离会保护。如果事务看到另外一个事务在锁定任意一个时间戳,他同样停止(第34行)。这是有可能的,比如某个事务已经在我们申请的时间戳之前提交,并且它可能正缓慢释放它持有的锁,但是我们认为这个不太可能,所以我们中止。如果没有冲突,我们写入锁并且写入每一个cell的start timestamp版本的数据(第36-38行)。
如果没有cells的冲突,那么事务可能是可以提交的并且继续进行第二个步骤.在第二阶段的开始,客户端从时间戳预报服务(timestamp oracle)中取得提交时间戳(第48行).然后,在每一个cell中(从主锁定开始(starting with the primary)),客户端释放它持有的锁并且通过(将排他锁)替换成读锁(或共享锁)让它对一条记录的写对读操作可见.写好的记录给读操作提示表示提交的数据已经出现在cell中了;它包含一个开始时间戳的引用,而这个时间戳就是能够读到实际数据的版本.一旦主 写(primary’s write)数据已经可见(第58行),事务必需提交.
一个Get()操作首先检查在时间戳为[0,start_timestamp]的范围内的锁, 这个范围也是这个事务快照中所能看到的时间戳范围(第12行).如果一个锁是存在的,并且是另外一个事务并发地写这个cell,所以读事务必需等待这个锁的释放(译者:写冲突返回错误,读写冲突读等待).如果没有冲突的锁,Get()在一个时间戳范围内读取最后一个写的记录并且返回与那个写记录对应的数据(第22行).
执行事务可能由于客户端的失败而变得复杂(tablet服务器挂掉不会影响系统因为BigTable保证在跨tablet服务器失败时写锁也是持久不丢失的).如果一个事务刚刚被提交,客户端就挂了,那么锁就会留在那里.Percolator必需清理这些锁,否则这些锁可能会让后续的事务永久的挂起:当一个事务A 遇到了一个由事务B遗留下来的冲突锁,A也许会认为B已经失败了并且消除它的锁(!!).
但是A完美地判定B失败这是非常困难的;所以我们必需避免 在A清除B的事务锁 和 实际没有失败的B提交了它的事务 之间的竞争.Percolator通过在每一个事务中选定一个cell作为任何提交或者清理操作的一个同步点来处理这种情况.这个cell的锁被叫做主锁(primary lock). A和B对哪个锁是主锁的观点是一致的(这个主锁的地址会写到其他所有cell上的锁).无论进行清理或者提交操作都需要修改主锁;因为这个操作是在一个BigTable行锁之下的,所以只有一个清理或者提交操作将会成功.具体来说:在B提交之前,它必需检查它仍然占有主锁并且将这个主锁替换成一条写记录(write record).在A擦除B的的锁之前,A必需检查主锁以确定B还没有提交;如果主锁仍然存在,那么A能够安全地擦除这个锁(译者:通过一个公共的原子锁做为标记解决客户端异常失败还锁定记录的情况,这个原子锁的粒度个人认为是同一个事务的多线程执行).
当客户端在第二阶段中挂掉,一个事务将会跨过提交点(已经至少有一条写记录了)但是仍然持有未完成的锁.我们必需对这些事务进行前滚操作(roll-forward).一个事务遇到一个锁,它能够通过观察主锁分辨出两种情况:如果主锁已经被替换成一个写记录了,那么向这个锁写数据的事务肯定已经提交了,并且这个锁肯定是前滚的(rolled forward).否则它应该被回滚.为了前滚,事务执行清理取代之前原始事务通过标准锁定写回记录的操作.
主锁上的清理操作是同步的,所以清理活着的客户端持有的锁是安全的,然而这样性能会非常低,因为这会导致其他事务因为强制回滚而中止. 所以,一个事务清理一个锁除非它觉得这个锁是属于一个死的或者卡住的worker.Percolator使用简单机制来决定其他事务是否是活着的. 运行中的workers会向Chubby锁服务写一个token以表明他们是属于这个系统的.其他workers能够使用这些token的存在不存在判定一个worker是否存活(当worker进程退出时token自动被删除(译者:类似zookeeper的非持久节点,但是轮询检测的原因,删除操作会有比较大的延迟,最起码zookeeper是这样).为了处理一个worker是活着的,但是执行很慢的情况,我们额外写一个时间(wall time)到锁里;一个锁如果包含一个太旧的时间,那么它将会被清理即使它所属的worker是活着的.不过有时候为了处理长时间提交的操作,workers会在提交期间周期性更新这个时间.