WiredTiger存储引擎-WAL日志之关键流程

793 阅读15分钟
原文链接: zhuanlan.zhihu.com

说明

由于知乎这个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:63

WAL日志落盘有几个关键的过程:

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数据写入日志文件有两种途径:

  1. 在用户请求线程中执行
  2. 在后台线程中执行

在用户请求线程中执行:

/*
 * __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_lsnsync_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如何实现。

需要关注的地方有两个:

  1. 从slot为事务日志分配写入缓冲区,具体是函数__wt_log_slot_join
  2. 向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