テストステ論

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

dm-lcの紹介 (7) error handling

私がdm-lcの開発を始めた当時, その開発環境はあまり良いものではありませんでした. dm-lcをテストするためには2つのディスクが必要になります. 私はUSB外付けのSATA2アダプタを2つ購入し, その中にそれぞれ2.5inchのSSDとHDDを入れて, テスト用に使いました.

動かしてみると, 時々エラーが起こっていることが分かりました. そのエラーを突き止めると, SSDに対して激しくI/Oをするとアダプタのコントローラの処理能力を超えて, エラーを返してくる*1ということが分かりました.

また, 現在の実装ではバッファが固定数pre-allocateされて, それが使い回されることにしていますが, 過去のバージョンではこのようなプーリングはせず, バッファを必要な時にアロケートしてましたので, SSDがログ書きを処理する速度よりバッファに書き込む速度が高い場合はいずれメモリが足りなくなり, アロケーションに失敗するというケースがありました.

dm-lcは現在の実装において, このようなエラーに対しては基本的に, 「失敗したらしばらく寝てからリトライして, 成功するまで粘る」という方針を採用しています. 例えば, 以下のようなコードです. 失敗したら1ms寝て, リトライします. I/Oでは, 失敗したら1000ms眠ります. これは処理のオーダーの問題です.

static void *do_kmalloc_retry(size_t size, gfp_t flags, int lineno)
{
        int count = 0;
        void *p;

retry_alloc:
        p = kmalloc(size, flags);
        if(! p){
                alloc_err_count++;
                count++;
                DMERR("L.%d: fail allocation(count:%d)", lineno, count);
                schedule_timeout_interruptible(msecs_to_jiffies(1));
                goto retry_alloc;
        }
        return p;
}
#define kmalloc_retry(size, flags) do_kmalloc_retry((size), (flags), __LINE__)

なぜこのような実装にしたのでしょうか. 以下の4点を理由として挙げます.
1. dm-lcは順序を大事にしています. 例えば, k番目のログ書き込みが失敗したからと言って, k+1番目のログ書き込みに進むことは禁止しています. 同様に, k'番目のsegmentについてmigrationが失敗したからと言って, k'+1番目のsegmentのmigrationに進むことも禁止しています. どちらも, 規則を破ったところで破綻するかは把握していませんが, 少なくとも, 先に進むくらいならリトライした方がいいということは言えます. また, 一貫して順序を守るクリーンな設計にした方が, 実装がシンプルになり, バグを含む可能性が少なくなります. 利得のないことをするのは, まともな人間のすることではないです.
2. エラー処理を関数の内部で閉じ込めてしまった方がコードがクリーンです. エラー処理が関数の外に伝搬していくのは, 本質的でないコードをばら撒く結果になります. dm-lcは多層のキャッシュを制御しますから, 下層でのエラーが上に伝搬していくコードが醜いものになることは明白です. 醜いだけでなく確実にバグるでしょう.
3. エラーが起こる確率は限りなく0に近いため, 一定時間眠ったところで現実的に, トータルとしては何らパフォーマンス損失がありません.
4. 仮に(まずあり得ませんが)何らかの要因によって永遠に眠ってしまったとしても, 最悪ケースとして, 電源を落とすという最終手段が残されています. dm-lcは, いつ何時電源を落としても, データロストになることはありません.

ただし, すべてのエラーに対してこのような姿勢をとっているかというと, そうではなく, 例えばユーザランドの管理スクリプトのエラーなどは, エラーが起きたらreturnするという設計になっていますし, sysfsや, dm-lcの初期化コードでもそうなっています. 基本的な考え方は, 使う側の人間が間違っていて本質的にどうしようもないのであればエラーを返すが, I/O処理やメモリ確保のように, いずれうまく行く望みがあるのであればリトライするというものです. また, 都合の良いことに(というか原理的にそういうものなのかも知れませんが), 前者の部分が, レイヤーとして浅い部分にあるため, この類のエラーに対して, エラーを伝搬させていくのは大きな穢れ*2になりません.

以上です. 今回は, dm-lcを開発する上で問題として上がってきたerror handlingに対してdm-lcがどのような姿勢で臨んだかを書きました. この話は比較的一般的なことですから, みなさんの日々のプログラミングにも通じるところがあったかも知れません.

*1:しかもコントローラ側で結構粘ってから返すので, 迷惑

*2:私は基本的に, どのようなコードも穢れと思っています. コードを書けば書くほど穢れが増え, ソフトウェアは柔軟でなくなります. はぁ・・・糞みたいなコードを書く人が多すぎる