说明
由于知乎这个SB网站对专栏字数的限制,我不得不将WAL日志分为了两个部分来写,前面写的是核心数据结构,这里主要写核心流程。
物理吐槽知乎,做得越来越垃圾,迟早迁走。
生成WAL日志项
日志与事务绑定:在事务修改时(插入、更新、删除)创建日志项内容并写入内存,在事务提交时,内存中的事务日志被写入磁盘。
以普通的事务更新为例,以下是日志项被创建时的函数调用栈:
#0 __txn_logrec_init (session=0x2aaaaaae60d0) at src/txn/txn_log.c:199
#1 __wt_txn_log_op (session=session@entry=0x2aaaaaae60d0, cbt=cbt@entry=0x6ab5f0) at src/txn/txn_log.c:244
#2 0x00002aaaaad5202e in __wt_row_modify (session=session@entry=0x2aaaaaae60d0, cbt=cbt@entry=0x6ab5f0, key=key@entry=0x6ab6f0, value=value@entry=0x6ab718,
upd_arg=upd_arg@entry=0x0, modify_type=modify_type@entry=4, exclusive=exclusive@entry=false) at src/btree/row_modify.c:209
#3 0x00002aaaaad12a0b in __cursor_row_modify_v (modify_type=4, value=0x6ab718, cbt=0x6ab5f0, session=0x2aaaaaae60d0) at src/btree/bt_cursor.c:410
#4 __btcur_update (cbt=0x6ab5f0, value=0x6ab718, modify_type=4) at src/btree/bt_cursor.c:1265
#5 0x00002aaaaad1b88c in __wt_btcur_update (cbt=cbt@entry=0x6ab5f0) at src/btree/bt_cursor.c:1532
#6 0x00002aaaaad7ef24 in __curfile_update (cursor=0x6ab5f0) at src/cursor/cur_file.c:366
#7 0x00002aaaaada3bb1 in __curtable_update (cursor=0x6ab450) at src/cursor/cur_table.c:603
#8 0x0000000000401007 in main (argc=1, argv=0x7fffffffbd08) at txn.c:61可以看到,如果开启WAL日志,那么在行数据被修改时即创建并初始化日志项:
static int
__txn_logrec_init(WT_SESSION_IMPL *session)
{
......
txn = &session->txn;
/* jeff.ding: WT_LOGREC_COMMIT = 1 */
rectype = WT_LOGREC_COMMIT;
fmt = WT_UNCHECKED_STRING(Iq);
if (txn->logrec != NULL)
return (0);
WT_ASSERT(session, txn->id != WT_TXN_NONE);
/* jeff.ding: header_size是什么?
* header_size是计算存储在日志项数据部分的起始信息的大小
* 在这里主要有: rectype & txn->id
* 调试结果显示,虽然rectype类型为uint32_t, txn->id类型为uint64_t
* 但是计算得出来的headersize仅为2(可能是为了节省空间而优化的)
* 下面的函数又是在干什么?
*/
WT_RET(__wt_struct_size(session, &header_size, fmt, rectype, txn->id));
WT_RET(__wt_logrec_alloc(session, header_size, &logrec));
WT_ERR(__wt_struct_pack(session,
(uint8_t *)logrec->data + logrec->size, header_size,
fmt, rectype, txn->id));
/*
* jeff.ding: logrec->size包含了日志项的固定头部大小(16B) + 日志项内容前面的headersize(2)
* 因此,目前总的大小好像是18B
* 接下来可能还要记录key/value等信息,会进一步变大
*/
logrec->size += (uint32_t)header_size;
txn->logrec = logrec;
return (ret);
}初始化WAL日志项过程:
1. 分配日志项内存数据结构:在wiredtiger中,所有内存结构都是WT_ITEM,其data字段指向缓冲区地址,其size代表缓冲区大小,分配的log record也不例外,另外,分配的缓冲区地址按照128B对齐,且最小为128B;
2. 每个WAL日志项在存储的时候有头部和日志项内容,头部长度固定(16B),日志项内容变长。在这里,会为日志项内容提前写入两个部分内容:rectype & txn_id,好像各占用一个字节,对于普通跟新来说,rectype为WT_LOGREC_COMMIT(1),而txn_id则是运行时状态,而logrec->size就记录了此时固定头部+当前日志项内容长度;
更新日志项内容
分配日志项缓冲区后接下来就是写入日志内容了,这是在函数__txn_op_log中进行:
static int
__txn_op_log(WT_SESSION_IMPL *session,
WT_ITEM *logrec, WT_TXN_OP *op, WT_CURSOR_BTREE *cbt)
{
......
if (cbt->btree->type == BTREE_ROW) {
switch (upd->type) {
case WT_UPDATE_MODIFY:
WT_RET(__wt_logop_row_modify_pack(
session, logrec, op->fileid, &cursor->key, &value));
break;
case WT_UPDATE_STANDARD:
WT_RET(__wt_logop_row_put_pack(
session, logrec, op->fileid, &cursor->key, &value));
break;
...
} else {
...
}
...
}
int
__wt_logop_row_modify_pack(
WT_SESSION_IMPL *session, WT_ITEM *logrec,
uint32_t fileid, WT_ITEM *key, WT_ITEM *value)
{
/*
* jeff.ding:
* fmt代表了后面要序列化进入logrecord内的字段的类型
* 这里有optype/0/fileid/key/value
* 分别是I/I/I/u/u
* I: 整形
* u: 字符串
*/
const char *fmt = WT_UNCHECKED_STRING(IIIuu);
optype = WT_LOGOP_ROW_MODIFY;
WT_RET(__wt_struct_size(session, &size, fmt,
optype, 0, fileid, key, value));
__wt_struct_size_adjust(session, &size);
WT_RET(__wt_buf_extend(session, logrec, logrec->size + size));
recsize = (uint32_t)size;
WT_RET(__wt_struct_pack(session,
(uint8_t *)logrec->data + logrec->size, size, fmt,
optype, recsize, fileid, key, value));
logrec->size += (uint32_t)size;
}这里真正将更新的key/value写入日志项。当然,写入的不仅仅只有key/value而已,还有一些控制信息,具体有:
* optype:本次操作类型,在个人调试时使用的是WT_LOGOP_ROW_MODIFY
* 0:不确定该字段含义是什么?
* fileid:日志文件id
再结合日志项创建时写入的头部信息,我们有理由相信,一个完整的日志项格式应该是如下图所示这样:

每个事务内可以进行多个修改操作,每个修改操作都会生成一个更新日志项。因此,上图准确地描绘了该情形。
日志落盘
在事务被提交的时候会先将WAL日志写入磁盘,其函数调用栈如下:
#0 __wt_log_release (session=session@entry=0x2aaaaaae60d0, slot=0x63a0b8, freep=freep@entry=0x7fffffffb9f0) at src/log/log.c:1772
#1 0x00002aaaaadb8d57 in __log_write_internal (flags=20, lsnp=0x0, record=<optimized out>, session=0x2aaaaaae60d0) at src/log/log.c:2571
#2 __wt_log_write (session=session@entry=0x2aaaaaae60d0, record=<optimized out>, lsnp=lsnp@entry=0x0, flags=20) at src/log/log.c:2472
#3 0x00002aaaaae345d9 in __wt_txn_log_commit (session=session@entry=0x2aaaaaae60d0, cfg=cfg@entry=0x7fffffffbb70) at src/txn/txn_log.c:289
#4 0x00002aaaaae2de23 in __wt_txn_commit (session=session@entry=0x2aaaaaae60d0, cfg=cfg@entry=0x7fffffffbb70) at src/txn/txn.c:801
#5 0x00002aaaaae13dc2 in __session_commit_transaction (wt_session=0x2aaaaaae60d0, config=0x0) at src/session/session_api.c:1678
#6 0x000000000040102a in main (argc=1, argv=0x7fffffffbd08) at txn.c:63WAL日志落盘有几个关键的过程:
1. 为日志项分配slot以及slot内的写入位置;
2. 将日志项数据拷贝至1分配的地方;
3. 将slot数据写入日志文件,并调用sync操作等待日志写入完成。
slot分配
在slot内分配写入缓存给新的WAL日志项是在函数__wt_log_slot_join中进行,它被__log_write_internal所调用:
void
__wt_log_slot_join(WT_SESSION_IMPL *session, uint64_t mysize,
uint32_t flags, WT_MYSLOT *myslot)
{
......
for (;;) {
WT_BARRIER();
slot = log->active_slot;
old_state = slot->slot_state;
if (WT_LOG_SLOT_OPEN(old_state)) {
flag_state = WT_LOG_SLOT_FLAGS(old_state);
released = WT_LOG_SLOT_RELEASED(old_state);
join_offset = WT_LOG_SLOT_JOINED(old_state);
if (unbuffered)
new_join = join_offset + WT_LOG_SLOT_UNBUFFERED;
else
new_join = join_offset + (int32_t)mysize;
new_state = (int64_t)WT_LOG_SLOT_JOIN_REL(
(int64_t)new_join, (int64_t)released,
(int64_t)flag_state);
} else {
......
}
}
myslot->slot = slot;
myslot->offset = join_offset;
myslot->end_offset = (wt_off_t)((uint64_t)join_offset + mysize);
}这里的目的是为本次要写入的日志项内容分配slot以及在slot内分配空间:
1. 分配slot:选择当前活跃的active_slot;
2. 无锁地在slot内分配一个写入偏移量:通过slot的state字段编码了当前slot已分配的offset,只需要从该offset后面开始分配即可,分配成功后再将更新state字段;
3. 分配时需要考虑当前日志项过大,超过了slot的最大范围(slot_buf_size / 2 = 128KB),这就是unbuffered的含义,关于大事务的日志处理会在后面详细解释;
4. 代码中好像没有处理这样一种情况:如果写入日志项没有超过128KB,但是slot内没有足够的空间来容纳日志项内容,应该怎么办?
5. 分配结果在myslot中,记录为该日志项分配的slot、在slot的起始和结束位置等信息。
日志项数据拷贝至slot
static int
__log_write_internal(WT_SESSION_IMPL *session, WT_ITEM *record, WT_LSN *lsnp,
uint32_t flags)
{
......
if (ret == 0)
// jeff.ding: 向slot写入WAL日志内容
// jeff.ding: 这里同时为WAL日志项分配LSN
// jeff.ding: 将force参数设置为false表示只需要将record写入slot缓冲区即可
ret = __wt_log_fill(session, &myslot, false, record, &lsn);
release_size = __wt_log_slot_release(&myslot, (int64_t)rdup_len);
......
}
int
__wt_log_fill(WT_SESSION_IMPL *session,
WT_MYSLOT *myslot, bool force, WT_ITEM *record, WT_LSN *lsnp)
{
......
/*
* 一般force设置为false,即将日志项内容直接拷贝至slot缓冲区内即可
*/
if (!force && !F_ISSET(myslot, WT_MYSLOT_UNBUFFERED))
memcpy((char *)myslot->slot->slot_buf.mem + myslot->offset,
record->mem, record->size);
else
WT_ERR(__log_fs_write(session, myslot->slot,
myslot->offset + myslot->slot->slot_start_offset,
record->size, record->mem));
if (lsnp != NULL) {
*lsnp = myslot->slot->slot_start_lsn;
// jeff.ding: myslot->offset代表了本次WAL日志在slot内的偏移
/* -------------------------------
* | WAL-1 | ... | WAL-MY | WAL-X |
* -------------------------------
* ^ ^
* | |
* | myslot->offset
* |
* slot->slot_start_lsn
*
* so myslot start lsn = slot->slot_start_lsn + myslot->offset
*/
lsnp->l.offset += (uint32_t)myslot->offset;
}
......
}根据上面的分析,一般情况下只需要将日志数据拷贝至slot内的内存缓冲区即可。在拷贝完成后还做了一件事:
int64_t
__wt_log_slot_release(WT_MYSLOT *myslot, int64_t size)
{
slot = myslot->slot;
my_start = slot->slot_start_offset + myslot->offset;
while ((cur_offset = slot->slot_last_offset) < my_start) {
if (__wt_atomic_casiv64(
&slot->slot_last_offset, cur_offset, my_start))
break;
WT_BARRIER();
}
/*
* Add my size into the state and return the new size.
* jeff.ding: 这个更关键,改变slot的state
* 这个state被使用的地方非常多
*/
rel_size = size;
if (F_ISSET(myslot, WT_MYSLOT_UNBUFFERED))
rel_size = WT_LOG_SLOT_UNBUFFERED;
my_size = (int64_t)WT_LOG_SLOT_JOIN_REL((int64_t)0, rel_size, 0);
return (__wt_atomic_addiv64(&slot->slot_state, my_size));
}这里的主要目的是修改slot->slot_last_offset这个成员变量,这里使用了无锁修改。另外还需要将本次写入的内容大小反馈到slot->slot_state字段(具体来说是反馈到其中的released字段)。因为下次再从slot为日志项分配缓冲区的时候需要用到。
这样,在以后即可简单判断出所有已分配的缓冲区是否被全部写入。只需要从slot->slot_state中解码出joined和released字段,并比较是否相等即可。
slot数据写日志文件
将slot数据写入日志文件有两种途径:
- 在用户请求线程中执行
- 在后台线程中执行
在用户请求线程中执行:
/*
* __log_write_internal --
* Write a record into the log.
*/
static int
__log_write_internal(WT_SESSION_IMPL *session, WT_ITEM *record, WT_LSN *lsnp,
uint32_t flags)
{
......
/*
* 如果slot完成了(被冻结了且该写的数据都已经写完了)
* 即可release了,直接在用户请求线程中执行slot内容刷盘
*/
if (WT_LOG_SLOT_DONE(release_size)) {
WT_ERR(__wt_log_release(session, myslot.slot, &free_slot));
if (free_slot)
__wt_log_slot_free(session, myslot.slot);
} else if (force) {
/* jeff.ding: 唤醒后台工作线程__log_server
* 由该工作线程来进行刷日志操作
*/
if (conn->log_cond != NULL) {
__wt_cond_signal(session, conn->log_cond);
__wt_yield();
} else
/* jeff.ding: 如果是强制刷盘,那么必须将slot内的所有WAL日志都写磁盘
* jeff.ding: 这可能会包括那些正在写入的事务的WAL日志,会产生问题么?
*/
WT_ERR(__wt_log_force_write(session, 1, NULL));
}
......
}
/*
* 后台线程工作流程
*
*/
static WT_THREAD_RET
__log_server(void *arg)
{
......
while (F_ISSET(conn, WT_CONN_SERVER_LOG)) {
/*
* 强制刷盘
*/
WT_ERR_BUSY_OK(__wt_log_force_write(session, 0, &did_work));
......
/* 等待下一次被唤醒 */
__wt_cond_auto_wait_signal(
session, conn->log_cond, did_work, NULL, &signalled);
}
......
}因此,可以看到,刷日志有两个调用路径__wt_log_force_write和__wt_log_release,下面一一分析之。
int
__wt_log_force_write(WT_SESSION_IMPL *session, bool retry, bool *did_work)
{
......
return (__wt_log_slot_switch(session, &myslot, retry, true, did_work));
}
/*
* 纵观__wt_log_slot_switch实现,最后也主要转化为三个主要调用
* __log_slot_close & __log_slot_new & __wt_log_release
* __log_slot_close负责冻结slot(不再接受join)
* __log_slot_new负责分配新的slot,避免由于老slot的冻结造成不可写
* __wt_log_release负责真正将日志项写回
*/
static int
__log_slot_switch_internal(
WT_SESSION_IMPL *session, WT_MYSLOT *myslot, bool forced, bool *did_work)
{
......
/*
* 冻结slot
*/
if (!F_ISSET(myslot, WT_MYSLOT_CLOSE)) {
ret = __log_slot_close(session, slot, &release, forced);
F_SET(myslot, WT_MYSLOT_CLOSE);
/*
* 标记slot可被release
*/
if (release)
F_SET(myslot, WT_MYSLOT_NEEDS_RELEASE);
}
/*
* 分配新的slot供写入线程join
*/
WT_RET(__log_slot_new(session));
F_CLR(myslot, WT_MYSLOT_CLOSE);
/*
* jeff.ding: 什么时候释放slot呢?
* 在myslot->flags的WT_MYSLOT_NEEDS_RELEASE被设置后才释放
* 而该标志位只会在一个地方被设置:👆的349行,即release为true的时候才设置该标志位
* 而release是在__log_slot_close函数中被修改
* 具体来说在函数__log_slot_close中,只有下面条件满足时才会修改release为true:
*
* if (WT_LOG_SLOT_DONE(new_state))
* *releasep = true;
*
* 而WT_LOG_SLOT_DONE成立的条件是
*
* #define WT_LOG_SLOT_DONE(state) \
* (WT_LOG_SLOT_CLOSED(state) && \
* !WT_LOG_SLOT_INPROGRESS(state))
*
* 即slot的状态被设置为CLOSE且未被其他人使用
* */
if (F_ISSET(myslot, WT_MYSLOT_NEEDS_RELEASE)) {
/*
* release slot
*/
WT_RET(__wt_log_release(session, slot, &free_slot));
F_CLR(myslot, WT_MYSLOT_NEEDS_RELEASE);
if (free_slot)
__wt_log_slot_free(session, slot);
}
return (ret);
}
/*
* jeff.ding: 释放slot的具体工作:
* 1. 将slot数据写入日志(这里可能只是写入了os)
* 2. 等待slot之前的slot数据写入日志文件(也是写入os即可)
* 3. 将日志文件刷磁盘(在SYNC标志位被设置的情况下)
*/
int
__wt_log_release(WT_SESSION_IMPL *session, WT_LOGSLOT *slot, bool *freep)
{
......
/* Write the buffered records */
// jeff.ding: 将slot缓冲区数据写入文件系统/磁盘
if (release_buffered != 0)
WT_ERR(__log_fs_write(session, slot, slot->slot_start_offset,
(size_t)release_buffered, slot->slot_buf.mem));
/*
* 如果调用者无需要求日志落盘,直接返回即可
*/
if (!F_ISSET(slot, WT_SLOT_FLUSH | WT_SLOT_SYNC | WT_SLOT_SYNC_DIR)) {
......
return (0);
}
/*
* Wait for earlier groups to finish, otherwise there could
* be holes in the log file.
* jeff.ding: 为什么接下来的操作要等到该slot之前的数据写入完成后才开始进行呢?
* jeff.ding: 其实本质上日志文件并非顺序追加,由于每个线程可以独立地写自己的slot
* jeff.ding: 每个slot写到其在日志文件中应该所在的position
* jeff.ding: 如果不等到该slot之前的slot写完成,接下来sync的时候会出现hole
* -------- --------
* | slot-1 | slot-2 |
* -------- --------
* -----------------
* | |////////| log_file
* -----------------
* jeff.ding: 如果slot-2的数据先写入日志文件,而slot-1数据尚未写入, 接下来sync以后
* jeff.ding: 日志文件slot-1的部分就会是空洞,可能会对正确性造成影响
*/
__log_wait_for_earlier_slot(session, slot);
log->write_start_lsn = slot->slot_start_lsn;
log->write_lsn = slot->slot_end_lsn;
// jeff.ding: 唤醒其他在__log_wait_for_earlier_slot中等待log->write_lsn的线程
// jeff.ding: 表示write_lsn已经被更新
__wt_cond_signal(session, log->log_write_cond);
/*
* Try to consolidate calls to fsync to wait less. Acquire a spin lock
* so that threads finishing writing to the log will wait while the
* current fsync completes and advance log->sync_lsn.
* jeff.ding: 这里需要等待老的日志文件sync完成
* jeff.ding: 否则,旧的日志文件数据没同步新的日志文件就写下去了,会产生正确性错误
* jeff.ding: 试想一下,在重启回放日志的时候,如果前一个日志文件的日志不完整的时候就回放了后面的事务日志,是有可能会出错的
*/
while (F_ISSET(slot, WT_SLOT_SYNC | WT_SLOT_SYNC_DIR)) {
if (log->sync_lsn.l.file < slot->slot_end_lsn.l.file ||
__wt_spin_trylock(session, &log->log_sync_lock) != 0) {
__wt_cond_wait(
session, log->log_sync_cond, 10000, NULL);
continue;
}
sync_lsn = slot->slot_end_lsn;
if (F_ISSET(slot, WT_SLOT_SYNC_DIR) &&
(log->sync_dir_lsn.l.file < sync_lsn.l.file)) {
WT_ERR(__wt_fsync(session, log->log_dir_fh, true));
log->sync_dir_lsn = sync_lsn;
}
if (F_ISSET(slot, WT_SLOT_SYNC) &&
__wt_log_cmp(&log->sync_lsn, &slot->slot_end_lsn) < 0) {
WT_ERR(__wt_fsync(session, log->log_fh, true));
log->sync_lsn = sync_lsn;
// jeff.ding: 同样,这里唤醒等待sync_lsn推进的线程
__wt_cond_signal(session, log->log_sync_cond);
}
}
......
}__wt_slot_release的逻辑相对比较简单,无非是将slot的日志项数据写入磁盘并调用fsync确保数据写入磁盘而非还在OS的page cache内。
这里的关键在于写入并fsync该slot的数据前必须等待该slot之前的日志项数据都已经完整写入。这是通过log对象的几个成员变量的同步等待来实现的,比如write_lsn、sync_lsn等。
总之,从函数__wt_slot_release正确退出时,可以保证该slot以及其之前的slot数据都已经被完整写入。
关键问题
slot的close和release
在wiredtiger的slot管理中,存在着slot_close和slot_release两种语义:区别是:
slot_close: 冻结slot,其含义是该slot不再接受新的日志项写入空间申请(在__wt_lot_slot_join中会检查),但对于已经申请过空间并正在进行日志项数据拷贝的事务不受影响。需要说明的是:一旦slot被冻结,那么全局的log->alloc_lsn会向前推进至该slot->end_lsn。意味着后面创建的slot的start_lsn必须从被冻结的slot以后开始分配,避免LSN被重用。
slot_release:释放slot,其含义是将该slot内的日志项数据全部刷盘并释放该slot,该slot可以被再次分配给其他事务所用。需要说明的是:release slot需要等到该slot真的不再有任何人使用(包括被CLOSE且已分配出去的WAL日志缓存全部已写入完成(通过比较slot_state中的joined和released字段即可))
大事务日志处理
需要考虑一种情况:如果一个事务的更新较多导致日志过大,超过了slot能容纳的上限,该如何处理?
最简单的处理办法应该是:对于此类大事务就不应该占用slot内的缓冲区,直接写日志是最合适的选择。我们看看wiredtiger如何实现。
需要关注的地方有两个:
- 从slot为事务日志分配写入缓冲区,具体是函数__wt_log_slot_join
- 向slot内拷贝日志项,具体来说是函数__wt_log_fill
void
__wt_log_slot_join(WT_SESSION_IMPL *session, uint64_t mysize,
uint32_t flags, WT_MYSLOT *myslot)
{
......
/*
* jeff.ding: 如果日志项大小超过WT_LOG_SLOT_BUF_MAX(slot缓冲区总大小的一半,128KB)
* 则设置标记WT_MYSLOT_UNBUFFERED
*/
if (mysize > WT_LOG_SLOT_BUF_MAX) {
unbuffered = true;
F_SET(myslot, WT_MYSLOT_UNBUFFERED);
}
for (;;) {
......
if (WT_LOG_SLOT_OPEN(old_state)) {
/*
* Try to join our size into the existing size and
* atomically write it back into the state.
*/
flag_state = WT_LOG_SLOT_FLAGS(old_state);
released = WT_LOG_SLOT_RELEASED(old_state);
join_offset = WT_LOG_SLOT_JOINED(old_state);
/*
* jeff.ding:
* WT_LOG_SLOT_UNBUFFERED = (WT_LOG_SLOT_BUF_SIZE << 1)
* join_offset永远不会超过WT_LOG_SLOT_UNBUFFERED
*/
if (unbuffered)
new_join = join_offset + WT_LOG_SLOT_UNBUFFERED;
else
new_join = join_offset + (int32_t)mysize;
new_state = (int64_t)WT_LOG_SLOT_JOIN_REL(
(int64_t)new_join, (int64_t)released,
(int64_t)flag_state);
......
}
}
......
}对于超过WT_LOG_SLOT_BUF_MAX(128KB)的日志项,选择直接使用unbuffered路径,会将myslot设置标记位WT_MYSLOT_UNBUFFERED,同时,将slot_state的join字段设置为join_offset + WT_LOG_SLOT_UNBUFFERED(512KB),由于join_offset的总大小只有256KB,因此,这就相当于将join_offset的高位置1,可能后面会有用途吧,暂时不得而知。
接下来看看函数__wt_log_fill
int
__wt_log_fill(WT_SESSION_IMPL *session,
WT_MYSLOT *myslot, bool force, WT_ITEM *record, WT_LSN *lsnp)
{
......
if (!force && !F_ISSET(myslot, WT_MYSLOT_UNBUFFERED))
memcpy((char *)myslot->slot->slot_buf.mem + myslot->offset,
record->mem, record->size);
else
WT_ERR(__log_fs_write(session, myslot->slot,
myslot->offset + myslot->slot->slot_start_offset,
record->size, record->mem));
......
}可以很简单就知道:如果myslot被设置为WT_MYSLOT_UNBUFFERED,那么便不会将日志项拷贝至slot缓冲区,而是直接写日志文件。
另外,为了保证该日志项之前的日志先写入,在__wt_log_slot_join和__wt_log_fill之间还必须将slot内的内容先写入磁盘:
static int
__log_write_internal(WT_SESSION_IMPL *session, WT_ITEM *record, WT_LSN *lsnp,
uint32_t flags)
{
...
__wt_log_slot_join(session, rdup_len, flags, &myslot);
/* 如果对当前日志项启用了unbuffered
* 那还必须先将slot内的已写入日志项写入日志文件
*/
if (myslot.end_offset >= WT_LOG_SLOT_BUF_MAX ||
F_ISSET(&myslot, WT_MYSLOT_UNBUFFERED) || force)
ret = __wt_log_slot_switch(session, &myslot, true, false, NULL);
if (ret == 0)
// jeff.ding: 向slot写入WAL日志内容
// jeff.ding: 这里同时为WAL日志项分配LSN
// jeff.ding: 将force参数设置为false表示只需要将record写入slot缓冲区即可
ret = __wt_log_fill(session, &myslot, false, record, &lsn);
}可见,在两者之间确实插入了一个__wt_log_slot_switch操作,以保证日志的顺序性。
日志项部分写问题
日志文件的写入是以slot为单位进行的,即会将slot数据整体写入而非只写入的其中某一个日志项,而slot内可能会容纳多个事务的更新日志,那其中某个事务的提交可能会将其他事务的日志一起写入日志文件,但此时其他事务的日志并非完整的,该如何处理这种情况?
通过以上的日志项写入流程分析,尤其是slot日志刷盘流程分析可以知道,当一个slot需要被写入磁盘时,是需要等到slot内所有已分配小的日志项缓存全部被写入完成后才可以真正地将slot数据一次性写入磁盘(参考slot->slot_state的joined和released)。这样就避免了事务日志的部分写入问题。
总结
通过上面的详细分析,可以总结一下,WiredTiger的WAL日志实现具有以下较为明显的特点:
1. 无锁实现,任何对slot的并发操作都是无锁,如:__wt_log_slot_join,__log_slot_close等,每个slot数据结构中的slot_state是实现无锁编程的核心所在;
2 多事务日志的批量写入(slot,大小256KB),提升日志IO吞吐;
3. 日志文件预分配
4. slot pool加速分配新的slot