## 背景介绍
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://box.kancloud.cn/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://box.kancloud.cn/2016-04-22_5719da3d36620.jpg)
![](https://box.kancloud.cn/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://box.kancloud.cn/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节点上进行搜索。
- 数据库内核月报目录
- 数据库内核月报 - 2016/09
- MySQL · 社区贡献 · AliSQL那些事儿
- PetaData · 架构体系 · PetaData第二代低成本存储体系
- MySQL · 社区动态 · MariaDB 10.2 前瞻
- MySQL · 特性分析 · 执行计划缓存设计与实现
- PgSQL · 最佳实践 · pg_rman源码浅析与使用
- MySQL · 捉虫状态 · bug分析两例
- PgSQL · 源码分析 · PG优化器浅析
- MongoDB · 特性分析· Sharding原理与应用
- PgSQL · 源码分析 · PG中的无锁算法和原子操作应用一则
- SQLServer · 最佳实践 · TEMPDB的设计
- 数据库内核月报 - 2016/08
- MySQL · 特性分析 ·MySQL 5.7新特性系列四
- PgSQL · PostgreSQL 逻辑流复制技术的秘密
- MySQL · 特性分析 · MyRocks简介
- GPDB · 特性分析· Greenplum 备份架构
- SQLServer · 最佳实践 · RDS for SQLServer 2012权限限制提升与改善
- TokuDB · 引擎特性 · REPLACE 语句优化
- MySQL · 专家投稿 · InnoDB物理行中null值的存储的推断与验证
- PgSQL · 实战经验 · 旋转门压缩算法在PostgreSQL中的实现
- MySQL · 源码分析 · Query Cache并发处理
- PgSQL · 源码分析· pg_dump分析
- 数据库内核月报 - 2016/07
- MySQL · 特性分析 ·MySQL 5.7新特性系列三
- MySQL · 特性分析 · 5.7 代价模型浅析
- PgSQL · 实战经验 · 分组TOP性能提升44倍
- MySQL · 源码分析 · 网络通信模块浅析
- MongoDB · 特性分析 · 索引原理
- SQLServer · 特性分析 · XML与JSON应用比较
- MySQL · 最佳实战 · 审计日志实用案例分析
- MySQL · 性能优化 · 条件下推到物化表
- MySQL · 源码分析 · Query Cache内部剖析
- MySQL · 捉虫动态 · 备库1206错误问题说明
- 数据库内核月报 - 2016/06
- MySQL · 特性分析 · innodb 锁分裂继承与迁移
- MySQL · 特性分析 ·MySQL 5.7新特性系列二
- PgSQL · 实战经验 · 如何预测Freeze IO风暴
- GPDB · 特性分析· Filespace和Tablespace
- MariaDB · 新特性 · 窗口函数
- MySQL · TokuDB · checkpoint过程
- MySQL · 特性分析 · 内部临时表
- MySQL · 最佳实践 · 空间优化
- SQLServer · 最佳实践 · 数据库实现大容量插入的几种方式
- 数据库内核月报 - 2016/05
- MySQL · 引擎特性 · 基于InnoDB的物理复制实现
- MySQL · 特性分析 · MySQL 5.7新特性系列一
- PostgreSQL · 特性分析 · 逻辑结构和权限体系
- MySQL · 特性分析 · innodb buffer pool相关特性
- PG&GP · 特性分析 · 外部数据导入接口实现分析
- SQLServer · 最佳实践 · 透明数据加密在SQLServer的应用
- MySQL · TokuDB · 日志子系统和崩溃恢复过程
- MongoDB · 特性分析 · Sharded cluster架构原理
- PostgreSQL · 特性分析 · 统计信息计算方法
- MySQL · 捉虫动态 · left-join多表导致crash
- 数据库内核月报 - 2016/04
- MySQL · 参数故事 · innodb_additional_mem_pool_size
- GPDB · 特性分析 · Segment事务一致性与异常处理
- GPDB · 特性分析 · Segment 修复指南
- MySQL · 捉虫动态 · 并行复制外键约束问题二
- PgSQL · 性能优化 · 如何潇洒的处理每天上百TB的数据增量
- Memcached · 最佳实践 · 热点 Key 问题解决方案
- MongoDB · 最佳实践 · 短连接Auth性能优化
- MySQL · 最佳实践 · RDS 只读实例延迟分析
- MySQL · TokuDB · TokuDB索引结构--Fractal Tree
- MySQL · TokuDB · Savepoint漫谈
- 数据库内核月报 - 2016/03
- MySQL · TokuDB · 事务子系统和 MVCC 实现
- MongoDB · 特性分析 · MMAPv1 存储引擎原理
- PgSQL · 源码分析 · 优化器逻辑推理
- SQLServer · BUG分析 · Agent 链接泄露分析
- Redis · 特性分析 · AOF Rewrite 分析
- MySQL · BUG分析 · Rename table 死锁分析
- MySQL · 物理备份 · Percona XtraBackup 备份原理
- GPDB · 特性分析· GreenPlum FTS 机制
- MySQL · 答疑解惑 · 备库Seconds_Behind_Master计算
- MySQL · 答疑解惑 · MySQL 锁问题最佳实践
- 数据库内核月报 - 2016/02
- MySQL · 引擎特性 · InnoDB 文件系统之文件物理结构
- MySQL · 引擎特性 · InnoDB 文件系统之IO系统和内存管理
- MySQL · 特性分析 · InnoDB transaction history
- PgSQL · 会议见闻 · PgConf.Russia 2016 大会总结
- PgSQL · 答疑解惑 · PostgreSQL 9.6 并行查询实现分析
- MySQL · TokuDB · TokuDB之黑科技工具
- PgSQL · 性能优化 · PostgreSQL TPC-C极限优化玩法
- MariaDB · 版本特性 · MariaDB 的 GTID 介绍
- MySQL · 特性分析 · 线程池
- MySQL · 答疑解惑 · mysqldump tips 两则
- 数据库内核月报 - 2016/01
- MySQL · 引擎特性 · InnoDB 事务锁系统简介
- GPDB · 特性分析· GreenPlum Primary/Mirror 同步机制
- MySQL · 专家投稿 · MySQL5.7 的 JSON 实现
- MySQL · 特性分析 · 优化器 MRR & BKA
- MySQL · 答疑解惑 · 物理备份死锁分析
- MySQL · TokuDB · Cachetable 的工作线程和线程池
- MySQL · 特性分析 · drop table的优化
- MySQL · 答疑解惑 · GTID不一致分析
- PgSQL · 特性分析 · Plan Hint
- MariaDB · 社区动态 · MariaDB on Power8 (下)
- 数据库内核月报 - 2015/12
- MySQL · 引擎特性 · InnoDB 事务子系统介绍
- PgSQL · 特性介绍 · 全文搜索介绍
- MongoDB · 捉虫动态 · Kill Hang问题排查记录
- MySQL · 参数优化 ·RDS MySQL参数调优最佳实践
- PgSQL · 特性分析 · 备库激活过程分析
- MySQL · TokuDB · 让Hot Backup更完美
- PgSQL · 答疑解惑 · 表膨胀
- MySQL · 特性分析 · Index Condition Pushdown (ICP)
- MariaDB · 社区动态 · MariaDB on Power8
- MySQL · 特性分析 · 企业版特性一览
- 数据库内核月报 - 2015/11
- MySQL · 社区见闻 · OOW 2015 总结 MySQL 篇
- MySQL · 特性分析 · Statement Digest
- PgSQL · 答疑解惑 · PostgreSQL 用户组权限管理
- MySQL · 特性分析 · MDL 实现分析
- PgSQL · 特性分析 · full page write 机制
- MySQL · 捉虫动态 · MySQL 外键异常分析
- MySQL · 答疑解惑 · MySQL 优化器 range 的代价计算
- MySQL · 捉虫动态 · ORDER/GROUP BY 导致 mysqld crash
- MySQL · TokuDB · TokuDB 中的行锁
- MySQL · 捉虫动态 · order by limit 造成优化器选择索引错误
- 数据库内核月报 - 2015/10
- MySQL · 引擎特性 · InnoDB 全文索引简介
- MySQL · 特性分析 · 跟踪Metadata lock
- MySQL · 答疑解惑 · 索引过滤性太差引起CPU飙高分析
- PgSQL · 特性分析 · PG主备流复制机制
- MySQL · 捉虫动态 · start slave crash 诊断分析
- MySQL · 捉虫动态 · 删除索引导致表无法打开
- PgSQL · 特性分析 · PostgreSQL Aurora方案与DEMO
- TokuDB · 捉虫动态 · CREATE DATABASE 导致crash问题
- PgSQL · 特性分析 · pg_receivexlog工具解析
- MySQL · 特性分析 · MySQL权限存储与管理
- 数据库内核月报 - 2015/09
- MySQL · 引擎特性 · InnoDB Adaptive hash index介绍
- PgSQL · 特性分析 · clog异步提交一致性、原子操作与fsync
- MySQL · 捉虫动态 · BUG 几例
- PgSQL · 答疑解惑 · 诡异的函数返回值
- MySQL · 捉虫动态 · 建表过程中crash造成重建表失败
- PgSQL · 特性分析 · 谈谈checkpoint的调度
- MySQL · 特性分析 · 5.6 并行复制恢复实现
- MySQL · 备库优化 · relay fetch 备库优化
- MySQL · 特性分析 · 5.6并行复制事件分发机制
- MySQL · TokuDB · 文件目录谈
- 数据库内核月报 - 2015/08
- MySQL · 社区动态 · InnoDB Page Compression
- PgSQL · 答疑解惑 · RDS中的PostgreSQL备库延迟原因分析
- MySQL · 社区动态 · MySQL5.6.26 Release Note解读
- PgSQL · 捉虫动态 · 执行大SQL语句提示无效的内存申请大小
- MySQL · 社区动态 · MariaDB InnoDB表空间碎片整理
- PgSQL · 答疑解惑 · 归档进程cp命令的core文件追查
- MySQL · 答疑解惑 · open file limits
- MySQL · TokuDB · 疯狂的 filenum++
- MySQL · 功能分析 · 5.6 并行复制实现分析
- MySQL · 功能分析 · MySQL表定义缓存
- 数据库内核月报 - 2015/07
- MySQL · 引擎特性 · Innodb change buffer介绍
- MySQL · TokuDB · TokuDB Checkpoint机制
- PgSQL · 特性分析 · 时间线解析
- PgSQL · 功能分析 · PostGIS 在 O2O应用中的优势
- MySQL · 引擎特性 · InnoDB index lock前世今生
- MySQL · 社区动态 · MySQL内存分配支持NUMA
- MySQL · 答疑解惑 · 外键删除bug分析
- MySQL · 引擎特性 · MySQL logical read-ahead
- MySQL · 功能介绍 · binlog拉取速度的控制
- MySQL · 答疑解惑 · 浮点型的显示问题
- 数据库内核月报 - 2015/06
- MySQL · 引擎特性 · InnoDB 崩溃恢复过程
- MySQL · 捉虫动态 · 唯一键约束失效
- MySQL · 捉虫动态 · ALTER IGNORE TABLE导致主备不一致
- MySQL · 答疑解惑 · MySQL Sort 分页
- MySQL · 答疑解惑 · binlog event 中的 error code
- PgSQL · 功能分析 · Listen/Notify 功能
- MySQL · 捉虫动态 · 任性的 normal shutdown
- PgSQL · 追根究底 · WAL日志空间的意外增长
- MySQL · 社区动态 · MariaDB Role 体系
- MySQL · TokuDB · TokuDB数据文件大小计算
- 数据库内核月报 - 2015/05
- MySQL · 引擎特性 · InnoDB redo log漫游
- MySQL · 专家投稿 · MySQL数据库SYS CPU高的可能性分析
- MySQL · 捉虫动态 · 5.6 与 5.5 InnoDB 不兼容导致 crash
- MySQL · 答疑解惑 · InnoDB 预读 VS Oracle 多块读
- PgSQL · 社区动态 · 9.5 新功能BRIN索引
- MySQL · 捉虫动态 · MySQL DDL BUG
- MySQL · 答疑解惑 · set names 都做了什么
- MySQL · 捉虫动态 · 临时表操作导致主备不一致
- TokuDB · 引擎特性 · zstd压缩算法
- MySQL · 答疑解惑 · binlog 位点刷新策略
- 数据库内核月报 - 2015/04
- MySQL · 引擎特性 · InnoDB undo log 漫游
- TokuDB · 产品新闻 · RDS TokuDB小手册
- PgSQL · 社区动态 · 说一说PgSQL 9.4.1中的那些安全补丁
- MySQL · 捉虫动态 · 连接断开导致XA事务丢失
- MySQL · 捉虫动态 · GTID下slave_net_timeout值太小问题
- MySQL · 捉虫动态 · Relay log 中 GTID group 完整性检测
- MySQL · 答疑释惑 · UPDATE交换列单表和多表的区别
- MySQL · 捉虫动态 · 删被引用索引导致crash
- MySQL · 答疑释惑 · GTID下auto_position=0时数据不一致
- 数据库内核月报 - 2015/03
- MySQL · 答疑释惑· 并发Replace into导致的死锁分析
- MySQL · 性能优化· 5.7.6 InnoDB page flush 优化
- MySQL · 捉虫动态· pid file丢失问题分析
- MySQL · 答疑释惑· using filesort VS using temporary
- MySQL · 优化限制· MySQL index_condition_pushdown
- MySQL · 捉虫动态·DROP DATABASE外键约束的GTID BUG
- MySQL · 答疑释惑· lower_case_table_names 使用问题
- PgSQL · 特性分析· Logical Decoding探索
- PgSQL · 特性分析· jsonb类型解析
- TokuDB ·引擎机制· TokuDB线程池
- 数据库内核月报 - 2015/02
- MySQL · 性能优化· InnoDB buffer pool flush策略漫谈
- MySQL · 社区动态· 5.6.23 InnoDB相关Bugfix
- PgSQL · 特性分析· Replication Slot
- PgSQL · 特性分析· pg_prewarm
- MySQL · 答疑释惑· InnoDB丢失自增值
- MySQL · 答疑释惑· 5.5 和 5.6 时间类型兼容问题
- MySQL · 捉虫动态· 变量修改导致binlog错误
- MariaDB · 特性分析· 表/表空间加密
- MariaDB · 特性分析· Per-query variables
- TokuDB · 特性分析· 日志详解
- 数据库内核月报 - 2015/01
- MySQL · 性能优化· Group Commit优化
- MySQL · 新增特性· DDL fast fail
- MySQL · 性能优化· 启用GTID场景的性能问题及优化
- MySQL · 捉虫动态· InnoDB自增列重复值问题
- MySQL · 优化改进· 复制性能改进过程
- MySQL · 谈古论今· key分区算法演变分析
- MySQL · 捉虫动态· mysql client crash一例
- MySQL · 捉虫动态· 设置 gtid_purged 破坏AUTO_POSITION复制协议
- MySQL · 捉虫动态· replicate filter 和 GTID 一起使用的问题
- TokuDB·特性分析· Optimize Table
- 数据库内核月报 - 2014/12
- MySQL· 性能优化·5.7 Innodb事务系统
- MySQL· 踩过的坑·5.6 GTID 和存储引擎那会事
- MySQL· 性能优化·thread pool 原理分析
- MySQL· 性能优化·并行复制外建约束问题
- MySQL· 答疑释惑·binlog event有序性
- MySQL· 答疑释惑·server_id为0的Rotate
- MySQL· 性能优化·Bulk Load for CREATE INDEX
- MySQL· 捉虫动态·Opened tables block read only
- MySQL· 优化改进· GTID启动优化
- TokuDB· Binary Log Group Commit with TokuDB
- 数据库内核月报 - 2014/11
- MySQL· 捉虫动态·OPTIMIZE 不存在的表
- MySQL· 捉虫动态·SIGHUP 导致 binlog 写错
- MySQL· 5.7改进·Recovery改进
- MySQL· 5.7特性·高可用支持
- MySQL· 5.7优化·Metadata Lock子系统的优化
- MySQL· 5.7特性·在线Truncate undo log 表空间
- MySQL· 性能优化·hash_scan 算法的实现解析
- TokuDB· 版本优化· 7.5.0
- TokuDB· 引擎特性· FAST UPDATES
- MariaDB· 性能优化·filesort with small LIMIT optimization
- 数据库内核月报 - 2014/10
- MySQL· 5.7重构·Optimizer Cost Model
- MySQL· 系统限制·text字段数
- MySQL· 捉虫动态·binlog重放失败
- MySQL· 捉虫动态·从库OOM
- MySQL· 捉虫动态·崩溃恢复失败
- MySQL· 功能改进·InnoDB Warmup特性
- MySQL· 文件结构·告别frm文件
- MariaDB· 新鲜特性·ANALYZE statement 语法
- TokuDB· 主备复制·Read Free Replication
- TokuDB· 引擎特性·压缩
- 数据库内核月报 - 2014/09
- MySQL· 捉虫动态·GTID 和 DELAYED
- MySQL· 限制改进·GTID和升级
- MySQL· 捉虫动态·GTID 和 binlog_checksum
- MySQL· 引擎差异·create_time in status
- MySQL· 参数故事·thread_concurrency
- MySQL· 捉虫动态·auto_increment
- MariaDB· 性能优化·Extended Keys
- MariaDB·主备复制·CREATE OR REPLACE
- TokuDB· 参数故事·数据安全和性能
- TokuDB· HA方案·TokuDB热备
- 数据库内核月报 - 2014/08
- MySQL· 参数故事·timed_mutexes
- MySQL· 参数故事·innodb_flush_log_at_trx_commit
- MySQL· 捉虫动态·Count(Distinct) ERROR
- MySQL· 捉虫动态·mysqldump BUFFER OVERFLOW
- MySQL· 捉虫动态·long semaphore waits
- MariaDB·分支特性·支持大于16K的InnoDB Page Size
- MariaDB·分支特性·FusionIO特性支持
- TokuDB· 性能优化·Bulk Fetch
- TokuDB· 数据结构·Fractal-Trees与LSM-Trees对比
- TokuDB·社区八卦·TokuDB团队