mdadmの検証中に発見したバグと今後の取り組み

はじめに

こんにちは、技術顧問の武内です。

Linuxにはmultiple devices(以下md)と呼ばれるソフトウェアRAID機能があります。この機能はmdadmというツールを使って管理します。サイボウズのSREチームは、Ubuntu16.04のmdadmを検証をした際に次のような2つのバグを発見しました。

  • mdのresync*1時に使うwrite intent bitmap*2(以後bitmapと表記)のサイズが所定量を超える場合、mdを構成するストレージデバイスに不良セクタを検出した際にbitmapを破壊する
  • mdのサイズ拡張時にbitmapの付与に失敗する

これに関して、本エントリでは、次のようなことをお伝えしたいと思います。

  • これらのバグが具体的にどういうものなのか
  • サイボウズはこのよう場合に、自分たちが使うものだけを修正するのではなくupstreamのOSSを修正するという方針
  • どういう思考プロセスに基づいてどのような流れで調査したのか

検出したバグ

バグ1: mdのresync時に使うbitmapのサイズが所定量を超える場合、mdを構成するデバイスに不良セクタを検出した際にbitmapを破壊する

影響

resync(正確にはmdから一度取り外したデバイスを再度追加する際のresync)をすると、resyncが正常終了するものの、実際には不正な状態になります。

原因

Ubuntu 16.04 から追加されたbad block list(以下bblと記載)というデータの配置位置の誤りによって発生しました。

bblはmdを構成する各デバイスに対して存在し、それぞれのデバイスに対するI/O時に見つかった不良セクタの一覧です。bitmap領域はデバイス先頭から8KB地点に配置されます。その一方、 bblはbitmap領域の後に配置されます。ただし、bitmap領域のサイズによらず、必ず同じ位置(デバイス先頭から40KB地点)に配置されます。このため、bitmap領域のサイズが32KBを超えると、bblへの書き込みが発生した際にbitmapを破壊します。

bitmapのサイズは次のようにして求められます。

256(bitmap領域のヘッダサイズ) + mdのサイズ/((bitmap内の1bitに対応する領域のサイズ(bitmap chunk size)。デフォルトは64MB)*8)

回避方法

以下のいずれかの方法を使います。

  • bitmapを使わない
  • bblを使わない
  • mdを構成するすべてのデバイスについて、次のような手順によってbblを再作成する
# mdadm /dev/md0 --fail /dev/sdc --remove /dev/sdc --re-add /dev/sdc --update=bbl

修正状況

upstreamにおいて修正済(commit 1b7eb672f7792313cc1517feaae8267575fc496b)です。Ubuntuでは17.10において修正済です。

今後の取り組み

Ubuntu 16.04に対してupstreamの修正をバックポートします。

バグ2: mdのサイズ拡張時にbitmapの付与に失敗する

影響

bitmap領域を使うことによるresyncのI/O量削減ができない。

原因

mdのサイズ拡張に伴ってbitmapのサイズを拡張する際に、bitmapのために使える予約領域(以後bitmap予約領域と記載)が少ないことが原因です。

bitmap予約領域のサイズはmdadm -Eコマンドによって求められます。以下20TBのmdを構成するデバイスの一つ、/dev/sdbについての値を計算する例です。

# mdadm -E /dev/sdb
...
Internal Bitmap : 8 sectors from superblock                      # (1)
... 
  Bad Block Log : 512 entries available at offset 96 sectors     # (2)
...

(2)の行の96から(1)の行の8を引いた88がbitmap予約領域のセクタ単位のサイズです。セクタの大きさは512バイトなので、バイト単位に直すと44KBです。bitmap chunk sizeを64MBとすると

64*1024**2*44*1024*8 = 24189255811072 [バイト]

約22TBほどまでしか拡張の余地が無いことがわかります。

修正状況

upstreamにおいても未修正です。Ubuntu 16.10以降にも同じバグが存在します。

回避方法

後述するbad block listを無効にすれば、bitmap予約領域は最大128KBまで増やせます。

今後の取り組み

upstreamのmdadmに対して修正を作成して、取り込んでもらう予定です。

サイボウズはOSSにフリーライドするのではなく、このような場合は積極的にupstream版を修正するという方針があります。過去にもMySQLやNginxを修正したという実績があります。

調査の流れ

注意: やったことを事細かに書くのではなく、重要な点に絞って、思考プロセスに重きを置いて説明します。

検証環境

  • ソフトウェアのバージョン

    • Ubuntu: 16.04
    • mdadm: 3.3
  • ストレージの構成

    • 2つのiSCSIデバイス(以下/dev/sd[bc]と記載)上に構築したRAID1のmd(以下/dev/md0と記載)
    • /dev/sd[bc]のサイズはそれぞれ20TB

バグ1の検出

/dev/md0を作成した後にbitmap領域のサイズとbbl領域の位置を確認すると、辻褄が合わないことがわかりました。

# ./mdadm --create /dev/md0 --level=raid1 --bitmap=internal --size=$((20*1024))G --raid-devices 2 /dev/sdb /dev/sdc
...
# mdadm -E /dev/sdc
...
Internal Bitmap : 8 sectors from superblock                          # bitmap領域の位置
...
  Bad Block Log : 512 entries available at offset 72 sectors  # bblの位置
       Checksum : d2f4290b - correct
...

mdを構成する全てのデバイスには、先頭から4KBの時点に、サイズ4KBのsuperblockというデータが仕様上存在します。bitmap領域、およびbbl領域のsuperblock先頭からの相対的な位置はsuperblock内の所定の場所に記録されています。それによると、bitmap領域のために使えるのは72-8=64セクタ、つまり32KBだということがわかります。

しかし、bitmapの実際のサイズを前述の式によって計算してみると、32KBを超えていることがわかりました。

$ ruby -e "puts 256 + 20*1024**4/(64*1024**2*8)"
41216                                          # 約40KB。81セクタを占める

これは、/dev/sdcに対するI/Oにおいてbblにデータが書き込まれると、bitmapの32KB以降のデータが破壊されることがわかります。

バグ1の修正状況確認

問題があることはわかったので、次はupstreamのmdadmにおいて問題が修正されているかどうかを確認することにしました。

$ git clone git://git.kernel.org/pub/scm/utils/mdadm/mdadm.git
...
$ cd mdadm
$ make -j16 >/dev/null
...
$ sudo
# ./mdadm --create /dev/md0 --level=raid1 --bitmap=internal --size=$((20*1024))G --raid-devices 2 /dev/sdb /dev/sdc
# mdadm -E /dev/ram1
...
Internal Bitmap : 8 sectors from superblock
...
  Bad Block Log : 512 entries available at offset 96 sectors
...
# 

今度はbitmap領域とbbl領域が重なっていませんでした。

次に、具体的にどのような修正なのかを確認しました。Ubuntu 16.04のmdadmはバージョン3.3にいくつかパッチを当てたものなので、mdadmの当該バージョンからHEADまでの間に修正が存在するかどうかを見ました。すべてのcommitを調査するのは大変なので、commit logのフィルタリングによって一次調査するcommitを絞り込みます。

$ git log --oneline mdadm-3.3.. | grep -i "bad.*block.*log"
e4467bc imsm: 4kn support for bad block log
c07a5a4 imsm: clear bad block from bad block log
6f50473 imsm: record new bad block in bad block log
bbab094 imsm: write bad block log on metadata sync
8d67477 imsm: parse bad block log in metadata on startup
1b7eb67 super1: fix setting bad block log offset in write_init_super1()                                                                                                                      86a406c super1: Do not create bad block log for clustered devices.                                                                                                                           968d2a3 md.4: replace "bad block log" with "bad block list"
$ 

怪しそうなcommit(1b7eb67 super1: fix setting bad block log offset in write_init_super1())を見つけたので、詳細調査することにしました。

$ git show 1b7eb67
commit 1b7eb672f7792313cc1517feaae8267575fc496b
Author: Artur Paszkiewicz <artur.paszkiewicz@intel.com>
Date:   Thu Nov 10 11:50:54 2016 +0100

    super1: fix setting bad block log offset in write_init_super1()

    Commit f79bbf4f6904 ("super1: don't put the bblog at the end of the free
    space.") changed the location of the bad block log to be after the
    write-intent bitmap, but a fixed offset was used and it can make bbl
    overlap with the bitmap, especially when using a small bitmap chunk.
    This patch changes it to use the actual offset and size of the bitmap.
    It also joins the cases for v1.1 and v1.2 superblock because the code
    was very similar.

何やらそれらしい修正です。この後具体的にソース調査をした結果、このパッチで間違いないことを確認しました。このバグについてはupstreamに存在するpatchをバックポートすれば解決しそうだということがわかりました。

バグ2の検出

バグ1の修正パッチをバックポートしたものを適用したmdadmを使って次のようなmdのリサイズ処理の検証をしました。

  1. bitmap(後述)を無効化: mdadm --grow /dev/md0 --bitmap=none
  2. iSCSIデバイスの容量を拡張: 今回の場合は4TB拡張
  3. mdのリサイズ: mdadm --grow /dev/md0 --size=max
  4. bitmapの再有効化: mdadm --grow /dev/md0 --bitmap=internal

すると、処理4において次のようなエラーメッセージを出して異常終了しました。

failed to create internal bitmap - chunksize problem.

ソース調査

このメッセージでソースを検索すると、Grow.c内のGrow_addbitmap()という関数内で失敗していることがわかりました。

...
                                        if (st->ss->add_internal_bitmap(
                                                    st,
                                                    &s->bitmap_chunk, c->delay, s->write_behind,
                                                    bitmapsize, offset_setable,
                                                    major)
                                                )                                              # この関数が失敗した
                                                st->ss->write_bitmap(st, fd2);
                                        else {
                                                pr_err("failed to create internal bitmap"
                                                       " - chunksize problem.\n");             # このメッセージが出た
                                                close(fd2);
                                                return 1;
                                        }
...

さらに調査したところ、st->ss->add_internal_bitmap()というのはsuper1.c内のadd_internal_bitmap1()という関数だということ、およびその中で呼ばれるbitmap予約領域の最大サイズは128KBに制限されていることがわかりました。

static int
add_internal_bitmap1(struct supertype *st,
                     int *chunkp, int delay, int write_behind,
                     unsigned long long size,
                     int may_change, int major)
{
...
                if (creating) {
                        offset = 4*2;
                        room = choose_bm_space(__le64_to_cpu(sb->size));      # ここでbitmap予約領域を計算
                        bbl_size = 8;
...
...
static unsigned long choose_bm_space(unsigned long devsize)
{                                                                                                                                                                                                    
        /* if the device is bigger than 8Gig, save 64k for bitmap usage,
         * if bigger than 200Gig, save 128k
         * NOTE: result must be multiple of 4K else bad things happen
         * on 4K-sector devices.
         */                                                                                                                                                                                          
        if (devsize < 64*2) return 0;
        if (devsize - 64*2 >= 200*1024*1024*2)
                return 128*2;
        if (devsize - 4*2 > 8*1024*1024*2)
                return 64*2;
        return 4*2;
}
...

さらにbblはbitmap予約領域の後に配置されるのではなく、上記add_internal_bitmap1()の後に呼ばれるwrite_init_super1()において、現在のbitmap領域の直後に配置されることがわかりました。

static int write_init_super1(struct supertype *st)
{ 
        ...
                /* work out how much space we left for a bitmap */
                if (sb->feature_map & __cpu_to_le32(MD_FEATURE_BITMAP_OFFSET)) {        # ここは真
                        bitmap_super_t *bms = (bitmap_super_t *)
                                        (((char *)sb) + MAX_SB_SIZE);
                        bm_space = calc_bitmap_size(bms, 4096) >> 9;                    # bm_spaceは現在のbitmapのサイズ
                        bm_offset = (long)__le32_to_cpu(sb->bitmap_offset);
                } else {
                        bm_space = choose_bm_space(array_size);
                        bm_offset = 8;
                }
        ...
                               sb_offset = st->minor_version == 2 ? 8 : 0;
                        sb->super_offset = __cpu_to_le64(sb_offset);
                        if (data_offset == INVALID_SECTORS)
                                data_offset = sb_offset + 16;

                        sb->data_offset = __cpu_to_le64(data_offset);
                        sb->data_size = __cpu_to_le64(dsize - data_offset);
                        if (data_offset >= sb_offset+bm_offset+bm_space+8) {            # ここは真
                                sb->bblog_size = __cpu_to_le16(8);
                                sb->bblog_offset = __cpu_to_le32(bm_offset +
                                                                 bm_space);             # bitmap予約領域ではなく現在のbitmapのサイズをもとにbblのオフセットを計算している

          ...
}

upstreamにおける修正確認

upstreamのmdadmにおいて実機確認をしたところ、Ubuntu16.04と同じ結果になりました。このため、このバグは未修正であることがわかりました。

修正案の検討

仕様(RAID superblock formats - Linux Raid Wiki)の確認によって、bblはどこに配置してもいいことがわかりました。この仕様の話と、これまでの調査の結果から、bblがbitmap領域の直後ではなく、bitmap予約領域の後に配置されるようにするという改善案が考えられます。詳細については今後upstreamのmdadmの開発者達と協議して決める予定です。