テストステ論

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

(dm-lc report) キャッシュデバイスのフォーマット処理性能改善

dm-lcは, キャッシュのメタデータ(backing storeのブロックとキャッシュブロックのマッピングなど)をキャッシュデバイスに吐くことによって, 障害時にもデータロストしない性質を得ています. dm-lcを使いはじめる時には, キャッシュデバイス上に一定間隔に配置されたメタデータ領域(各4KB)を適切に初期化する必要があります. この処理が遅いことが問題でした. この問題をクリアしたので報告します.

従来の処理は, nr_segments個あるメタデータ領域を先頭から順に同期的にWRITE_FUAフラグをつけて書き込むというものでした. しかしこれは, VM環境ではありますが, たった3GBのキャッシュデバイスを初期化するのに6秒かかっていました. lc-format-cacheというのが, デバイスのフォーマット処理であり, lc-resume-cacheはフォーマットされたメタデータ領域を読み込んでいき, 実行時に利用するRAM上のメタデータを構築する処理です. 実は後者はこの実験では, キャッシュヒットをしてしまってる可能性がある(とはいえ, キャッシュヒットしてWRITE_FUAライトの15倍なわけがなかろうという気もしますが)ので正確な測定とは言いがたいですが, 3GBで6秒ということは300GBでは600秒(10分)かかるということですから, 実運用的には「どの道フォーマットは一回しかしないのだから」問題ないとはいえ, 測定などで毎回フォーマットする開発者の(主に心の)負担を軽くするためには, 短縮したいところです. format_cache_device関数の中で閉じる話ですし, 実装としてそこまで難しいことをせずに改善が可能と考えたのでいつもどおり一気にやってしまいました.

#lc-format-cache
real    0m6.371s
user    0m0.032s
sys     0m0.088s

#lc-resume-cache
real    0m0.445s
user    0m0.024s
sys     0m0.064s

新しい処理方式は, すべてのメタデータ領域に対していっぺんに非同期WRITEを送ってしまい, 処理すべきI/O数が全部decされるまで待つという典型的な手法です. キャッシュデバイスからbacking storeへのmigration処理でも似たようなことをしており, コードを検討する必要があまりないことも利点です.

結果, 8倍ほど高速化しました.

#lc-format-cache
real    0m0.820s
user    0m0.020s
sys     0m0.016s

#lc-resume-cache
real    0m0.458s
user    0m0.016s
sys     0m0.084s

参考までに, 以下がコード差分です.

--- a/Driver/dm-lc.c
+++ b/Driver/dm-lc.c
@@ -1491,6 +1491,16 @@ static size_t calc_nr_segments(struct dm_dev *dev)
        return devsize / (1 << LC_SEGMENTSIZE_ORDER) - 1;
 }

+struct format_segmd_context {
+       atomic64_t count;
+};
+
+static void format_segmd_endio(unsigned long error, void *__context)
+{
+       struct format_segmd_context *context = __context;
+       atomic64_dec(&context->count);
+}
+
 static void format_cache_device(struct dm_dev *dev)
 {
        size_t nr_segments = calc_nr_segments(dev);
@@ -1512,13 +1522,17 @@ static void format_cache_device(struct dm_dev *dev)
        dm_safe_io_retry(&io_req_sup, &region_sup, 1, false);
        kfree(buf);

+       struct format_segmd_context context;
+       atomic64_set(&context.count, nr_segments);
+
        size_t i;
        buf = kzalloc(1 << 12, GFP_KERNEL);
        for(i=0; i<nr_segments; i++){
                struct dm_io_request io_req_seg = {
                        .client = lc_io_client,
-                       .bi_rw = WRITE_FUA,
-                       .notify.fn = NULL,
+                       .bi_rw = WRITE,
+                       .notify.fn = format_segmd_endio,
+                       .notify.context = &context,
                        .mem.type = DM_IO_KMEM,
                        .mem.ptr.addr = buf,
                };
@@ -1530,6 +1544,12 @@ static void format_cache_device(struct dm_dev *dev)
                dm_safe_io_retry(&io_req_seg, &region_seg, 1, false);
        }
        kfree(buf);
+
+       while(atomic64_read(&context.count)){
+               schedule_timeout_interruptible(msecs_to_jiffies(100));
+       }
+
+       blkdev_issue_flush(dev->bdev, GFP_KERNEL, NULL);
 }

以上です. この前の平安京ビューでもnumpyを使うことで性能改善を行いましたが, 性能改善は常に気持ちが良いものです. 性能改善で重要なのもやはり, バランスです. 得られた性能向上による価値に対してコードがあまりに穢れてしまっては, 後の柔軟性を失うだけです. 検討をして, 自分で手を一気に動かせなければ, 何も出来ないどころか邪魔だ. そう思います.