RAIDの仕組みと故障時の復元
同じデータを冗長化の方式ごとに配置し、失ったブロックを復元できる条件を調べます。
説明
RAIDは複数の装置を組み合わせ、データの分散や冗長化を行います。冗長性のない分散、同じ内容の複製、XORによるパリティでは、必要な容量と故障時に残る情報が異なります。
4バイトのデータをRAID 0・RAID 1・RAID 5の小さな配置モデルに置きます。一台の故障を与えて読める内容を確認し、二つの欠損と更新途中の不一致から、復元できる条件を調べます。
1分散だけを行う配置
4バイトを順に二台へ分散します。各要素を一つのチャンクとするRAID 0のモデルです。
data = bytes.fromhex("12 34 56 78")元のデータ; striped = [list(data[0::2]), list(data[1::2])]交互の装置へ配置; print([[f"{v:02x}" for v in disk] for disk in striped])装置別の内容装置0には12と56、装置1には34と78です。二台の容量をデータに使えますが、同じブロックの複製や復元用情報はありません。
2RAID 0で一台が失われた場合
装置1を失った状態を作り、元の並びへ戻します。
striped[1] = [None, None]装置1の故障; readback = [striped[i % 2][i // 2] for i in range(4)]元の順序へ戻す; print(readback)欠けた位置を確認元の34と78が欠けます。生き残った12と56だけでは全データを復元できません。配列上で一部の値を取り出せても、実際のRAID 0ボリューム全体が正常利用できるという意味ではありません。
3同じ内容を二台へ複製
二台へ同じ4バイトを置くRAID 1を考えます。一台が失われた後、残る複製を読みます。
mirrors = [bytes(data), bytes(data)]二つの同一内容; mirrors[0] = None一台を失う; print(mirrors[1].hex(" "), mirrors[1] == data)残る内容を照合; print("usable/raw=", len(data), "/", 2 * len(data))使用可能容量と総容量残った装置だけで元の4バイトを読めます。二台に計8バイトを使い、データ容量は4バイトです。このモデルでは両方への更新が完了しており、生き残った複製は正常という条件です。
4パリティを分散した配置
三台のRAID 5で、各ストライプにデータ二つとXORパリティ一つを置きます。パリティ位置は行ごとに変えます。
a, b, c, d = data4バイトを取り出す; rows = [[a, b, a ^ b], [c, c ^ d, d]]行ごとにパリティを配置; print([[f"{v:02x}" for v in row] for row in rows])三台の配置; print([row[0] ^ row[1] ^ row[2] for row in rows])各行のXOR第一行は12・34・26、第二行は56・2e・78です。各行の三つをXORすると0になります。二行でデータ4バイトとパリティ2バイトを使い、装置ごとに列を持ちます。
5失われた列をXORで復元
装置0が失われたとき、各行の残る二つから装置0の内容を求めます。
lost = [row[0] for row in rows]元の列を照合用に保持; rebuilt = [row[1] ^ row[2] for row in rows]残る列から復元; print(bytes(rebuilt).hex(" "), rebuilt == lost)元の内容と比較12と56が復元されます。x XOR y XOR y=xという性質により、一つの未知値は残る二つから決まります。再構築では残る装置の読取りが必要で、その間にも読取りエラーや追加故障が起きると復元条件が崩れます。
6二つの欠損は一つのパリティで決まらない
第一行のデータ二つを失い、パリティ26だけが残った場合の候補を数えます。
parity = rows[0][2]残るパリティ; candidates = [(x, x ^ parity) for x in range(256)]同じXORを持つ候補; print(len(candidates), candidates[:3], (a, b) in candidates)候補数と元データ256組が同じパリティを満たし、元の12と34もその一つにすぎません。RAID 5の一つのパリティでは、同じストライプの二つの未知値を一意に決められません。
7データとパリティが別々に更新された場合
第一行の12を13へ更新する途中で、データだけが保存され、パリティは26のまま残った状態を計算します。
new_a = 0x13; stale_parity = a ^ bデータだけ更新された状態; wrong_b = new_a ^ stale_parity不整合な値から復元; print(f"{wrong_b:02x}", f"{b:02x}", wrong_b == b)誤った復元を確認; correct_parity = new_a ^ b; print(f"{correct_parity:02x}")必要な新パリティ古いパリティから復元すると35になり、正しい34と一致しません。新しいパリティは27です。障害時にデータとパリティの対応が崩れる問題を避けるには、ジャーナル等を含む実装側の整合性対策が必要です。
8冗長化されても削除は複製される
正常に動いているミラー上でファイルの内容が削除された状態を、両方の複製へ反映します。
mirrors = [b"", b""]削除を両方へ反映; print(mirrors, any(value == data for value in mirrors))元データが残るか確認どちらにも元の内容はありません。RAIDは装置故障への対策であり、誤削除前の版を必ず残す仕組みではありません。過去の状態へ戻すには、別に保持した世代やバックアップが必要です。
まとめ
RAID 0は分散、RAID 1は複製、RAID 5は分散パリティで配置します。
一つの欠損をXORで復元するには、残るデータとパリティが正しく対応している必要があります。
冗長化は誤削除前の版や正常な内容を別途保存する代わりにはなりません。
操作履歴と解説を振り返れます。
Python 3
data = bytes.fromhex("12 34 56 78"); striped = [list(data[0::2]), list(data[1::2])]; print([[f"{v:02x}" for v in disk] for disk in striped])[['12', '56'], ['34', '78']]
striped[1] = [None, None]; readback = [striped[i % 2][i // 2] for i in range(4)]; print(readback)[18, None, 86, None]
mirrors = [bytes(data), bytes(data)]; mirrors[0] = None; print(mirrors[1].hex(" "), mirrors[1] == data); print("usable/raw=", len(data), "/", 2 * len(data))12 34 56 78 True usable/raw= 4 / 8
a, b, c, d = data; rows = [[a, b, a ^ b], [c, c ^ d, d]]; print([[f"{v:02x}" for v in row] for row in rows]); print([row[0] ^ row[1] ^ row[2] for row in rows])[['12', '34', '26'], ['56', '2e', '78']] [0, 0]
lost = [row[0] for row in rows]; rebuilt = [row[1] ^ row[2] for row in rows]; print(bytes(rebuilt).hex(" "), rebuilt == lost)12 56 True
parity = rows[0][2]; candidates = [(x, x ^ parity) for x in range(256)]; print(len(candidates), candidates[:3], (a, b) in candidates)256 [(0, 38), (1, 39), (2, 36)] True
new_a = 0x13; stale_parity = a ^ b; wrong_b = new_a ^ stale_parity; print(f"{wrong_b:02x}", f"{b:02x}", wrong_b == b); correct_parity = new_a ^ b; print(f"{correct_parity:02x}")35 34 False 27
mirrors = [b"", b""]; print(mirrors, any(value == data for value in mirrors))[b'', b''] False
演習完了