テストステ論

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

(writeboost report) writeboost_mapの詳細

昨日, writeboost_mapリファクタリングした. writeboostの.mapフックである.

I/Oの本質的な部分であるので, 変な分割をすると性能の上限が下がる可能性があり, リファクタリングを極力放置していた(それでも, 部分的にはリファクタリングをして200行くらいにはしていたが). コードが大体フィックスされたので, 最後の大きな仕事として, 昨日一気にやった. 結果に非常に満足しているため, コードを紹介しようと思う.

より詳細に知りたい人はコードを見て欲しい.
https://github.com/akiradeveloper/dm-writeboost

static int writeboost_map(struct dm_target *ti, struct bio *bio)
{
    struct wb_device *wb = ti->private;

    struct per_bio_data *map_context;
    map_context = dm_per_bio_data(bio, ti->per_bio_data_size);
    map_context->ptr = NULL;

    DEAD(
        bio_endio(bio, -EIO);
        return DM_MAPIO_SUBMITTED;
    );

    if (bio->bi_rw & REQ_DISCARD)
        return process_discard_bio(wb, bio);

    if (bio->bi_rw & REQ_FLUSH)
        return process_flush_bio(wb, bio);

    return process_bio(wb, bio);
}

美しい. 芸術だ. 整然としている.

  • DEADというのは, 閉塞時のコード. ライトブーストは, 使うデバイスのいずれかが一度failすると速攻で閉塞する.
  • discardについては, backingだけに発行する. キャッシュ上のdiscardは非常に難しい. また, discardするデータというのは, 書いてからだいぶ時間が経っているはずであろうから, 大抵の場合はライトバックされている. 従って, backingだけに発行して十分なのである.
  • flushは, plogがない場合はackを遅延する. plogがある場合は, SSDの永続処理をしてackしてforegroundでackする.
  • process_bioが本質的なコードである.
static int process_bio(struct wb_device *wb, struct bio *bio)
{
    return io_write(bio) ? process_write(wb, bio) : process_read(wb, bio);
}

なんと, process_bioは, writeとreadで関数を振り分けてるだけである. この中で独立にmutexをとるコードになっているため, rwセマフォを使う形に簡単に書き換えることが可能である. ライトブーストの本質はライトにあるから, コードとしても明確に分離する方が理解しやすくなる意味がある(もともとは一つの関数内でフロー上は分離されている感じだった).

static int process_read(struct wb_device *wb, struct bio *bio)
{
    struct lookup_result res;
    u8 dirty_bits;

    mutex_lock(&wb->io_lock);
    cache_lookup(wb, bio, &res);
    mutex_unlock(&wb->io_lock);

    if (!res.found) {
        bio_remap(bio, wb->origin_dev, bio->bi_sector);
        return DM_MAPIO_REMAPPED;
    }

    dirty_bits = read_mb_dirtiness(wb, res.found_seg, res.found_mb);
    if (unlikely(res.on_buffer)) {
        if (dirty_bits)
            migrate_buffered_mb(wb, res.found_mb, dirty_bits);

        atomic_dec(&res.found_seg->nr_inflight_ios);
        bio_remap(bio, wb->origin_dev, bio->bi_sector);
        return DM_MAPIO_REMAPPED;
    }

    /*
    * We must wait for the (maybe) queued segment to be flushed
    * to the cache device.
    * Without this, we read the wrong data from the cache device.
    */
    wait_for_flushing(wb, res.found_seg->id);

    if (likely(dirty_bits == 255)) {
        struct per_bio_data *map_context =
            dm_per_bio_data(bio, wb->ti->per_bio_data_size);
        map_context->ptr = res.found_seg;

        bio_remap(bio, wb->cache_dev,
              calc_mb_start_sector(wb, res.found_seg, res.found_mb->idx) +
              io_offset(bio));
    } else {
        migrate_mb(wb, res.found_seg, res.found_mb, dirty_bits, true);
        cleanup_mb_if_dirty(wb, res.found_seg, res.found_mb);

        atomic_dec(&res.found_seg->nr_inflight_ios);
        bio_remap(bio, wb->origin_dev, bio->bi_sector);
    }
    return DM_MAPIO_REMAPPED;
}

cache_lookupは, 結果をresに格納する. この関数はprocess_writeにおいても使われる. やっていることは, 「ヒットしたのか?」「RAMバッファでヒットしたのか?」「4kまるまるヒットしたか?」という条件分岐である.

static int process_write(struct wb_device *wb, struct bio *bio)
{
    struct write_pos pos;
    prepare_write_pos(wb, bio, &pos);
    return write_on_devices(wb, bio, &pos);
}

process_writeはこれまた芸術である. write処理を

  1. 書く場所を決める
  2. 書く

の2つの処理に分離出来ることを発見した. そして, 前者でしかmutexをとっていない. Haskellのように型の下に何もかも隠蔽してしまう高級な関数型プログラミングなどであればこの分離は自明であろう. しかし, ロックが関わり, ストレージへの副作用を隠蔽しながら, C言語でこれを実現するのは簡単ではない.

static void prepare_write_pos(struct wb_device *wb, struct bio *bio,
                  struct write_pos *pos)
{
    struct lookup_result res;

    mutex_lock(&wb->io_lock);

    /*
    * for design clarity, we insert this function here right after mutex is taken.
    * making the state valid before anything else is always a good practice in the
    * in programming.
    */
    might_queue_current_buffer(wb, bio);

    cache_lookup(wb, bio, &res);

    if (res.found) {
        pos->mb = res.found_mb;
        if (unlikely(res.on_buffer)) {
            pos->plog_head = advance_plog_head(wb, bio);
            mutex_unlock(&wb->io_lock);
            return;
        } else {
            invalidate_previous_cache(wb, res.found_seg, res.found_mb,
                          io_fullsize(bio));
            atomic_dec(&res.found_seg->nr_inflight_ios);
        }
    }

    pos->plog_head = advance_plog_head(wb, bio);
    pos->mb = wb->current_seg->mb_array + mb_idx_inseg(wb, advance_cursor(wb));
    BUG_ON(pos->mb->dirty_bits);

    ht_register(wb, res.head, pos->mb, &res.key);

    atomic_inc(&wb->current_seg->nr_inflight_ios);
    mutex_unlock(&wb->io_lock);
}

might_queue_current_bufferは, 今使っているバッファやplogを交換する必要があるかどうか判定し, あるならば交換する. この関数は非常に深い上, 異常なほどに美しい. 興味のある方はコードを参照されたい. 同時に, writeboost_ctrからも下っていくと, その交差点にある美しさに心打たれるだろう.

この関数がやっていることは, ルックアップをして, hit/missによって分岐しているだけである. 結果をposに格納する. posは, plog_head(plog上の書き込みセクタ)とmb(書き込むmetablock. metablockは4kキャッシュラインに相当したメタデータのこと)を持つ.

static int write_on_devices(struct wb_device *wb, struct bio *bio,
                struct write_pos *pos)
{
    taint_mb(wb, wb->current_seg, pos->mb, bio);

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

    append_plog(wb, pos->mb, bio, pos->plog_head);

    atomic_dec(&wb->current_seg->nr_inflight_ios);

    /*
    * deferred ACK for FUA request
    *
    * bio with REQ_FUA flag has data.
    * so, we must run through the path for usual bio.
    * And the data is now stored in the RAM buffer.
    */
    if (!wb->type && (bio->bi_rw & REQ_FUA)) {
        wbdebug("FUA");
        queue_barrier_io(wb, bio);
        return DM_MAPIO_SUBMITTED;
    }

    LIVE_DEAD(
        bio_endio(bio, 0);
        ,
        bio_endio(bio, -EIO);
    );

    return DM_MAPIO_SUBMITTED;
}
  • taint_mbは, metablockのdirtinessを更新する.
  • write_on_rambufferは, RAMバッファへデータを書き込む
  • append_plogは, plogへの追記を行う.
  • inflight_iosは, 前記したprepare_write_posでとったmutex内でincされている. これによって, mutexによるクリティカルセクションを短めに切り上げることが出来るというロックのテクニックである. writeパスをすべてmutexとるのはスループットに多大な影響がある.
  • もし, plogなし(!wb->type)であれば, FUAのackは遅延される.
  • そうでなければそのままackする.

ちなみに, ライトブーストは, test/以下にテストを持っている. $ sh build.shしたあとに# runtest.shをすると実行されるはずである. リグレッションテストがないとストレージのコードはいじれない. このテスト, 何度もライトブーストのバグを発見してきたから, 結構的を得ているテストなのだと思う. 開発に参加したい人はぜひ, プルリクを送る前にテストを実行して欲しい.