テストステ論

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

(akashic report) メタデータキャッシングの構想

metadata caching · Issue #45 · akiradeveloper/akashic-storage · GitHub

アカシックストレージに保存されるデータは不変なので, 安全にキャッシュすることが出来る.

しかし設計上の問題から, あるキーに関するnullバージョン(Versioningなし)では, オーバーライト時にバージョンオブジェクトごと置換する仕組みになっている. 過去には, オーバーライトを避けるためにVersioningのあるなしに関わらず常に追記という設計をとっていたが, 他の部分でも設計に一貫性をとるためには無駄に複雑になる意味があるのと, 将来行うVersioning(Enabled, Suspended)の実装上nullバージョンは固定である方が都合が良いと判断して今の設計になった. (追記するとクリーナーが必要になり, これがうざい)

しかしそれが結果として, メタデータキャッシングを難しくしている. 例えば, nullバージョンを書いた時にメタデータをキャッシュして, 次の瞬間にそのバージョンをオーバーライトするとどうなるか?メタデータの管理とファイルの置換はアトミックではないので, 不整合を生じる瞬間がある. この問題を解決するには, ファイルを置換する前に(ユーザリクエストが新しいnullバージョンに接触する前に), 古いメタデータをinvalidateする必要がある. これは概念的には容易だが, 実装上は問題を起こす. もっとも美しいのは, ファイルシステムへのリクエストをフックしてinvalidateを行うことだが, ほぼ不可能である. 従って, 現存のコードにメタデータキャッシュへの依存が入ってしまうわけだが, これが綺麗になるかは不明だ.

概念的には,

  • 何らかの型Aをラップする
  • AからはReadしてTを作ることも, TからAにWriteすることも出来る. 後者によって, Write時にキャッシュすることが出来る.
  • サーバー全体で, T型に対するキャッシュがあり, ここに何かしらのKey -> Tを保存する.
  • Keyは, メモリに存在してはならず, アカシックストレージのロジックやファイルシステムの情報から計算出来る必要がある.

使い方は,

今,

a: A = ...
t.writeTo(a)
t: T = T.readFrom(a)

となっているコードを

server.mdcache.forT = Cache[T](opts)
server.mdcache.forU = Cache[T](...)

val cachedA: Cached[A, T] = makeCache(server.mdcache.forT, key, writer: (T, A) => (), reader: A => T)
cachedA.put(T) // キャッシュしつつファイルに書く. key -> Tを登録
val t: T = cachedA.get // キャッシュにないならステージングして返す

とする感じにするのではシンプルなのではなかろうか.

例えば, S3のオブジェクトは複数のバージョンが存在しうるので, keyはバージョンも反映したものである必要がある. それはおそらく画一的に決められるものではなく, ぶっちゃけ信頼出来る文字列がいい. また, MultipartUploadのように, 処理の開始と終了が断絶しているパスからオブジェクトが作られることもあるのでこれもカバー出来るものである必要がある. 例えば, 自分を包含するディレクトリのcreation timeはアカシックストレージのロジック的に信用出来そうだが, それをいちいち問い合わせるのでは, ファイルシステムがネットワーク越しの分散ファイルシステムである場合にキャッシュの効能が小さくなってしまう. やはり現実的には, Cachedにinvalidateをつけて, そのオブジェクトを破棄するときに呼ぶようにするというのが一番自然か.

アカシックストレージに配置されるすべてのオブジェクトは, 生成と破棄をAstralという場所で必ず行い, そしてmoveする. 従って, 生成と破棄をこの場所で捉えることは可能である. 例えば, ここを通過出来るオブジェクトのすべてをCachableとして, それにデフォルトのinvalidateをつけるというのでもよい

さらに考えているのは, オブジェクトデータのローカルキャッシュである. ただ実際には, 分散ファイルシステムnfsで接続などするときに, fscacheなどを使う方がアーキテクチャとしては筋がいい.

(追記)

実は上書き時に同Keyのキャッシュがあったら上書きでもいいかも知れないね. どのみち, ファイルが全部揃うまではユーザはアクセス出来ないという設計なので, ファイルが全部揃うまでにメタデータを全部揃えることも可能.

(追記)

ノードAで作って, キャッシュして, 次にノードBでそのファイルを更新したらキャッシュがstaleになる. 一番簡単なのは, last modified timeを毎回チェックすることだけど, これは秒精度なので明らかに不足. (まぁぶっちゃけ, 書いた一秒後にまた書くって, かなり病的なユースケースだとは思うけど)