テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

(writeboost report) Persistent Loggingのコーディングを行った

http://akiradeveloper.hatenadiary.com/entry/2014/02/11/193351 の続き

とりあえず, 書くだけ書いてコンパイルを通した. 大体トータルでは600行くらいの追加になった. 結構大変であった. 結果として, トータルでは大体4300行くらいのコードになってしまった. ふつうのキャッシュのようにHDDとSSDだけの関係に閉じずRAM bufferがもともとあったところにさらに永続ログを追加したため, コードはともかく実現していることはかなり複雑である. とりあえず, 機能的に幹の部分は全部やりきったという形になったため, 今後は瑣末なコーディングしかしないだろう. Persistent RAMを使うコードを入れてもプラス200行程度だろうと思う.

 Driver/dm-writeboost-metadata.c | 551 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
 Driver/dm-writeboost-metadata.h |   1 +
 Driver/dm-writeboost-target.c   | 276 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
 Driver/dm-writeboost.h          |  55 ++++++++++++--

構造体

struct wb_deviceには以下のメンバを追加した.

+       /********************
+        * Persistent Logging
+        ********************/
+
+       /* common */
+       char plog_dev_desc[16]; /* passed as essential argv to describe the persistent device */
+       wait_queue_head_t plog_wait_queue; /* wait queue to serialize writers */
+       sector_t plog_size; /* Const. the size of a plog in sector */
+       sector_t alloc_plog_head; /* next relative sector to allocate */
+       sector_t cur_plog_head; /* current relative sector to append */
+       sector_t plog_start_sector; /* the absolute start sector of the current plog */
+       void *plog_buf; /* 9 sector pre-allocated buffer for the plog write */
+       u32 nr_plogs; /* Const. number of plogs */
+
+       /* type 1 */
+       struct dm_dev *plog_dev_t1;

また, logのメタデータとして, 以下を追加した.

/*
 * plog = metadata (512B) + data (512B-4096B)
 * A plog contains a self-contained information of a accepted write.
 */

struct plog_meta_device {
        __le64 id;
        __le64 sector;
        __le32 checksum; /* checksum of the data */
        __u8 idx; /* idx in the segment */
        __u8 len; /* length in sector */
        __u8 padding[512 - 8 - 8 - 4 - 1 - 1];
} __packed;

ログ書き込み

永続ログには, ライト用のmutexをとった順に整列させる. つまり, ログを復帰させる時には, ログはびっしり詰まっていないことが保証されている. こういう実装にすることで得るものを重視した.

具体的にはこのようなコードによって永続ログにログを追加する. RAM bufferに書いたあとに, append_plogによってログを追加している. これはmutexの外のコードである. このあとに, ACKを返す.

        taint_mb(wb, wb->current_seg, mb, bio);

        write_on_rambuffer(wb, wb->current_seg, mb, bio);

        append_plog(wb, mb, bio, plog_head);

append_plogは, ログの先頭cur_plog_headを持ったライトしかdo_append_logに進ませない. これによって, ログに順番に書かれることを保証する.

static void
append_plog(struct wb_device *wb, struct metablock *mb,
            struct bio *bio, sector_t plog_head)
{
        int r = 0;

        if (!wb->type)
                return;

        wait_event_interruptible(wb->plog_wait_queue,
                        wb->cur_plog_head == plog_head);

        do_append_plog(wb, mb, bio, plog_head);
        wb->cur_plog_head += (1 + io_count(bio));

        IO(blkdev_issue_flush(wb->cache_dev->bdev, GFP_NOIO, NULL));

        wake_up_interruptible(&wb->plog_wait_queue);
}

このplog_headという引数は, mutexの中で以下の与えられる.

static sector_t advance_plog_head(struct wb_device *wb, struct bio *bio)
{
        sector_t old = wb->alloc_plog_head;
        wb->alloc_plog_head += (1 + io_count(bio));
        return old;
}

一つのplogをあふれてしまうと思ったら, 次のplogを獲得する. そのコードは以下である. このコードはmutex上で実行される. 獲得のタイミングは, 新しいRAM bufferstruct rambufferの獲得と同じである. このコードは, acquire_new_segの最後, acquire_new_rambufferのあとに呼ばれる. セグメントと, RAM bufferと, plogの獲得は同じタイミングで行われるということでSimplicityを得ている.

static void acquire_new_plog(struct wb_device *wb, u64 id)
{
        u32 tmp32;

        if (!wb->type)
                return;

        wait_for_flushing(wb, SUB_ID(id, wb->nr_plogs));

        div_u64_rem(id - 1, wb->nr_plogs, &tmp32);
        wb->plog_start_sector = wb->plog_size * tmp32;
        wb->alloc_plog_head = 0;
        wb->cur_plog_head = 0;
}

ログのフラッシュ

fault後か否かに関わらず, デバイスの再生成時には, 永続ログをすべてキャッシュデバイス(SSD)に吐く. この後, キャッシュデバイスからのログリプレイ(セグメントを舐めて, データを復活させる)を行う.

具体的にはこういうコードである. ログリプレイアルゴリズムをキャッシュデバイス上で閉じさせる意味合いがある.

static int __must_check recover_cache(struct wb_device *wb)
{
        int r = 0;

        r = flush_plogs(wb);
        if (r) {
                WBERR("failed to write back all the persistent data on non-volatile RAM");
                return r;
        }

        r = replay_log_on_cache(wb);
        if (r) {
                WBERR("failed to replay log");
                return r;
        }

        prepare_first_seg(wb);
        return 0;
}

flush_plogsは, ログ上で最小のidを発見して, そこから順々にたどって, 永続ログ上のダーティをキャッシュデバイス上に書き出していく.

もっとも肝となるコードは以下である. rebuild_rambufferは, ある一つのplogをたどっていき, RAM bufferを復元する.

/*
 * Rebuild a RAM buffer (metadata and data) from a plog
 */
void rebuild_rambuf(void *rambuffer, void *plog_buf)
{
        struct segment_header_device *seg = rambuffer;
        struct metablock_device *mb;

        void *cur = plog_buf;
        while (true) {
                u8 i;
                u32 actual, expected;
                sector_t sector_cpu;
                size_t bytes;
                void *addr;

                struct plog_meta_device meta;
                memcpy(&meta, cur, 512);
                sector_cpu = le64_to_cpu(meta.sector);

                actual = crc32c(WB_CKSUM_SEED, cur + 512, meta.len << SECTOR_SHIFT);
                expected = le32_to_cpu(meta.checksum);

                if (actual != expected)
                        return;

                /* update header data */
                seg->id = meta.id;
                if ((meta.idx + 1) > seg->length)
                        seg->length = meta.idx + 1;

                /* metadata */
                mb = seg->mbarr + meta.idx;
                mb->sector = meta.sector;
                for (i = 0; i < meta.len; i++) {
                        mb->dirty_bits |= (1 << (do_io_offset(sector_cpu) + i));
                }

                /* data */
                bytes = do_io_offset(sector_cpu) << SECTOR_SHIFT;
                addr = rambuffer + ((1  + meta.idx) * (1 << 12) + bytes);
                memcpy(addr, cur + 512, meta.len << SECTOR_SHIFT);

                /* shift to the next "possible" plog */
                cur += ((1 + meta.len) << SECTOR_SHIFT);
        }

        /* checksum */
        seg->checksum = cpu_to_le32(calc_checksum(rambuffer, seg->length));
}

このコードはflush_plogで利用される. 前記rebuild_rambufを使ってRAM bufferを復元し, idに該当するキャッシュデバイス上のセグメントにこれをライトする(flush_rambuf).

/*
 * Flush a plog (stored in a buffer) to the cache device.
 */
static int flush_plog(struct wb_device *wb, void *plog_buf)
{
        int r = 0;
        struct segment_header *seg;
        void *rambuf;

        struct plog_meta_device meta;
        memcpy(&meta, plog_buf, 512);

        rambuf = kzalloc(1 << wb->segment_size_order, GFP_KERNEL);
        if (r)
                return -ENOMEM;
        rebuild_rambuf(rambuf, plog_buf);

        seg = get_segment_header_by_id(wb, le64_to_cpu(meta.id));
        r = flush_rambuf(wb, seg, rambuf);
        if (r)
                DMERR("failed to flush a plog");

        kfree(rambuf);
        return r;
}

その他

実装を追加すると, コードの設計は進化する (もしその追加が正しければ).

今回その効果が顕著だったのは, init_devicesをfactor out出来たことである. I/O用に利用するデバイスを初期化するという意味であるが, 前記したようにセグメント/RAM buffer/plogの獲得を同期させている設計思想とマッチしている. 初期化も同期させるという発想である. これは美しい. 新しいデバイスを追加したことによって気づいたというのが発見の理屈である.

static int init_devices(struct wb_device *wb)
{
        int r = 0;

        bool formatted = false;

        r = might_format_cache_device(wb, &formatted);
        if (r)
                return r;

        r = init_rambuf_pool(wb);
        if (r) {
                WBERR("failed to allocate rambuf pool");
                return r;
        }

        r = alloc_plog_dev(wb, formatted);
        if (r)
                goto bad_alloc_plog;

        return r;

bad_alloc_plog:
        free_rambuf_pool(wb);
        return r;
}