uchan note

プログラミングや電子工作の話題を書きます

mdadm の write-intent bitmap と bad block log を競合させてみる

本記事は Linux Advent Calendar 2017 の 4 日目の記事です.

概要

Linux のソフトウェア RAID の機能を管理するための mdadm コマンドにまつわるバグの再現実験を行います. そのバグとは,次の記事で紹介されている「バグ 1」のことです.

blog.cybozu.io

この記事で説明すること

主に次のことを説明します.バグの概要は上の記事を参照してください.

  • mdadm の軽い紹介
  • mdadm で RAID1 を構成した場合のディスク上のメタデータ配置
  • バグ 1 の再現方法

mdadm とは

mdadm は Linux のソフトウェア RAIDバイスを作成,変更,削除,管理などをするためのコマンドラインツールです. MD は Multiple Devices の略で,複数のデバイスを束ねて 1 つのデバイスとして機能させるという意味があります. 色々な RAID タイプに対応していますが,今回は RAID1 で実験を行います.

RAID1 はミラーリングです.複数のデバイスに同じデータを書き込むことで冗長性を確保します.

修理,交換などのために,デバイスを 1 台取り外し,また取り付ける,ということが発生します. 再度取り付ける際は,取り付けようとしているデバイスに対してデータの再同期(resync)を行う必要があります.

取り付けようとしているデバイスが以前に同じ MD から取り外されたデバイスであれば,原理的には再同期でデータ全体をコピーする必要はありません.差分転送で良いはずです.

mdadm には write-intent bitmap という,再同期を差分転送にするための機能があります. データ領域をチャンクという単位に分けておき,1 ビットで 1 チャンクを管理するビットマップです. 書き込みがあったチャンクに対応するビットに「変更があった」マークを付けておくことで,再同期を高速化します.

実験環境

この記事を書くにあたり,Ubuntu Server 16.04.3 上で実験しました. mdadm のバージョンには依存しますが,カーネルのバージョンは違っていても再現するかと思います.

mdadm の入手

バグ 1 は Ubuntu 16.04 にはあるが Ubuntu 17.10 で修正されたということなので,Ubuntu 16.04 と 17.10 の mdadm バイナリをそれぞれ手に入れます.

Ubuntu のバイナリパッケージは Launchpad に登録されています. Launchpad の mdadm ページ にある各 Ubuntu バージョンへのリンクをたどると次のようなリンクがあります.

mdadm (amd64) (arm64) (armhf) (i386) (ppc64el) (s390x)

私の実験環境は amd64 なのでそれをダウンロードしました.xenial (Ubuntu 16.04) と artful (Ubuntu 17.10) 用のパッケージをダウンロードします. ダウンロードしたら dpkg-deb -x mdadm_4.0-2_amd64.deb contents というように展開しておきます.

この時点でファイル構成はこんな感じになります.

$WORK/archives/
    xenial/
        contents/
        mdadm_3.3-2ubuntu7.6_amd64.deb
    artful/
        contents/
        mdadm_4.0-2_amd64.deb

実験用デバイス群の準備

下図の構成のデバイス群を準備します.片方は不良セクタがある構成の MD,もう片方は対照実験のための不良セクタがない構成の MD です.

f:id:uchan_nos:20171125230407p:plain

このデバイス群を一気に生成するためのスクリプト(procedure.sh)を次に示します.

#!/bin/sh -uex
DISKS=$PWD/disks
ARCHIVES=$PWD/archives

MIB=2048
BMCHUNK=4

LO=0
for md in xenial xenial_nobb
do
    mkdir -p $DISKS/$md
    dd of=$DISKS/$md/sda if=/dev/zero bs=1048576 count=$MIB
    dd of=$DISKS/$md/sdb if=/dev/zero bs=1048576 count=$MIB

    sudo losetup /dev/loop$LO $DISKS/$md/sda
    LO=$(($LO + 1))
    sudo losetup /dev/loop$LO $DISKS/$md/sdb
    LO=$(($LO + 1))
done

SECTORS=$(($MIB * 1024 * 2))
DMNAME=disk_with_badblock
UNDERDEV=/dev/loop0

cat > $DMNAME.def << EOF
 0 5096  linear $UNDERDEV 0
 5096 1  error
 5097 $(($SECTORS - 5097))  linear $UNDERDEV 5097
EOF

cat $DMNAME.def
sudo dmsetup create $DMNAME $DMNAME.def

sudo $ARCHIVES/xenial/contents/sbin/mdadm -C /dev/md/xenial --auto=md --symlink=no -n 2 -l 1 -e 1.2 -b internal --bitmap-chunk=$BMCHUNK /dev/mapper/disk_with_badblock /dev/loop1
sudo $ARCHIVES/xenial/contents/sbin/mdadm -C /dev/md/xenial_nobb --auto=md --symlink=no -n 2 -l 1 -e 1.2 -b internal --bitmap-chunk=$BMCHUNK /dev/loop2 /dev/loop3

メタデータ配置

上で紹介したスクリプトを使って構成した MD デバイスメタデータがどのようになっているか調べてみます. mdadm --examine コマンドを MD の下位デバイス /dev/loop2 に適用してみると次の結果が得られます.

項目名
Raid Level raid1
Raid Devices 2
Data Offset 4096 sectors
Super Offset 8 sectors
Internal Bitmap 8 sectors from superblock
Bad Block Log 512 entries available at offset 72 sectors

この結果から,メタデータが次のように配置されていることが分かります. 左側にあるオフセットの数字はデバイス先頭からのセクタ数です.

f:id:uchan_nos:20171126111715p:plain:w400

mdadm に --bitmap-chunk=4 を渡しているので,1 ビットあたり 4KiB のチャンクに対応する bitmap が生成されています. bitmap 領域の大きさはブログ記事の式で求めることができます.

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

1 セクタが 512 バイトですから,64.25KiB は約 128 セクタです. bad block log がデバイス先頭から 80 セクタの位置に配置されますから,bitmap 領域と bad block log 領域が重なってしまっていることが分かります. この状態で不良セクタが発生すると bad block log にデータが書き込まれ,それが bitmap の変更だと誤検知され,競合状態になることが予想できます.

競合発生の確認

競合が起きていることを確認します. procedure.sh を実行した直後に調べた bad block log の内容は次のようになっていました.

$ sudo mdadm --examine-badblocks /dev/mapper/disk_with_badblock
Bad-blocks on /dev/mapper/disk_with_badblock:
                5096 for 8 sectors

dmsetup コマンドで不良セクタを作った場所が 5096 セクタ目でした.きちんと不良セクタとして検出されているようですね.

bad block log の位置はデバイス先頭から 80 セクタ目,つまり 0xa000 バイト目です. disks/xenial/sda の内容を hexdump し,0xa000 付近をみると次のようになっていました.

000a000 a008 004f 0000 0000 ffff ffff ffff ffff
000a010 ffff ffff ffff ffff ffff ffff ffff ffff
000a020 ffff ffff ffff ffff ffff ffff ffff ffff

数分後,同じコマンドを実行すると表示が変わります.

$ sudo mdadm --examine-badblocks /dev/mapper/disk_with_badblock
Bad-blocks on /dev/mapper/disk_with_badblock:
                   0 for 0 sectors
                   0 for 0 sectors
                   0 for 0 sectors
                   …

なんだかおかしな表示ですね. bad block log が空の場合,普通は Bad-blocks list is empty in /dev/loop1 とかになるはずです. 競合が発生していてデータが不整合になっているのでしょう.

hexdump の結果を見てみます.

000a000 0000 0000 0000 0000 0000 0000 0000 0000
000a010 0000 0000 0000 0000 0000 0000 0000 0000
000a020 0000 0000 0000 0000 0000 0000 0000 0000

bad block log がゼロクリアされています. bad block log を扱う mdadm のコードを読んでみると,空エントリは全ビット 1 になることが仮定されているため,ゼロクリアされた結果として変な表示になるようです.

おわりに

本記事の内容は OSS Gate のワークショップにおける作業が元になっています. https://github.com/oss-gate/workshop/issues/665 を見ると,バグ再現までの手順が記録されています.

バグを再現したり,バグを絞り込んだりするときには,細かく作業ログを取ることが大切だと学びました. ちょっと面倒ですがちゃんとログを取っておくことで,後から見返すのに役立ちますし,無駄な作業をしないので結果的には時間短縮できますね.