MySQL · TokuDB · TokuDB索引结构–Fractal Tree

最后更新于:2022-04-01 10:35:42

## 背景介绍 TokuDB采用的是Fractal Tree作为索引的数据组织方式。它是一种面向磁盘I/O优化的数据结构,采用“分期偿还”策略减少在数据插入过程中从root节点到leaf节点的搜索过程。这种搜索过程可以简称为locate_position,就是寻找要插入key在Tree中位置的过程。 一般B+Tree的插入过程分为两个部分: 1. Locate_position: 从root开始使用binary search方法递归地寻找应该插入到哪个子节点上,直到在leaf节点找到应该插入的位置然后返回; 2. Insert_into_postion: 在locate_position返回的位置进行插入操作,如果当前leaf节点存储的key个数超过预定义的最大值可能会引起split操作,最坏的情况是引起从leaf节点到root节点的split。 Fractal Free把每个操作都看成一个message。每个internal节点维护了一个msg_buffer按照FIFO顺序缓存message;索引的有序序列是在leaf节点维护的。所谓采用“分期偿还”是指:在Fractal Tree中插入时,只需要把(key, value)对插入到root节点(或者若干深度的internal节点)的msg_buffer就可以返回了,这个过程可以简称为`push_into_root`。中间节点msg_buffer中的message是由后台工作线程分批地flush到子节点上,最终刷到leaf节点上的,这个过程简称为`push_into_child`。与Fractal Tree类似的面向磁盘I/O优化的数据结构还有Buffer Tree [论文链接](http://www.cs.cmu.edu/~guyb/realworld/slidesF10/buffertree.pdf) 和Log Structured Merge Tree, 感兴趣的朋友可以看一下。 下面我一起看一下Fractal Tree的数据结构定义,维护Fractal Tree的基本算法的插入,删除,分裂,合并和查询过程。 ## Fractal Tree数据结构 大多数情况下message不是直接插入到leaf节点上,而是被缓存在internal节点,由后台工作线程异步flush到leaf节点上。在某个时间段,对同一个key可能存在多个未applied到leaf节点的修改,每个修改对应一个message,这些message有可能缓存被在不同的internal节点上(这些internal节点一定都在从root到leaf路径上)。为了区分message的先后顺序,在插入到root节点(`push_into_root`)时,每个message被赋予了一个有序递增的序列号(msn),它是全局唯一的。 ~~~ struct ftnode { MSN max_msn_applied_to_node_on_disk; // applied最大的msn unsigned int flags; // 创建标志,等于ft->h->flags BLOCKNUM blocknum; // 节点对应的块号 int height; // 高度:0表示leaf节点,>0表示中间节点 int dirty; // 脏页标记,用于cachetable脏页回刷 uint32_t fullhash; // 在cachetable的哪个bucket上 int n_children; // partition个数 ftnode_pivot_keys pivotkeys; // 每个partition的key值区间 TXNID oldest_referenced_xid_known; // 最近一次push_into_root操作时系统可能的最老事务id struct ftnode_partition *bp; // internal节点:子节点的msg_buffer; leaf节点:有序数据结构 struct ctpair *ct_pair; // 对应cachetable的pair(cache entry) }; ~~~ ## Fractal Tree layout ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-22_5719da3d1177d.jpg) 上图中蓝色圆圈表示internal节点,旁边的白色矩形表示msg_buffer; 最下面一层蓝色矩形表示leaf节点 Fractal Tree 的bp域指向partition。对于Internal节点,bp域指向每个子节点对应的msg_buffer。一个internal节点最多有16个(可配置)的子节点,每个子节点对应的子树保存某个key值区间的数据。对于leaf节点,bp域指向leaf节点的有序数据结构。Leaf节点可以由多个有序的子结构组成,每个子结构构称作basement节点,实际上是一个弱平衡树形结构(或者数组)。Internal节点和leaf节点的缺省大小是4M字节,basement节点的缺省大小是128K字节。从root节点到leaf节点查询的过程,是通过各级节点的 pivotkeys 来路由的。 Pivotkeys域表示子节点的key值范围: * 第0个子节点的key范围在 ( -∞, pivotkeys[0] ] * 第1个子节点的key范围在( pivotkeys[0],pivotkeys[1] ] * 第i个子节点的key范围在( pivotkeys[i-1],pivotkeys[i] ] * 最后一个子节点的key范围在( pivotkeys[n_children-1],+∞) Bp域表示每个partition: * 第0个partition:bp[0] * 第1个partition:bp[1] * 第i个partition:bp[i] * 最后一个partition:bp[n_children-1] ### Partition信息 ~~~ struct ftnode_partition { BLOCKNUM blocknum; // internal节点:子节点块号; leaf节点:unused uint64_t workdone; // internal节点:applied message字节数; leaf节点:unused struct ftnode_child_pointer ptr; // internal节点:子节点msg_buffer信息; leaf节点:basement节点 enum pt_state state; // internal节点:子节点状态; leaf节点:basement节点状态 uint8_t clock_count; // evictor线程根据此变量判断是否可以partial evict }; enum pt_state { PT_INVALID = 0, // 无效 PT_ON_DISK = 1, // 在磁盘上 PT_COMPRESSED = 2, // 已读入内存,压缩格式 PT_AVAIL = 3 // 已读入内存,解压缩格式 }; typedef struct ftnode_child_pointer { union { struct sub_block *subblock; //压缩后的buffer struct ftnode_nonleaf_childinfo *nonleaf; // internal节点:msg_buffer struct ftnode_leaf_basement_node *leaf; // leaf节点:basement节点 } u; } FTNODE_CHILD_POINTER; ~~~ ### Internal节点存储的msg_buffer ~~~ struct ftnode_nonleaf_childinfo { message_buffer msg_buffer; // 缓存的message off_omt_t broadcast_list; // 用于支持on-line add index, on-line add column marked_off_omt_t fresh_message_tree; // 未apply的message在msg_buffer 里的offset,按(key,msn)顺序排序 off_omt_t stale_message_tree; // 已经applied过的message在msg_buffer里的offset,按(key,msn)顺序排序 uint64_t flow[2]; // 流控 }; ~~~ ### Leaf节点的basement节点 ~~~ struct ftnode_leaf_basement_node { bn_data data_buffer; // 有序序列,弱平衡树形结构(或者数组) unsigned int seqinsert; // 判断顺序 insert的hint MSN max_msn_applied; // applied最大的msn }; ~~~ ## 维护Fractal Tree的基本操作 ### 创建新的Fractal Tree的索引 每个Fractal Tree由FT来表示。下面是列举了FT中比较重要的域。 ~~~ struct ft { FT_HEADER h; // header FT_HEADER checkpoint_header; // checkpoint开始时刻header的克隆 CACHEFILE cf; // 描述了FT对应的磁盘文件和数据节点信息。 toku::comparator cmp; // DBT compare函数 block_table blocktable; // FT逻辑块号到文件offset的映射表 struct toku_list live_ft_handles; // 打开此FT的handle列表 uint32_t num_txns; // 访问此FT的事务个数 bool pinned_by_checkpoint; // 表示checkpoint正在进行 BLOCKNUM rightmost_blocknum; // 最右leaf节点块号,用于顺序insert优化 } ; typedef struct ft *FT; struct ft_header { enum ft_type type; // header类型 int dirty; // 脏标记 uint64_t checkpoint_count; // checkpoint计数 LSN checkpoint_lsn; // checkpoint开始时刻的lsn const uint64_t time_of_creation; // FT创建时间 TXNID root_xid_that_created; //创建FT的事务ID uint64_t time_of_last_modification; // 最近一次更新时间 BLOCKNUM root_blocknum; // root节点块号 const unsigned int flags; // 创建标志 unsigned int nodesize; // 节点大小,缺省值4M字节 unsigned int basementnodesize; // basement节点大小,缺省值128K字节 enum toku_compression_method compression_method; // 压缩算法 unsigned int fanout; // internal节点的fanout MSN max_msn_in_ft; // FT最大的msn }; enum ft_type { FT_CURRENT = 1, // FT header FT_CHECKPOINT_INPROGRESS // checkpoint开始时刻FT header的克隆 }; ~~~ 创建新的Fractal Tree索引(简称FT)时候,调用函数`toku_ft_create`做一些必要的初始化工作:创建FT索引的header,创建block table(FT逻辑块号到文件offset的映射表),以及创建FT的root节点。创建root节点的过程很简单:初始化一个空的leaf节点(块号为0),并把它加到cachetable中,此时的root节点只包含一个basement节点。在leaf节点被回刷到磁盘之前会把leaf节点按照128K为单位分割成多个basement节点,这样做的好处是加速leaf节点上的查询过程。 ### Fractal Tree insert 在第一节背景介绍里面谈到的,向Fractal Tree插入(key,value)对的操作是`push_into_root`。在TokuDB代码实现里面,insert操作是通过调用`toku_ft_root_put_msg`给FT发一个FT_INSERT/FT_INSERT_NO_OVERWRITE(出现duplicate key时保留老值)消息实现的。 向FT发送一个message的过程如下: * 调用`toku_pin_ftnode`获取root节点(加share锁),这个过程中可能会引起从磁盘读取root节点的操作,在这里假设所有节点都已缓存在内存中; * 调用`toku_ftnode_get_reactivity`判断root节点是否需要split。Leaf节点需要split的条件是:leaf节点所需的磁盘空间大于nodesize(缺省4M);internal节点需要split的条件是:子节点个数大于fanout(缺省16)。如果需要split,root节点需要把share锁升级为exclusive锁,拿到exclusive锁后调用`ft_init_new_root`进行root分裂并生成新的root节点,这个返回时root节点块号保持不变(还是0),并持有exclusive锁,`ft_init_new_root`返回后需要把exclusive锁降级为share锁。一般情况下root节点是不要split的; * 向root节点push message。 下面就是push message的过程: * 如果root是leaf节点或者message是广播消息的话(比如on-line add index或者on-line add column)就把message放到root节点就可以返回了。这里需要解释一下,广播消息是需要apply到每一个leaf节点上的,所以需要从root节点向下逐级flush,不能跳过任何一个范围; * 如果Fractal Tree的高度大于1,调用`push_something_in_subtree`把message放到root的节点的某个子树上; * 如果Fractal Tree的高度等于1,首先调用`toku_ftnode_which_child`确定应该把message放到哪个leaf节点上。如果目标的leaf节点是最左(childnum == 0)或者最右(childnum == node->n_children - 1) 的情况,调用`push_something_in_subtree`把message放到目标leaf节点上;否则放到root节点即可返回,最左最右的情况是优化顺序查询。最后一节会看到,一般的FT search是需要把root到leaf搜索路径上所有落在bounds区间internal节点上的message先apply到leaf节点,然后再leaf节点上进行搜索的。在TokuDB的engine层,`get_first/get_last`操作直接读取最左/最右的leaf节点,所以最左最右插入的时候需要把message 同步apply到leaf节点上的。伪代码如下: ~~~ void toku_ft_root_put_msg(FT ft, const ft_msg &msg, txn_gc_info *gc_info) { ftnode_fetch_extra bfe; bfe.create_for_full_read(ft); // fetch整个node pair_lock_type lock_type = PL_READ; // 初始状态:申请读锁 change_lock_type: toku_pin_ftnode(ft, root_key, fullhash, &bfe, lock_type, &node, true); enum reactivity re = toku_ftnode_get_reactivity(ft, node); switch (re) { case RE_STABLE: case RE_FUSIBLE: // root节点不支持merge操作 if (lock_type != PL_READ) { // 其他thread抢先做了split toku_unpin_ftnode_read_only(ft, node); lock_type = PL_READ; goto change_lock_type; } break; case RE_FISSIBLE: if (lock_type == PL_READ) { // 需要split,升级为写锁 toku_unpin_ftnode_read_only(ft, node); lock_type = PL_WRITE_CHEAP; goto change_lock_type; } else { // root节点split ft_init_new_root(ft, node, &node); // split完成,降级为读锁 toku_unpin_ftnode(ft, node); lock_type = PL_READ; goto change_lock_type; } break; } if (root ->height == 0 || ft_msg_type_is_broadcast(msg.type())) { // root是leaf节点或者messsage是广播消息,把message放到root节点上 // 这里释放锁的原因是:inject_message_at_this_blocknum里面会重新拿锁 toku_unpin_ftnode_read_only(ft, root); inject_message_at_this_blocknum(ft, root_key, fullhash, msg, flow_deltas, gc_info); } else if (root->height > 1) { // root高度大于1时,把message加到root节点的某个子树上 push_something_in_subtree(ft, node, -1, msg, flow_deltas, gc_info, 0, LEFT_EXTREME | RIGHT_EXTREME, false); } else { // root高度等于1时,确定message应该放到哪个leaf节点上 int childnum = toku_ftnode_which_child(node, msg.kdbt(), ft->cmp); // 目标leaf节点是root的最左或最右子节点时,把message放到相应的leaf节点上 if (childnum == 0 || childnum == node->n_children - 1) { push_something_in_subtree(ft, node, childnum, msg, flow_deltas, gc_info, 0, LEFT_EXTREME | RIGHT_EXTREME, false); } else { // 把message放到root节点上 // 这里释放锁的原因是:inject_message_at_this_blocknum里面会重新拿锁 toku_unpin_ftnode_read_only(ft, node); inject_message_at_this_blocknum(ft, root_key, fullhash, msg, flow_deltas, gc_info); } } } toku_ftnode_get_reactivity返回的reactivity定义如下: enum reactivity { RE_STABLE, // 正常状态 RE_FUSIBLE, // 需要merge RE_FISSIBLE // 需要split }; ~~~ 下面我们一起看一下`push_something_in_subtree`的处理过程。这里有个promotion的概念:message有可能不会直接放到root节点上,而是放到root的第一级子节点或者第二级子节点上(至多往下看两级子节点)。Promotion的一般原则: * 只promote非广播message; * 如果子节点对应的msg_buffer非空,就不会递归向下promote; * 为了优化顺序insert,如果是最左/最右的情况一定要把message apply到leaf节点上; * 非最左/最右的情况,遇到height == 1的internal节点或者depth ==2 (已做了两级的promotion),promote就停止了; * 修改leaf节点是很耗时的(在后面会看到),所以一般选择把message放到internal节点上; 函数`inject_message_in_locked_node`实现了把message push到node节点上。伪代码如下: ~~~ static void inject_message_in_locked_node(FT ft, FTNODE node, int childnum, const ft_msg &msg, size_t flow_deltas[],txn_gc_info *gc_info) { // 生成全局(FT内)唯一的msn MSN msg_msn = { .msn = toku_sync_add_and_fetch(&ft->h->max_msn_in_ft.msn, 1) }; ft_msg msg_with_msn(msg.kdbt(), msg.vdbt(), msg.type(), msg_msn, msg.xids()); toku_ftnode_put_msg(ft->cmp, ft->update_fun, node, childnum, msg_with_msn, true, gc_info, flow_deltas, &stats_delta); if (node->height > 0 && toku_ftnode_nonleaf_is_gorged(node, ft->h->nodesize)) { // 如果node节点是internal节点,msg_buffer非空并且node节点所需的磁盘空间 + 各个子节点的workdone(query过程中同步flush的message大小的总和)大于nodesize(缺省4M),trigger client线程池的工作线程异步flush这个节点。 // msg_buffer非空并且node节点所需的磁盘空间 + 各个子节点的workdone: 表示node节点逻辑上的size toku_ft_flush_node_on_background_thread(ft, node); } else { toku_unpin_ftnode(ft, node); } } ~~~ 函数`toku_ftnode_put_msg`的作用是把message push到node节点上。如果node是internal节点,调用函数`ft_nonleaf_put_msg把message`加到msg_buffer里面。Internal节点维护了一个FIFO队列msg_buffer,按照message到达节点的顺序追加到FIFO尾部。Internal节点里面还维护了两个有序结构:`fresh_message_tree`和`stale_message_tree`,这两个有序结构记录了按照(key,msn)排序的message(实际上存的是message在msg_buffer中的offset)。`fresh_message_tree`保存的是未apply的message;`stale_message_tree`保存的是在query过程中同步applied过的message。`inject_message_in_locked_node`在退出之前判断internal节点是否缓存了大量message,如果是则会把当前这个节点放到cachetable (TokuDB的buffer pool)的client线程池,让工作线程在后台把这个节点上的message flush到下一级节点。其实cachetable有个后台线程cleaner线程也在做同样的事情,但是cleaner线程是每1秒钟启动一次,而且一个cleaner线程负责处理cachetable所有需要flush的internal节点,是很有可能来不及处理的。 这里重点看一下把message 放到leaf节点的过程,这里我们不考虑广播消息。首先是调用`toku_ftnode_which_child`判断message应放到哪个basement节点上,然后调用`toku_ft_bn_apply_msg`把message放到目标basement节点上。前面函数中都没有考虑message的类型,直到这个函数才会根据message类型做不同的处理。对应insert操作,首先尝试顺序insert的优化(即跟basement最后一个key作比较,若大于最后一个key表示是升序顺序insert情况)。如果不是顺序insert的pattern,会在basement节点的有序数据结构bn里面进行binary search查找key对应的LEAFENTRY,然后调用`toku_ft_bn_apply_msg_once`在basement节点进行insert。 `toku_ft_bn_apply_msg_once`是`toku_le_apply_msg`的简单封装。`toku_le_apply_msg`伪代码如下: ~~~ toku_le_apply_msg(const ft_msg &msg, LEAFENTRY old_leafentry, // 老的LEAFENTRY bn_data* data_buffer, // basement节点的bn,如果是basement节点上第一个message,这个值NULL uint32_t idx, // old_leafentry在bn上的index uint32_t old_keylen, txn_gc_info *gc_info, LEAFENTRY *new_leafentry_p, //新的LEAFENTRY int64_t * numbytes_delta_p) { if (old_leafentry == NULL) { msg_init_empty_ule(&ule); } else { le_unpack(&ule, old_leafentry); // 把LEAFENTRY转成ULE } msg_modify_ule(&ule, msg); // 把message加到ULE的MVCC // 把ULE转成LEAFENTRY // 若data_buffer == NULL,le_pack分配basement节点的bn int r = le_pack(&ule, data_buffer, idx, msg.kdbt()->data, keylen, old_keylen, oldmemsize, new_leafentry_p, &maybe_free); } ~~~ 如果是FT_INSERT_NO_OVERWRITE,需要检查key是否已存在。若存在,直接退出。否则在ULE的MVCC结构添加一个类型为XR_INSERT的provisional txn。 ~~~ msg_modify_ule(ULE ule, const ft_msg &msg) { switch (type) { case FT_INSERT_NO_OVERWRITE: { UXR old_innermost_uxr = ule_get_innermost_uxr(ule); // key已存在,保留老值 if (uxr_is_insert(old_innermost_uxr)) break; } case FT_INSERT: { uint32_t vallen = msg.vdbt()->size; void * valp = msg.vdbt()->data; ule_apply_insert(ule, xids, vallen, valp); break; } ~~~ ## Fractal Tree delete 在Fractal Tree删除一个对的方法是:调用函数 `toku_ft_root_put_msg` 给FT发送一个类型为 FT_DELETE_ANY 的message。其处理过程与insert类似,函数`msg_modify_ule`会判断message的类型,如果是FT_DELETE_ANY,就会给ULE的MVCC添加一个 XR_DELETE 类型的provisional txn。 ~~~ msg_modify_ule(ULE ule, const ft_msg &msg) { switch (type) { case FT_DELETE_ANY: ule_apply_delete(ule, xids); break; } } ~~~ ## LEAFENTRY的隐式提交 之前有篇月报[《MySQL·TokuDB·事务子系统和 MVCC 实现》](http://mysql.taobao.org/monthly/2016/03/01/)讨论TokuDB在事务提交一节里有这样的描述: > 如果是root txn调用apply_txn对rollback log的每一个item进行commit操作。如果是nested child txn把child txn的rollback log挂到parent的rollback log尾部,等到root txn 提交的时候对所有rollback log的item进行commit。需要注意的是,对于大部分DML操作rollback log item->commit都是noop。 那么在LEAFENTRY里面provisional txn是如何提交的呢?LEAFENTRY采用LAZY的方式提交,就是说provisional txn并不会在txn commit的时候变成commit txn,而是推迟到下一次修改这个LEAFENTRY的时候隐式提交。 原因是:修改LEAFENTRY需要先转成ULE结构,做完修改再把ULE转成LEAFENTRY。这样做的好处是把两次LEAFENTRY=>ULE=>LEAFENTRY合并了。Txn rollback时候,会对DML操作的key发FT_ABORT_ANY类型的message,这种message也是采用push到root节点(或者其下面的某个子节点)就返回。 ~~~ static void msg_modify_ule(ULE ule, const ft_msg &msg) { XIDS xids = msg.xids(); enum ft_msg_type type = msg.type(); if (type != FT_OPTIMIZE && type != FT_OPTIMIZE_FOR_UPGRADE) { ule_do_implicit_promotions(ule, xids); } } static void ule_do_implicit_promotions(ULE ule, XIDS xids) { if (ule->num_puxrs > 0) { int num_xids = toku_xids_get_num_xids(xids); uint32_t max_index = ule->num_cuxrs + min_i32(ule->num_puxrs, num_xids) - 1; uint32_t ica_index = max_index; uint32_t index; for (index = ule->num_cuxrs; index <= max_index; index++) { TXNID current_msg_xid = toku_xids_get_xid(xids, index - ule->num_cuxrs); TXNID current_ule_xid = ule_get_xid(ule, index); if (current_msg_xid != current_ule_xid) { //ICA表示innermost common ancestor ica_index = index - 1; break; } } if (ica_index < ule->num_cuxrs) { // 隐式提交上一次对这个LEAFENTRY的修改 invariant(ica_index == ule->num_cuxrs - 1); ule_promote_provisional_innermost_to_committed(ule); } else if (ica_index < ule->num_cuxrs + ule->num_puxrs - 1) { ule_promote_provisional_innermost_to_index(ule, ica_index); } } } ~~~ ## Cleaner线程异步flush机制 Cleaner线程找到消耗内存最多的internal节点,调用`cleaner_callback`去做flush。在`get_write_callbacks_for_node`注册的`cleaner_callback`是`toku_ftnode_clone_callback`。这个函数首先把node的所有partition读到内存,然后选择内存消耗最大的子节点,把那个节点对应的bnc缓存的message flush到相应的子节点上。 Flush bnc的过程: * 记下需要flush的bnc; * 新创建一个空的bnc记做new_bnc,并用new_bnc代替bnc来缓存新的message; * 调用`toku_bnc_flush_to_child`把bnc缓存的message flush到子节点上。 `toku_bnc_flush_to_child`会遍历bnc的所有message,然后对每个message调用`toku_ftnode_put_msg`把message放到子节点上的。一个bnc被flush完之后,可能会引起子节点递归的flush,也可能引起节点的split或者merge。由于篇幅有限请大家自己分析。 ## Fractal Tree split和merge Fractal Tree节点的split和merge操作是自上而下进行的,这些操作都是在向这个节点push一个message之后触发的。 ### Fractal Tree split Fractal Tree节点split的条件是: * leaf节点:所需磁盘空间大于nodesize(缺省4M) * Internal节点:子节点个数大于fanout(缺省16) Split的过程分为两个部分: * 分割数据:对于leaf节点来说是把basement和其存储的LEAFENTRY按照split类型分为两部分;对于internal节点来说是把msg_buffer平均分成两部分; * 分割pivotkeys:按照第一步分割数据的方式把pivotkeys分成两部分,并把splitk加到父节点上。Split完成后这两个节点拥有相同的`max_msn_applied_to_node_on_disk`和`oldest_referenced_xid_known`。 Fratcal Tree支持的split类型: ~~~ enum split_mode { SPLIT_EVENLY, SPLIT_LEFT_HEAVY, SPLIT_RIGHT_HEAVY }; ~~~ Split之后,internal节点可能会选择进一步做flush message的操作。为什么split之后还需要flush呢?这部分代码比较晦涩,笔者认为可能是因为split的条件是子节点个数大于fanout,没有考虑到节点的逻辑大小。 ### Fractal Tree merge:把相邻的两个节点合并成一个节点。 Fractal Tree节点merge的条件是: * Leaf节点:所需磁盘空间小于1/4 nodesize(缺省4M) * Internal节点:子节点个数小于1/4 fanout(缺省16) 节点merge的过程也分成两种情况: * Leaf节点:如果nodea大小+nodeb大小<3/4 nodesize才会考虑合并;否则,如果nodea大小<1/4 nodesize或者nodeb大小<1/4, 会balance这两个节点(先合并,再平均split成两个节点) * Internal节点:把msg_buffer合并 ## Rebalance leaf节点 Leaf节点被回刷到磁盘之前会调用`toku_ftnode_leaf_rebalance`函数把leaf节点的basement节点按照basementnodesize(缺省128K)大小平均分割成若干个basement节点。还记得吗?root节点刚创建的时候只有1个basement节点的。 ## Fractal Tree上的查询 Fractal Free在查询方面是比较特殊的: * FT需要先同步flush root->leaf路径上满足查询条件的所有message * FT形态与B+Tree不同:leaf节点没有双向链表 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-22_5719da3d36620.jpg) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-22_5719da3d67c44.jpg) 上面的图是FT的树形结构,下面的图是B+Tree的树形结构 FT查询每次都是从root节点开始的,在internal节点中的查找是由pivotkeys路由到相应的子节点上,在leaf节点上是由pivotkeys路由到basement节点,然后在basement节点进行binary search寻找search key所在的位置。当要进行一个range查询的时候,首先使用set_range方法找到满足查询range区间的第一个key,然后把上一次得到的key作为参数调用get_next方法。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-22_5719da3de5da3.jpg) FT中查找key的入口函数是`toku_ft_search`。这个函数读取root节点,并根据search->key找到需要读取的子节点(`bfe.child_to_read`),然后调用`ft_search_node`在root节点的相应子节点查找。读root节点的时候,`bfe.read_all_partitions`被设置被TRUE。Root节点包含的所有paritition的信息都会从磁盘上读上来。即当root是internal节点的时候,所有子节点的msg_buffer信息都会读到内存来;root是leaf节点的时候,所有的basement节点的有序结构都会读到内存来。伪代码如下: ~~~ int toku_ft_search(FT_HANDLE ft_handle, ft_search *search, FT_GET_CALLBACK_FUNCTION getf, void *getf_v, FT_CURSOR ftcursor, bool can_bulk_fetch) { try_again: trycount++; ftnode_fetch_extra bfe; // 只读取包含search key的那个子节点 bfe.create_for_subset_read(ft, search, &ftcursor->range_lock_left_key, &ftcursor->range_lock_right_key, ftcursor->left_is_neg_infty, Ftcursor->right_is_pos_infty,ftcursor->disable_prefetching, true); FTNODE node = NULL; { //读取root节点,并设置bfe.child_to_read为包含search key的子节点 toku_pin_ftnode(ft, root_key, fullhash, &bfe, PL_READ, &node, true); } struct unlock_ftnode_extra unlock_extra = {ft_handle,node,false}; struct unlockers unlockers = {true, unlock_ftnode_fun, (void*)&unlock_extra, (UNLOCKERS)NULL}; { bool doprefetch = false; bfe.child_to_read r = ft_search_node(ft_handle, node, search, bfe.child_to_read, getf, getf_v, &doprefetch, ftcursor, &unlockers, (ANCESTORS)NULL, pivot_bounds::infinite_bounds(), can_bulk_fetch); if (r==TOKUDB_TRY_AGAIN) { // 获取锁时需要等待或者需要partial fetch if (unlockers.locked) { toku_unpin_ftnode_read_only(ft_handle->ft, node); } goto try_again; } else { assert(unlockers.locked); } } assert(unlockers.locked); toku_unpin_ftnode_read_only(ft_handle->ft, node); } ~~~ `ft_search_node` 是一个递归调用的函数,它根据 node->height 决定调用`ft_search_child`(internal节点)或者`ft_search_basement_node`(leaf节点)。`ft_search_basement_node`就是在(`node`,`bfe.child_to_read`)对应的basement节点的有序数据结构里面查找满足cmp函数的key。`ft_search_child`调用`toku_pin_ftnode_for_query`读取(`node`,`bfe.child_to_read`)表示子节点childnode。每次进入`ft_search_child`定义了一个新的bfe(记做bfe_1)表示读取childnode节点的hint信息。读取的时候指定了`bfe_1.read_all_partitions = (childnode->height >=1)`。当childnode是internal节点时,会把所有partition的msg_buffer都读到内存来;childnode是leaf节点的时候,只读search->key对应basement节点。`toku_pin_ftnode_for_query`采用non-blocking的方式获取childnode上的锁(这里是读锁)。Non-blocking的含义是说如果这个锁可以获取就立即返回;否则在等待之前把之前获得的从root到父节点的锁全部释放掉,然后阻塞在那个锁上。被唤醒以后立即释放锁返回TRY_AGAIN重新从root开始search。从父节点到root节点获得的锁被记录在unlockers队列里面,队列尾部是root节点。 函数`toku_pin_ftnode_for_query`返回成功以后,会间接递归调用`ft_search_node`在childroot节点上进行搜索。`ft_search_node`还有一个副产品就是把上层传过来的bounds映射到(`node`,`bfe.child_to_read`)的key值区间,经过几次间接递归调用`ft_search_node`最终会到达leaf节点。如果是leaf节点`toku_pin_ftnode_for_query`需要检查bounds对应的key值区间的msg是否都applied过了,如果有未apply的msg,则遍历unlockers队列记录的祖先节点,把祖先节点key值在bounds范围的message flush到leaf节点上。确保所有在bounds范围的message都apply到leaf节点以后,在leaf节点对应的basement节点上进行搜索。
';