Linux MD(mdadm)で組んだRAID1はデータの異常に耐性がないことが分かった話

これは私の思い違いだったってだけの記事です。

要約

RAID*1の目的は複数のデバイスを使って冗長化する仕組み。データのバックアップや、データ保護といった仕組みはない。」

ということだけ、持ち帰ってもらえればと思います。

  • Linux MD(mdadm)におけるRAID1は1byteの異常では故障扱いにならない
  • 1blockの読み取り不備が起きるぐらいでは故障扱いにならない
    • 残りのデバイスがある場合は正常なデバイスを使って読み直すが、故障扱いにならない
    • 残りのデバイスがない場合はIOエラーが起きるが、故障扱いにならない
  • 具体的に故障扱いになる理由はよくわからなかった
  • データの破損がないか確認する仕組みにも配慮したほうがよさそう(未完)
    • データの完全性を確認するdm-integrityをブロックデバイスレイヤに挟む
    • データの完全性を確認するファイルシステムを使う(Btrfs, ZFSなど)

背景

新しくNASのハードウェアを組み立ててから1年経っているがまだ本番投入していない…

RAID構成やZFSの採用でずっと悩んでいた。手元でベンチマークを雑にとると、ZFSは比較的安定しているが、CPU使用率が高くなったり、Linux MDのほうがCPU使用率が明らかに少ないとかでずっと悩んでいた。

Linux MDでRAID6にすべきかZFSRAID-Z2にすべきか、としか考えてなかったが、次の記事を見て少し考えが変わった。

jrs-s.net

この記事自体は「ZFSではRAID-Zを使うな、mirror vdevをpoolして使え」という記事で、実際それはどうなんだ?と思っていたんですが、考えてみればストレージ効率が50%になるのと、2台故障かつ屑運引いた場合に復旧できないことがある以外にデメリットが全くないとも思えたんですね。

で、なら今まで通りLinux MDでRAID1でいいんじゃないか、ってちょっと思ったわけですが、ふと、RAID1ってどこまで故障を直してくれるのか、って思ったんですよ。

故障って何なんですかね。故障を検知はできるのは過去に経験はしていたんだけども、溜まっているデータを見るとまれに日付が1970年みたいなデータもあって、あれこれこんな日付だったっけ、っておもうこともあったり。

そもそも、RAID1なら1台で誤ってるデータがあったらそれ比べて異常検知できるんじゃないんか?って。

じゃ、やってみましょう、というのが今回の記事。結果は要約の通りです。

手順

  • ループバックデバイスを2つ作る(ただの1GBのファイルをブロックデバイスのように認識させる)
  • ループバックデバイスを2つ使いmdadmを使ってLinux MDのRAID1を組み、ext4でフォーマットし、ファイルを書き込む
  • ループバックデバイスの1つを1byteだけ文字化けさせて動きを見る

という感じです。言うのは簡単ですがやるのはちょっと面倒だった。実際はもっとうまいやり方があると思います。

MD用のデバイス/dev/md7 としています。数字に意味はなくて元の環境で被らない値にします。

あとrootで作業してます。sudo厨は適宜読み替えてください。

ループバックデバイス準備~ファイルシステム作成とマウントまで

# ループバックデバイス用のファイルを用意
dd if=/dev/zero of=./vd1.img bs=1M count=1024
dd if=/dev/zero of=./vd2.img bs=1M count=1024

# ループバックデバイスとして対応させる
losetup /dev/loop0 ./vd1.img
losetup /dev/loop1 ./vd2.img

# MDアレイを構成する(yesを応答する必要あり)
mdadm --create /dev/md7 --level=1 --raid-devices=2 /dev/loop0 /dev/loop1

MDアレイの様子は cat /proc/mdstatmdadm --detail /dev/md7 などから確認できる。

# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1] loop0[0]
      1046528 blocks super 1.2 [2/2] [UU]

# mdadm --detail /dev/md7
/dev/md7:
           Version : 1.2
     Creation Time : Sat Oct  5 22:36:33 2024
        Raid Level : raid1
        Array Size : 1046528 (1022.00 MiB 1071.64 MB)
     Used Dev Size : 1046528 (1022.00 MiB 1071.64 MB)
      Raid Devices : 2
     Total Devices : 2
       Persistence : Superblock is persistent

       Update Time : Sat Oct  5 22:38:31 2024
             State : clean
    Active Devices : 2
   Working Devices : 2
    Failed Devices : 0
     Spare Devices : 0

Consistency Policy : resync

              Name : localhost:7  (local to host localhost)
              UUID : a2bc0b3a:fcebbe92:6143d802:c2ddd7d5
            Events : 17

    Number   Major   Minor   RaidDevice State
       0       7        0        0      active sync   /dev/loop0
       1       7        1        1      active sync   /dev/loop1

適当にFSを構成

mkfs.ext4 /dev/md7

mkdir ./mnt
mount /dev/md7 ./mnt

整合性確認をしておく

echo check > /sys/block/md7/md/sync_action

進捗や状況はcat /proc/mdstatmdadm --detail /dev/md7 で確認できる

# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1] loop0[0]
      1046528 blocks super 1.2 [2/2] [UU]
      [===========>.........]  check = 57.4% (601984/1046528) finish=0.0min speed=300992K/sec
# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1] loop0[0]
      1046528 blocks super 1.2 [2/2] [UU]

mdadm --detailでは、StateやCheck Statusの有無で状況がわかる。

#  mdadm --detail /dev/md7
/dev/md7:
           Version : 1.2
     Creation Time : Sat Oct  5 22:36:33 2024
        Raid Level : raid1
        Array Size : 1046528 (1022.00 MiB 1071.64 MB)
     Used Dev Size : 1046528 (1022.00 MiB 1071.64 MB)
      Raid Devices : 2
     Total Devices : 2
       Persistence : Superblock is persistent

       Update Time : Sat Oct  5 22:42:08 2024
             State : clean, checking
    Active Devices : 2
   Working Devices : 2
    Failed Devices : 0
     Spare Devices : 0

Consistency Policy : resync

      Check Status : 57% complete

              Name : localhost:7  (local to host localhost)
              UUID : a2bc0b3a:fcebbe92:6143d802:c2ddd7d5
            Events : 21

    Number   Major   Minor   RaidDevice State
       0       7        0        0      active sync   /dev/loop0
       1       7        1        1      active sync   /dev/loop1

#  mdadm --detail /dev/md7
/dev/md7:
           Version : 1.2
     Creation Time : Sat Oct  5 22:36:33 2024
        Raid Level : raid1
        Array Size : 1046528 (1022.00 MiB 1071.64 MB)
     Used Dev Size : 1046528 (1022.00 MiB 1071.64 MB)
      Raid Devices : 2
     Total Devices : 2
       Persistence : Superblock is persistent

       Update Time : Sat Oct  5 22:42:10 2024
             State : clean
    Active Devices : 2
   Working Devices : 2
    Failed Devices : 0
     Spare Devices : 0

Consistency Policy : resync

              Name : localhost:7  (local to host localhost)
              UUID : a2bc0b3a:fcebbe92:6143d802:c2ddd7d5
            Events : 23

    Number   Major   Minor   RaidDevice State
       0       7        0        0      active sync   /dev/loop0
       1       7        1        1      active sync   /dev/loop1

作成したファイルシステムでファイルの書き込みを試してみる。

echo "hello world 12345" > ./mnt/hello.txt
cat ./mnt/hello.txt

hello world 12345

当然できる。

壊す場所を探す

壊すための位置を特定する必要があります。grep -abo "検索文字列" ファイル とすると、見つかればオフセット値とともに表示されます。

# grep -abo "hello world 12345" ./vd1.img
136843264:hello world 12345
# grep -abo "hello world 12345" ./vd2.img
136843264:hello world 12345

RAID1だからか全く同じ場所に書かれている感じだった。前後20byteをhexdumpで見てみる。

# オフセット値を設定
OFFSET=136843264
# 検索文字の長さ
PATTERN_LEN=17
# 前後 N byteを指定
N=16

# 表示
dd status=none if=./vd1.img bs=1 skip=$((OFFSET - N)) count=$((PATTERN_LEN + N * 2)) | hexdump -C
dd status=none if=./vd2.img bs=1 skip=$((OFFSET - N)) count=$((PATTERN_LEN + N * 2)) | hexdump -C

結果は、まぁそうですね、という感じ。

# dd status=none if=./vd1.img bs=1 skip=$((OFFSET - N)) count=$((PATTERN_LEN + N * 2)) | hexdump -C
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  68 65 6c 6c 6f 20 77 6f  72 6c 64 20 31 32 33 34  |hello world 1234|
00000020  35 0a 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |5...............|
00000030  00                                                |.|
00000031
# dd status=none if=./vd2.img bs=1 skip=$((OFFSET - N)) count=$((PATTERN_LEN + N * 2)) | hexdump -C
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  68 65 6c 6c 6f 20 77 6f  72 6c 64 20 31 32 33 34  |hello world 1234|
00000020  35 0a 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |5...............|
00000030  00                                                |.|
00000031

一応この時点でチェックを走らせてみる。

# echo check > /sys/block/md7/md/sync_action
# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1] loop0[0]
      1046528 blocks super 1.2 [2/2] [UU]
      [===>.................]  check = 19.2% (201984/1046528) finish=0.0min speed=201984K/sec

unused devices: <none>

# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1] loop0[0]
      1046528 blocks super 1.2 [2/2] [UU]

unused devices: <none>

# cat ./mnt/hello.txt
hello world 12345

問題ないですね。

壊す

では壊してみましょう。vd1.imgのほうのhello worldの"h"を"i"にしてみる。vd2.imgと差が出る状態になります。

printf "i" | dd of=./vd1.img  bs=1 seek=$((OFFSET)) count=1 conv=notrunc

hexdumpで見てみましょう。

# dd status=none if=./vd1.img bs=1 skip=$((OFFSET - N)) count=$((PATTERN_LEN + N * 2)) | hexdump -C
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  69 65 6c 6c 6f 20 77 6f  72 6c 64 20 31 32 33 34  |iello world 1234|
00000020  35 0a 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |5...............|
00000030  00                                                |.|
00000031
# dd status=none if=./vd2.img bs=1 skip=$((OFFSET - N)) count=$((PATTERN_LEN + N * 2)) | hexdump -C
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  68 65 6c 6c 6f 20 77 6f  72 6c 64 20 31 32 33 34  |hello world 1234|
00000020  35 0a 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |5...............|
00000030  00                                                |.|
00000031

書き換わってますね。これを下位の存在であるLinux MDは検知できるのでしょうか。

# cat ./mnt/hello.txt
hello world 12345

壊れてない?いいえ、きっとこれはディスクキャッシュのせいです。ディスクキャッシュを消して再度試してみると…

echo 3 > /proc/sys/vm/drop_caches

再度確認。

# cat ./mnt/hello.txt
iello world 12345

壊したデータが読めてしまった。

今回はたまたま壊したループバックデバイスを見ていたということですね。

でもアクセスしたら「あれ?片方のデバイスと一致してねーや!」って気づかないのだろうか?cat /proc/mdstatで故障扱いになっていないか見てみる。

# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1] loop0[0]
      1046528 blocks super 1.2 [2/2] [UU]

正常扱いでした。ではチェックを流してみましょう。

# echo check > /sys/block/md7/md/sync_action

# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1] loop0[0]
      1046528 blocks super 1.2 [2/2] [UU]
      [===========>.........]  check = 57.4% (601984/1046528) finish=0.0min speed=300992K/sec

# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1] loop0[0]
      1046528 blocks super 1.2 [2/2] [UU]

うーん、問題なし。何をチェックしたのやら。

ということで、この時点で、

  • 読み取りに異常がなければ読める
  • 1byteの不一致は異常ではないらしい
  • ext4ファイルシステムも1byteが狂うぐらいは気にしていない(これはこれで困る)

ここまでやってRAIDはデータを守る仕組みじゃないんだね、ってことを体で学びました(愚者は経験から学ぶ)

読み取りエラーを起こさせる

よくある故障として「データ、読み取れませんでした」という状態を作ってみましょう。

ChatGPTと会話してみると、Device mapperを利用すると簡単に一部を読み取れなくすることができるらしい。やってみましょう。

# dmsetupでいじる場合、デバイスを使っていない状態にする必要がある

# アンマウント
umount ./mnt
# MDアレイの停止
mdadm --stop /dev/md7

# device mapperでloop0のデータがある付近の故障をエミュレート
BLOCK_SIZE=$(blockdev --getsz /dev/loop0)
dmsetup create loop0 << __EOF__
0 $((OFFSET / 512)) linear /dev/loop0 0
$((OFFSET / 512)) 1 error
$((OFFSET / 512 + 1)) $((BLOCK_SIZE - (OFFSET / 512 + 1) )) linear /dev/loop0 $((OFFSET / 512 + 1))
__EOF__
# dmsetup remove loop0 で削除できる

# MDアレイの再開 (dmを経由したloop0のデバイスを使う)
mdadm --assemble /dev/md7 /dev/loop1 /dev/mapper/loop0
# 同期確認
cat /proc/mdstat
# マウント
mount /dev/md7 ./mnt

ファイルが読めるか確認

cat ./mnt/hello.txt

読めちゃった。

# cat ./mnt/hello.txt
hello world 12345

なんでだろうなーと思ったら、気になるログがdmesgに出てた。

[8314675.848584] EXT4-fs (md7): mounted filesystem with ordered data mode. Opts: (null)
[8314685.926359] md/raid1:md7: dm-5: rescheduling sector 263176
[8314685.926531] md/raid1:md7: redirecting sector 263176 to other mirror: loop1

なんてことはなく、いたずらで書き換えたデバイス(loop0)で故障をエミュレートしたので、正常なデバイス(loop1)から読み直した、ってだけらしい。

しかし、cat /proc/mdstat を見ても変化はなかった。これだけでは故障としてマークされないんか…

# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1] dm-5[0]
      1046528 blocks super 1.2 [2/2] [UU]

チェックするしかあるまい。

# echo check > /sys/block/md7/md/sync_action

# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1] dm-5[0]
      1046528 blocks super 1.2 [2/2] [UU]
      [===============>.....]  check = 77.4% (811136/1046528) finish=0.0min speed=202783K/sec

# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1] dm-5[0]
      1046528 blocks super 1.2 [2/2] [UU]

# dmesg
...
[8315865.812910] md: data-check of RAID array md7
[8315871.205671] md: md7: data-check done.

故障扱いにならんのか…まぁ1発アウトはさすがに厳しすぎるからだろうか…閾値でもあるんだろうか。SMART読めないとダメとか?よくわからない

mdadmで故障とマークする

何としてでも壊れたデータを持つデバイス(loop0)を壊れたデバイスと認識させたい。

一旦、正常なデバイスである loop1 をあえて切り離してみる。

# loop0を故障としてマーク
mdadm --manage /dev/md7 --fail /dev/loop1
# mdadm --manage /dev/md7 --fail /dev/loop1
mdadm: set /dev/loop1 faulty in /dev/md7
# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1](F) dm-5[0]
      1046528 blocks super 1.2 [2/1] [U_]

縮退モードになりましたね。読んでみましょう。

# cat ./mnt/hello.txt
hello world 12345

ディスクキャッシュが残ってた。

# echo 3 > /proc/sys/vm/drop_caches
# cat ./mnt/hello.txt
cat: ./mnt/hello.txt: Input/output error

エミュレート通り読みとりできなくなってる!期待通りですね。

でもこれでもloop0(dm-5)は故障扱いにならない。

# cat /proc/mdstat
Personalities : [raid1]
md7 : active raid1 loop1[1](F) dm-5[0]
      1046528 blocks super 1.2 [2/1] [U_]

うーん。どうやったら故障になるんだろう?

ここまできて、データの破損に対してはRAID1は何もしてくれないので、別のアプローチをすべきだと思ったので、ここまでとする。判断が遅い。

後始末

後始末は以下の感じでできるでしょう。

# dmデバイスを削除
dmsetup remove loop0

# アンマウント
umount ./mnt
# MDアレイの停止
mdadm --stop /dev/md7

# ループバックデバイスの削除
losetup -d /dev/loop0
losetup -d /dev/loop1

# イメージファイルを削除
rm -f ./vd1.img
rm -f ./vd2.img

データの破損に備えるには?

チェックサムなどでデータの完全性を検証する仕組みを持つファイルシステム(Btrfs, ZFS)を使うのがよいのではないか、という気持ちになっています。NASならZFSという気さえしている…

また、dm-integrityでデータの整合性をチェックするレイヤを追加することもできるようです。チェックサム計算とストレージ消費量といったオーバヘッドはありますが、ext4やxfsなどのデータの完全性に関して保証がないファイルシステムなどにも導入できるのはよさそうです。(というか、今となってはこれが一般的ではない理由がわからない)

そういえば、RAID5,6のパリティチェックはデータの破損に一役買ってくれるんでしょうか。どうなんでしょうか。

多分、同じ要領でできそうな気がします。気が向いたらやります。

*1:ただしRAID0を除く