mysql重启后自动加载innodb和其他的动态plugin,包括tokudb。每一plugin在注册的时候指定init和deinit回调函数。tokudb的init/deinit函数分别是<code>tokudb_init_func</code>和<code>tokudb_done_func</code>。
mysql重启过程中调用<code>tokudb_init_func</code>进行必要的初始化。在<code>tokudb_init_func</code>里面,调用<code>db_env_create</code>创建一个env实例,进行参数设置和callback设置。<code>db_env_create</code>是一个简单的封装,最终会调用<code>toku_env_create</code>来进行参数设置,callback设置和初始化的。<code>toku_env_create</code>初始化工作中一个很重要的事情就是调用<code>toku_logger_create</code>初始化tokudb的日志子系统。
在tokudb中,日志子系统是由tokulogger数据结构管理的。下面仅列出了主要的数据成员。
logger子系统在env->create阶段由<code>toku_logger_create</code>进行初步的初始化工作。代码片段如下:
logger子系统在env->open阶段,调用<code>toku_logger_open</code>函数进行进一步的初始化。函数<code>toku_logger_open</code>是<code>toku_logger_open_with_last_xid</code>的简单封装。env->open最终调用<code>toku_logger_open_with_last_xid</code>解析redo log file获取下一个可用的lsn,下一个可用的redo log file的序列号index并打开相应redo log file。在env->open时,调用<code>toku_logger_open_with_last_xid</code>的最后一个参数last_xid为txnid_none,表示由<code>toku_logger_open_with_last_xid</code>指定事务子系统初始化时最新的txnid。
解析redo log file的过程在函数<code>toku_logfilemgr_init</code>实现,依次解析redo log目录下的每一个文件名符合特定格式的redo log file,从中读取最后一个log entry的lsn保存下来。redo log文件名遵循”log$index.tokulog$version”格式,$index是64位无符号整数表示的redo log file的序列号index,$version是32位无符号整数表示版本信息。
如果最新的redo log file最后一个log entry是lt_shutdown(表示正常关闭不需要进行recovery),那么把对应的txnid记录在last_xid_if_clean_shutdown变量,作为tokudb事务子系统初始化时最新的txnid。在解析redo log file的时候,还会用最新的redo log file的最后一个log entry的lsn更新logger的lsn,written_lsn,fsynced_lsn。接着,<code>toku_logger_find_next_unused_log_file</code>找到下一个可用的redo log文件的序列号,并创建新的redo log file。每个redo log file最开始的12个字节是固定的,首先是8个字节的magic字符串“tokulogg“,紧接着4个字节是log的版本信息。代码片段如下:
下面我们一起看一下往redo log新加一条insert的过程。函数<code>toku_log_enq_insert</code>的第2,第5,第6,第7,第8参数表示描述一条insert的五元组(lsn, ft, xid, key, value)。代码片段如下:
tokudb的logger有两个buffer:inbuf和outbuf。inbuf表示接收log entry的buffer,而outbuf表示写到redo log文件的buffer。这两个buffer是如何切换的呢?当inbuf满或者inbuf里的free space无法满足新来的log entry的存储需求时,需要触发redo buffer flush过程,即将inbuf日志flush到redo log文件里。这个过程比较耗时,而且很可能inbuf里面还有free space,只是由于当前这个log entry比较大而无法满足存储需求,tokudb实现了output permission机制,使得需要free space的请求等待在output permission的条件变量上,其他client thread上下文的redo log请求可以继续使用inbuf写日志。等待上一个flush完成后(即条件变量被signaled),检查当前inbuf的free space,如果可以满足这条redo log entry就直接返回,说明别的线程帮我们flush好了。如果free space不够,需要在当前线程的上下文去做flush,实际上是把inbuf和outbuf互换,然后把outbuf写到redo log文件中。写完之后适当调整inbuf的大小使之满足当前redo log entry请求。最后唤醒等待inbuf提供足够空间的线程(阻塞在output permission上的线程)。简而言之,把redo log buffer拆分成inbuf和outbuf,最重要的作用是在redo log flush的时候不会阻塞新的log entry写入,感兴趣的朋友可以看一下函数<code>toku_logger_maybe_fsync</code>的实现,这里就不一一展开了。函数<code>toku_logger_make_space_in_inbuf</code>的代码片段如下:
前面提到mysql重启过程中会调用<code>db_env_create</code>创建env实例,进行参数设置和callback设置,然后调用env->open来做进一步初始化。同样env->open也是一个回调函数,它是在<code>db_env_create</code>设置的,指向env_open函数。
在env_open里调用validate_env判断是否需要进行recovery。validate_env函数返回时表明这个env是否是emptyenv (env目录为空,且不存在rollback文件,不存在数据文件),是否是newnev (env目录不存在),是否是emptyrollback (env目录存在,rollback文件为空)。
如果满足条件 !emptyenv && !new_env && is_set(db_recovery) 就尝试进行recovery。简单地说recovery的条件就是env存在,log_dir存在,redo log存在。
判断是否真正做recovery的函数是<code>tokuft_needs_recovery</code>。代码如下:
<code>tokuft_needs_recovery</code>尝试读取最后一条redo log entry,如果不是lt_shutdown,就需要真正做recovery。读取最后一条redo log entry的代码片段如下:
在读最后一个log entry的过程中,在读log entry出错的情况下(crash的时候把redo log写坏了)会调用<code>lc_fix_bad_logfile</code>尝试修复redo log文件。修复的过程很简单:从当前redo log头部开始向后scan直到找到第一非法log entry的位置,并把redo log文件truncate到那个位置。此时,文件指针也指向文件末尾。极端的情况是,修复完redo log,发现当前redo log中的所有entry都是坏的,那样需要切换到前面一个redo log文件。
如果需要做recovery,tokudb会调用do_recovery进行恢复,恢复的时候先做redo log apply,然后进行undo rollback。代码片段如下:
scan log entry分别两个阶段:backward阶段和forward阶段。这两个阶段是由scan_state状态机控制的。在scan开始之前在<code>scan_state_init</code>函数中把状态机ss的初始状态设置为backward_newer_checkpoint_end。
backward阶段:从最后一个log entry开始向前读,直到读到checkpoint end。对在这个过程中读到的每一个log entry调用<code>logtype_dispatch_assign(le, toku_recover_backward_, r, renv)</code>。在这个阶段对于checkpoint以外的操作,toku_recover_backward_前缀的处理函数都是noop。当读到checkpoint end的log entry时,会把ss状态设置为backward_between_checkpoint_begin_end,并记录这个checkpoint的begin_lsn和lsn。然后继续向前scan直到读到checkpoint begin的log entry,确保ss中记录的checkpoint_begin_lsn和log entry的lsn是相等的,然后 把ss的状态设置为forward_between_checkpoint_begin_end,并设置renv->goforward为true。
forward阶段:对当前的log entry调用<code>logtype_dispatch_assign(le, toku_recover_, r, renv)</code>重放redo log。然后向后scan直到读到checkpoint end,确保ss中记录的<code>checkpoint_begin_lsn</code>和<code>checkpoint_end_lsn</code>与log entry里面记录的<code>lsn_begin_checkpoint</code>和lsn是相等的,然后把ss的状态设置为forward_newer_checkpoint_end。这样,崩溃之前的最后一个checkpoint就回放完成了。下面要做的事情就是,回放committed txn的redo log。代码片段如下:
上面我们是tokudb recovery的过程。对读redo log一笔带过。现在一起看看读log entry的过程:
向后读:从当前位置读4个字节的长度len1,然后读1个字节cmd。然后按照不同cmd的定义来读log entry。
向前读:从当前位置读nocrc的长度len2,把文件指针向前移动len2个字节。从那个位置向后读。
verify:读的过程需要计算crc校验码。len1是参与crc计算的,而len2不参与crc计算。计算得到的crc应该与log entry里面记录的crc相等。而且len1应该等于len2。