Large-scale Incremental Processing Using Distributed Transactions and Notifications ,系统名字叫 Percolator 。其实是为了提升Google的搜索引擎索引系统效率而存在的。它的设计其实有很多局限性的。首先它主要是用于增量事务,比如说搜索引擎中扫描到页面中某一个部分(比如blog里的推荐阅读列表)发生变化了,其他内容其实不需要重新算权重。那么整个页面的权重可能只要重算这个变化的区域+页面宗权重即可。另外一个特性就是延迟不敏感,因为 Percolator 里很多设计都是高延时的(茫茫多的锁和冲突时Waiting),所以要求应用不太在意延时。也是比如搜索引擎里,过几分钟再建索引并不是大问题。
事务支持
Percolator 的事务依赖 Bigtable 时间戳的多版本数据保存和事务。每个Value都绑定了一系列元数据列,写入到 Bigtable 的同一个本地组(Locality group)里。因为一个 Locality group 的物理部署在同一组 Bigtable 节点上,这样可以实现对同一个 Locality group 的多列进行原子操作,也能加快关联数据的查找速度。
主要逻辑代码
classTransaction{structWrite{ Row row; Column col; string value;}; vector<Write> writes ;int start_ts ;Transaction(): start_ts (oracle.GetTimestamp()) {}voidSet(Writew){writes .pushback(w);}boolGet(Rowrow,Columnc,string*value){while(true){bigtable::Txn T =bigtable::StartRowTransaction(row);// Check for locks that signal concurrent writes.if(T.Read(row, c+"lock",[0, start_ts ])){// There is a pending lock; try to clean it and waitBackoffAndMaybeCleanupLock(row, c);continue;}// Find the latest write below our start timestamp. latest_write =T.Read(row, c+"write",[0, start_ts ]);if(!latest_write.found())returnfalse;// no dataint data_ts =latest_write.start_timestamp();*value =T.Read(row, c+"data",[data_ts,data_ts]);returntrue; } }// Prewrite tries to lock cell w, returning false in case of conflict.boolPrewrite(Write w, Write primary) { Column c =w.col;bigtable::Txn T =bigtable::StartRowTransaction(w.row);// Abort on writes after our start timestamp . . .if(T.Read(w.row, c+"write",[start_ts, ∞])) return false;// . . . or locks at any timestamp.if(T.Read(w.row, c+"lock",[0, ∞]))returnfalse;T.Write(w.row, c+"data", start_ts ,w.value);T.Write(w.row, c+"lock", start_ts , {primary.row,primary.col});// The primary’s location.returnT.Commit(); }boolCommit() { Write primary = writes [0]; vector<Write>secondaries(writes .begin()+1,writes .end());if(!Prewrite(primary, primary))returnfalse;for(Write w : secondaries)if(!Prewrite(w, primary))returnfalse;int commit_ts =oracle .GetTimestamp();// Commit primary first. Write p = primary;bigtable::Txn T =bigtable::StartRowTransaction(p.row);if(!T.Read(p.row,p.col+"lock",[start_ts,start_ts])) return false;// aborted while workingT.Write(p.row,p.col+"write", commit_ts, start_ts );// Pointer to data written at start_ts .T.Erase(p.row,p.col+"lock", commit_ts);if(!T.Commit())returnfalse;// commit point// Second phase: write out write records for secondary cells.for(Write w : secondaries) {bigtable::Write(w.row,w.col+"write", commit_ts, start_ts );bigtable::Erase(w.row,w.col+"lock", commit_ts); }returntrue; }}// class Transaction
在原文里描述的是每个写事务都会写入时间戳到对应的 Paxos group 里,但是我的理解因为已经用 Paxos group 对分片进行选主了。按理在leader确定的情况下其实可以直接由这个分片的leader来分配这个时间戳。但是这样在容灾和副在均衡期间这个分配可能延时会变高,但是毕竟这是少频率操作,而大部分情况下的写请求里分配时间戳不走 Paxos group ,可以大幅提高响应速度。
每个 F1 服务器节点内最多有两个版本的元表,服务器内正在使用的元表要么是当前元表,要么是下一个元表。
元表变更的事务分为几个阶段,并且每个阶段必须是互相兼容的。
比如上面的例子里,第一步增加一个只处理删除操作的索引 \( I \) , 然后等所有服务器节点都获知这个索引之后,再开启所有节点对 \( I \) 的写入权限和初始化。等所有的节点初始化完成后再返回索引建立完成。这样在第一阶段中间删除数据的话,仅仅是最后初始化的时候就不会再建立这个索引了;而在第二阶段任意节点删除数据,也能通知到删除这条数据的索引。
Optimistic transactions 的优点首先毋庸置疑的,对于长时间事务而言,锁冲突变少了;然后 F1 对这种事务透明地重试也变容易了;其次,由于状态保留在客户端,如果服务器重试失败,客户端也可以换节点重试,也更利于负载均衡;最后就是事务执行期间,如果其他事务需要读这些行的数据也是不会阻塞的。当然Optimistic transactions 也有一些缺陷,其一是新插入的数据行冲突问题(因为时间戳存在于行数据里,多个节点插入同一个Key的话涉及插入冲突),这点可以通过对上层父级表加乐观锁实现;另一个是如果真的同时有大量的对同一行的数据操作,会导致失败率非常高,这也是所有乐观锁的缺陷。这个就需要上层业务自己斟酌做批量提交或是使用其他类型的事务了。
F1 默认是使用行级锁,但是也可以业务层自己指定锁的分组,以进一步减小锁冲突的可能性。
和大多数数据库服务一样, F1 也提供变更历史的功能。变更历史有很多作用,比如主从同步、故障恢复等等。 F1 里还有个重量级的应用是用作订阅-发布模型的消息队列系统, 有了变更历史,这个分布式的订阅-发布模型的消息队列也很容易实现了。 F1 的变更历史存储是作为实际数据表的子表存储的,这样可以尽可能放在一起,减少高延时操作。
F1 的客户端设计
个人感觉 F1 的客户端还是比较重的,首先它舍弃了多行读取的隐式的顺序保证,来换得更高的并发度。这个我的理解就是多个节点处理数据的时候,谁先返回就先拿到谁呗。然后对于NoSQL的接入也很简单,本来 F1 、Spanner 的底层存储也就是Key-Value的。
F1 客户端的SQL接入就比较复杂一些了。毕竟也要靠它提供 OLTP 和 OLAP 的功能。 对于 OLTP 功能, F1 客户端采用中心化的访问策略,其实前面的机制也算基本完成了。 而 OLAP 采用分布式的访问策略,并且仅访问快照。 整体上F1 客户端的功能还是比较重的。我记一下我认为的重要的地方吧。
受制于 Spanner 和 F1 的特性,Client(特别是负责写事务的时候)显然和服务器部署在一起能显著减少延迟。按文中描述的是部署在美国东西海岸各两个副本,中部一个副本的情况下,再加上2PC的机制,写事务单单多副本间通信的延迟就到50ms了。整体平均延迟读事务5-10ms,写事务50-150ms。并且500行数据一个Batch写事务的情况下,每秒500个事务是比较轻松的。