テストステ論

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

(writeboost report) 不揮発ログの検討

背景

writeboostの唯一の弱点は, ライトバリアへのペナルティである. writeboostのライト先であるSSDでもペナルティはあるが, writeboostは, バッファ大をライト単位としているため, ペナルティがより大きい.

この対策として, 揮発バッファにあるデータを不揮発ログにも保存しておき, 電断時にバッファ消滅しても復帰出来るようにする. 不揮発ログとして想定するものは, 不揮発バッファを持ったSSDか, 本物の不揮発RAMである. 前者はすぐに手に入るし, 後者はブロックI/Fと新しいI/Fの両方で提供されるはずである.

当初の予定では, 現在の実装のみでアップストリーム化してしまい, この件についてはのちのち追加するというつもりであったが, よくよく考えてみると, 実際にやって実証しなければあとになって困るように思うので, 先にやってしまうことにする.

検討

もともと, 揮発バッファをそのまま不揮発にすれば終わりかと思っていたが, これは間違っている.

揮発バッファは先頭4KBに16N(255)のメタデータが入っており, それに対応したデータ4KBNがあとに続く. 通常のライトはこのバッファに揮発のまま格納され, 後にSSDに書き出される. この時点でバッファは破棄出来る.

もしこのバッファが不揮発になった場合, メタデータとデータの格納がatomicである必要になる. 片方だけ更新された場合, データの一部だけ更新された場合, いずれにしろ障害となる. 症状としては「ACKを返してきていないのにリードするデータが変わってしまった」があり得る. 一言でいうと, バッファが不揮発になったことが却ってあだとなる.

いずれにしろ, SSDへのライトとバッファへのリードヒットでは, カーネルが管理するページ(今の実装でいうrampool)が必要である(dm_ioの要請)ため, 現在の実装は完全に残し, その上で不揮発ログを別に追加することとする. このある/なし, 不揮発ログのmediumについては, 初期化引数typeで切り替えることが出来る. 既存の実装を残したまま, 新しく追加するということなので, 実装上の穢れも少ない.

追加する実装は

  1. バッファへのライト後に, ログの追加を行う (append_persistent_log)
  2. ログ領域は, SSDへのライトが終わるまで破棄出来ない. 新しいものを獲得するタイミングは, 揮発バッファと同様とする (acquire_new_persistent_log)
  3. ログリプレイは, (1) 不揮発ログからSSDに書き出す (2) SSD上でリプレイする という2つの処理とする. (1)を実装する必要がある (flush_persistent_log)
  4. とか

複数アドレスに対するライトをアトミックに出来る特殊なハードウェアは存在するが, writeboostはこういうものには依存しない. 代わりに, checksumを使う(揮発バッファの書き出しもchecksumを用いてデータの完全性をチェックしているので今更文句を言うことは出来ない).

ログは, 上記Nに対して, 9sector * Nの領域を確保する. データ書き出しは, 1sectorのメタデータ(checksum含む)と8sector(4KB)のデータを含む. これを単に追記ライトしていく. 典型的な4KBライトについては, N個分を完全に格納出来るサイズということでLinuxカーネルにおいて嫌われるヒューリスティックに依存しない. メタデータは例えば以下のような情報を含む.

  1. 本来のLBA (64bit)
  2. チェックサム (32bit)
  3. 長さ (8bit)
  4. バッファ上でのindex (8bit)

データは4KBライトばかりではない. 1sectorライトもある. この時は1sector(メタデータ) + 1sector(データ)という組み合わせにする. この場合, ライトバッファが512KBフルに使われることはないが, 仮にすべてのライトが1sector単位であったとしても, 同4KB単位だった時に比べて損失は, (1 + 1) * 8 / (1+8)であるため二倍に満たない. 従って同アドレスへのin-placeライトがないと仮定すると, 常に256KB以上のバッファ大は保証出来ることになり, これは悪い話ではない(実際は1sector単位のライトの割合はもっと小さいため, 現実的には500KB近いライトサイズになるはずである. もちろん, バースト的に4KBライトが降ってくる場合も数多くあるため, この場合は常に512KBサイズのライトを発行出来る).

しかし問題は, 以下のようなケースが起こった場合にこのアルゴリズムは破綻することである.

[正常に書かれたメタデータA][Aのデータ4KB]¥([不正なメタデータ][不正かも知れないデータ]¥)+

上記アルゴリズムでは, 「次のログを探すためにはメタデータが正しく書かれている」ことが条件となっている(ログの長さを追っていくから).
厳密にいうと, メタデータ部に何らかのバイト列を埋め込むことによってそれがメタデータであると判断したのち, checksumを使ってデータの完全性を問えば, 悪いログかどうかの判定が出来るため, 「もし不正なログがあっても1sectorごとにリニアサーチしていけば次のログを発見出来」そうではあるが, 非常にめんどくさいので出来ればやりたくない. そもそも, 本当に正しいのか少しきな臭い印象がある.

というわけで, ログ追記は常に一直線に行われることとして, 間に不正なデータがあり得ないようにするというのが実装としてはあり得る. ただし, 正しく動かす実装自体はそれほど厄介ではないものの, 性能的なオーバーヘッドを伴う可能性があるため, いずれにしろ微妙なところである. writeboostは超性能ソフトウェアなので, 正しくてかつ性能的に最善の実装をしなければならない.

今回は検討に留める. まだ読み切れないことが多く, たぶん実際にやってみて誤りに気づくことになると思う. 開発では実際のところ, いくら机上検討したところで結局はその検討は誤っていることに気づく. その時に, 修正が可能であるか, そもそも検討誤りが起きにくい実装方式を選択するということも, 特にカーネルでは求められるため, ずっと頭で考え続ける必要があり, 鬱になるのである.


この機能は, ファイルシステムの実装をシンプルにする. 現在私が持つ装置群を使って性能を測定するだけであれば, 不揮発ログとして単なるSSDを使って十分であると思う. もっと良いbackingになって, 高い性能を目指そうとすると, 問題になってくる可能性が微レ存だが. あともちろん, 揮発バッファしか持たないSSDをこの用途で使うことは, 寿命の観点から良くないので運用としては全くオススメ出来ない. たぶんSSDが即死して泣く. もっとも, 1MB程度の不揮発ログが死んだところで失われるデータはたかが知れてるわけですが...

キャッシュとしてのSSDが死んでも, 不揮発ログが死んでも, ただ昔のデータに戻るだけというのがwriteboostの良いところですね.