标签:
Tablet是kudu表的水平分区,类似于google Bigtable的tablet,或者HBase的region。每个tablet存储着一定连续range的数据(key),且tablet两两间的range不会重叠。一张表的所有tablet包含了这张表的所有key空间。
Tablet由RowSet组成,RowSet由一组rows组成(n条数据、n行数据)。RowSet是不相交的,即不同的RowSet间的row不会交叉,因此一条给定的数据,只会存在于一个RowSet中。虽然Rowset是不相交的,但是两两间的key空间是可以相交的(key的range)。
一个RowSet存储在内存中,它被称为MemRowSet,一个tablet中只有一个MemRowSet。MemRowSet是一个in-memory的B-Tree树,且按照表的主键排序。所有的insert直接写入进MemRowSet。受益于MVCC(Multi-Version Concurrency Control 多版本并发控制,下文中会讲述),一旦数据写入到MemRowSet,后续的reader能立马查询到。
注意:不同于BigTable,Kudu只有插入和插入与flush前的mutation才会被记录到MemRowSet。mutation例如基于磁盘数据的update、deletion,下文会有介绍。
任何一条数据都以entry的形式精确的存在于一个MemRowSet中,entry由一个特殊的header和实际的row data内容组成。由于MemRowSet只存于内存中,最终会被写满,然后Flush到磁盘里(一个或者多个DiskRowSet中)。(下文会详细介绍)
Kudu为了提供一些有用的特性,使用多版本并发控制:
为了提供MVCC功能,每个操作(mutation)会带有一个时间戳(timestamp)。Timestamp是由TS-wide Clock实例提供的,tablet的MvccManager能保证在这个tablet中timestamp是唯一的不重复的。MvccManager决定了数据提交的timestamp,从而这个时间点后的查询都可以获取到刚刚提交的数据。查询在被创建的时候,scanner提取了一个MvccManager时间状态的快照,所有对于这个scanner可见的数据都会跟这个MvccSnapshot比较,从而决定到底是哪个insertion、update或者detete操作后的数据可见。
每个tablet的Timestamp都是单调递增的。我们使用HybridTime技术来创建时间戳,它能保证节点之间的时间戳一致。
为了支持快照和历史快照功能,多个版本的数据必须被存储。为了防止空间无限扩展,用户可以配置一个保留时间,并将这个时间之前的记录GC(这个功能可以防止每次查询都从最原始版本开始读取)。
为了在MemRowSet中支持MVCC功能,每行插入的数据都会带着时间戳。而且,row会有一个指针,它指向紧随其后的mutations列表,每个mutation都有带有timestamp:
在传统的关系型数据库术语里,这个有序的mutations列表可以被称作“RODO log”。
任何reader需要访问MemRowSet的row中的mutations,才能得到正确的快照。逻辑如下:
注意,mutation可以是如下的任何一种:
举个真实例子,表结构(key STRING, val UINT32),经过如下操作:
在MemRowSet中,会有如下结构:
注意,当更新过于频繁时,会有如下的影响:
考虑到如上低效率的操作,我们给出如下假设:
如果如上提到的低效率影响到了实际应用,后续会有很多降低开销的优化可以去做。
当MemRowSet满了,会触发Flush操作,它会持续将数据写入disk。
数据flush到disk成了CFiles文件(参见src/kudu/cfile/README)。数据里的每行都通过一个有序的rowid标识了,而且这个rowid在DiskRowSet中是密集的、不可变的、唯一的。举个例子,如果一个给定的DiskRowSet包含有5行数据,那么它们会以key上升的顺序被分配为rowid0~4。不同的DiskRowSet,会有不同的行(rows),但却可能有相同rowid。
读取时,系统会使用一个索引结构,把用户可见的主键key和系统内部的rowid映射起来。上述例子中的主键是一个简单的key,它的结构嵌入在主键列的cfile里。另外,一个独立的index cfile保存了编码后的组合key,使用了提供了类似的方法。(不懂)
注意:rowid不是精确的跟每行数据的data存在一起,而是在这个cfile里根据数据有序的index的一个隐式识别。在一部分源码中,将rowid定义为 “row indexes” 或者 “ordinal indexes”。
注意:其他系统,例如C-Store把MemRowSet称为”write optimized store” (WOS),把DiskRowSet称为”read-optimized store” (ROS)。
为了让on-disk data具备MVCC功能,每个on-disk的Rowset不仅仅包含当前版本row的data,还包含UNDO的记录,如此,可以获取这行数据的历史版本。
当用户想读取flush后最新版本的数据时,只需要获取base data。因为base data是列式存储的,这种查询性能是非常高的。如果不是读取最新数据,而是time-travel查询,就得回滚到指定历史时间的一个版本,此时就需要借助UNDO record数据。
当一个查询拿到一条数据,它处理MVCC信息的流程是:
举个例子,回顾一下之前MVCC Mutations in MemRowSet章节例子的一系列操作:
当这条数据flush进磁盘,它将会被存成如下形式:
每条UNDO record是执行处理的反面。例如在UNDO record里,第一条INSERT事务会被转化成DELETE。UNDO recod旨在保留插入或者更新数据的时间戳:查询的MVCC快照指定的时间早于Tx1时,Tx1还未提交,此时将会执行DELETE操作,那么这时这条数据是不存在的。
再举两个不同查询的例子:
每个例子都处理了正确时间的UNDO record,以产生正确的数据。
最常见的场景是查询最新的数据。此时,我们需要优化查询策略,避免处理所有的UNDO records。为了达到这个目标,我们引入文件级别的元数据,指向UNDO record的数据范围。如果查询的MVCC快照符合的所有事务都已经提交了(查询最新的数据),这组deltas就会短路(不处理UNDO record),这时查询将没有MVCC开销。
更新或者删除已经flush到disk的数据,不会操作MemRowSet。它的处理过程是这样的:为了确定update/delete的key在哪个RowSet里,系统将巡视所有RowSet。这个处理首先使用一个区间tree,去定位一组可能含有这key的RowSet。然后,使用boom filter判断所有候选RowSet是否含有此key。如果某一些RowSet同时通过了如上两个check,系统将在这些RowSet里寻找主键对应的rowid。
一旦确定了数据所在的RowSet,mutation将拿到主键对应的rowid,然后mutation会被写入到一个称为DeltaMemStore的内存结构中。
一个DiskRowSet里就一个DeltaMemStore,DeltaMemStore是一个并行BTree,BTree的key是使用rowid和mutation的timestamp混合成的。查询时,符合条件的mutation被执行后得到快照timestamp对应数据,执行方式与新数据插入后的mutation类似(MemRowSet)。
当DeltaMemStore存入的数据很大后,同样也会执行flush到disk,落地为DeltaFile文件:
DeltaFile的信息类型与DeltaMemStore是一致的,只是被压实和序列化在密集型的磁盘里。为了把数据从base data更新成最新的数据,查询时需要执行这些DeltaFile里的mutation事务,这些DeltaFile集合称作REDO文件,而file里这些mutation称作REDO record。与存于MemRowSet里的mutation类似,当读取比base data更新版本的数据时,它们需要被一次应用(执行)。
一条数据的delta信息可能包含在多个DeltaFile文件,这种情况下,DeltaFile是有序的,后边的变更会优先于前边的变更。
注意,mutation存储结构没必要包含整行的数据。如果在一行中,仅仅只有一列数据被更新,那么mutation结构只会包含这一列的更新信息。不读取或者重写无关的列,这样更新数据操作就快而有效率。
总结一下,每个DiskRowSet逻辑上分三部分:
UNDO record 和REDO record存储格式是一样的,都称为DeltaFile。
当DeltaFile里的mutation堆积越来越多,读取RowSet数据效率就越来越低,最坏情况,读取最新版本数据需要遍历所有REDO record并与base data merge。换一句话说,如果数据被更新了太多次,为了得到最新版本的数据,就需要执行这么多次的mutation。
为了提高读取性能,Kudu在后台将低效率的物理布局转化成更加高效的布局,且转化后具有同样的逻辑内容。这种转化称为:delta compaction。它的目标如下:
注意:BigTable的设计是timestamp绑定在data里,没有保留change信息(insert update delete);而kudu的设计是timestamp绑定在change里,而不是data。如果历史的UNDO record被删除,那么将获取不到某行数据或者某列数据是什么时候插入或者更新的。如果用户需要这个功能,他们需要保存插入或者更新的timestamp列,就跟传统关系型数据库一样。
delta campaction分minor和major两种。
Minor delta compactoin:
Minor compaction是多个delta file的compaction,不会包含base data,compact生成的也是delta file。
Major delta compaction:
Major compaction是对base data和任意多个delta file的compact。
Major compaction比minor compaction更耗性能,因为它需要读取和重写base data,并且base data比delta data大很多(因为base data存了一行数据,而delta data是对某一些column的mutation,需要注意的base data是列式存储的,delta data不是)。
Major compaction可以对DiskRowSet里的任意多个或者一个column进行compact。如果只有一列数据进行了多次重要的更新,那么compact可以只针对这一列进行读取和重写。在企业级应用中会经常遇到这种情况,例如更新订单的状态、更新用户的访问量。
两种类型的compaction都维护RowSet里的rowid。因为它们完全在后台执行,且不会带锁。compact的结果文件会采用原子swapping的方式被引入进RowSet。Swap操作结束后,compact前的那些老文件将会被删除。
随着越来越多的数据写入tablet,DiskRowSet数量也会累积的越来越多。如此这般将会降低kudu性能:
如上所述,我们应该merge RowSet以减少RowSet的数量:
与如上提到的Delta Compaction不同,请注意,merging Compaction不会保持rowid一样。这使得处理并发的mutation错综复杂。这个过程在compaction.txt文件中有比较详细的描述。
与BigTable不同的设计方式点如下:
标签:
原文地址:http://blog.csdn.net/lookqlp/article/details/51416829