Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue27のパッチ #29

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Conversation

yuezato
Copy link
Member

@yuezato yuezato commented Apr 10, 2019

このPRの狙い

異常終了するケースで、EndOfRecords(以下 EoR )が存在しないジャーナル領域が生じ、以降そのlusfファイルが読み込めなくなるというissue #27 を解決する。

EoR が消失する問題は、メモリ上のジャーナルバッファを永続化する処理が完了しない(=途中で強制終了する)場合に起こりうる、ジャーナル領域が破壊される分かりやすい一例である。
こうしたジャーナル領域の破損の例としては、EoR の消失以外にも、特定のジャーナルレコードが中途半端に書き出されてしまい、再起動できなくなるといったこともある。

本PRは、ジャーナルバッファの書き出し処理を「安全に」行うことで、強制終了した場合にも正常なジャーナル領域に回復する、より一般的な解決を目指す。
より正確には、ジャーナルバッファの書き出し処理がatomicになるようにし、書き出しが完了してジャーナル領域が完全に更新されるか、または書き出しの途中で失敗した場合は書き出しを行う直前まで戻す。

本PRに関する前提知識

Issue27とは何か

Issue27は、大まかには次の二つの状況で EoR が消失することを表している:

状況1(ジャーナルバッファをメモリからdiskにflushする際の話)

...[recordA][EoR] => ジャーナルバッファの内容をflushする
...[recordA].......... => ジャーナルバッファの内容を全てflushしきる前にcrashしてEoRが消え去る

この場合には、ジャーナル領域に EoR が存在せず、ジャーナル領域がどこまでか認識できないため起動に失敗する。

状況2(ジャーナル領域の末尾に到達したので先頭までシークする際の話)

[recordA]...[recordX][EoR] => GoToFrontを書き込み、先頭にシークした後にrecordYを追加したい
[recordA]...[recordX][GoToFront] => GoToFrontは書き込んだが、recordYの書き込み前にcrash

この場合にも EoR が存在しないために、ジャーナル領域全体を認識できない。

このPRでの上にあげた状況への対応

状況1について

メモリバッファの書き出し時に、EoRを上書きすることになるメモリバッファ先頭512バイトの書き出しを後回しにする。すなわち

  1. 先頭512バイトより後ろを先に書き出す。
  2. syncによる永続化を行う。(この瞬間、Diskには古いEoRと新しいEoRの2つが存在する)
  3. 最後に先頭512バイトを書き出す。(3の書き出しが、実際にDiskにreachすれば、古いEoRは消える)

さて、以下2つの点について説明したい:
a. EoRは2つ存在しても良いのか?
b. 先頭512バイトを特別扱いするのはなぜか?

a. については、EoRは2つ存在しても問題ない。Cannylsは再起動時に、Diskに永続化されているジャーナル先頭領域から順にエントリを取り出し、最初にEoRに到達したところで処理をやめる。従って、EoRが2つあったとしても正常に起動することはできる。
メモリバッファの書き出しに完全に成功した場合は、すなわち3. の書き出しまで完了したならば、ジャーナル領域は正常に更新されることになる。
一方で3.の書き出し完了まで到達しなかった場合は、古いEoRまでをもってジャーナル領域と認識するが、これはメモリバッファの書き出しに成功しなかったことを考えると自然な挙動であるといえる。

先頭512バイトを特別扱いする理由は、現実の多くのHDDが512バイト=1セクタに対する書き込みに関してatomicになっているからである。実際には古いEoRを、新しいメモリバッファの先頭でatomicに上書きさえできれば良い。EoRの消去(上書き)がatomicに行われずに強制終了した場合は、ジャーナル領域として壊れてしまう。この最後の書き込みがatomicに行われるために、メモリバッファ全体の書き出しがatomicになるとも言える。

コードではどうなっているか

flush_write_bufメソッドが、上記のアルゴリズムに沿うように変更された
https://github.com/frugalos/cannyls/pull/29/files#diff-5664d011af898065aad8d33346649d30R115

Cannylsを新しい挙動で使いたい場合には、以下のようにする

    let filenvm = FileNvm::create(filepath, capacity).unwrap();
    let mut builder = StorageBuilder::new();
    builder.journal_safe_flush(true); // <-
    builder.create(filenvm).unwrap()

状況2について

状況1と同じように、新しい方のEoRを書き出してから、既存のEoRGoToFrontで上書きする。

  1. GoToFrontを書く前に、一度先頭にシークする
  2. レコードとEoRを書く
  3. 永続化する
  4. GoToFrontを書くべき位置までシークによって戻り、書き出す。

コードでの対応

上に述べた挙動でジャーナルレコードを追加するsafe_enqueueメソッドを導入した:
https://github.com/frugalos/cannyls/pull/29/files#diff-42653effaf88e3f0b6c1217764dc36aeR182

Cannylsを新しい挙動で使いたい場合には、以下のようにする

    let filenvm = FileNvm::create(filepath, capacity).unwrap();
    let mut builder = StorageBuilder::new();
    builder.journal_safe_enqueue(true); // <-
    builder.create(filenvm).unwrap()

このPRの有効性の確認

issue27の検証用リポジトリにこのPRを取り込んだブランチ https://github.com/yuezato/cannyls_eor_vanish/tree/use_PR_for_issue27 を用いると、これまで発生していたStorageCorruptedエラーが生じなくなる。

パフォーマンス低下について

状況1について

状況1では、これまでジャーナルバッファのflush時にはsyncを行っていなかったのに対して、この修正では1発のsyncが必要になるため、バッファflush時には50ms前後(一般的なHDDに対するfsyncはこの程度の時間を要する)のレイテンシ悪化が発生する。

ジャーナルバッファのflushをユーザ層で意図的に大量に発生させるためには、埋め込みPUTしたバッファ上にあるデータに対するGETを行う場合に限られる。
Frugalosではfrugalos_raftの票管理やlog_prefix, log_suffixで埋め込みPUTを行うため、パフォーマンスが劣化する可能性がある。

状況2について

状況2でも新たにsyncを行うため同様にレイテンシが悪化するが、ring状のジャーナル領域の末尾を指すtail pointerが一周する時にのみ発生するため、ほとんどの場合は問題がない。

@sile
Copy link
Member

sile commented Apr 11, 2019

この対応良いですね(※ 方針ドキュメントを読んだだけで、まだコードはみていないです)。
パフォーマンス低下に関しては、最低限、オプションでこのモードのON/OFFが切り替え可能になっていれば大丈夫なような気がしました。
(もちろん、実用途に適用する前にはベンチマークを取るのは必須だとは思いますが)

@yuezato yuezato marked this pull request as ready for review August 1, 2019 05:24
/// これにより、
/// ジャーナルバッファを書き出している途中でプロセスがクラッシュしても
/// 再起動後には書き出し直前の状態に戻ることができる。
pub fn journal_safe_flush(&mut self, safe_flush: bool) -> &mut Self {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

descriptionの状況1に対応するためのフラグ

/// GoToFrontを書き込んでからジャーナル領域先頭にseekするという
/// 比較的長い処理をしている間にプロセスがクラッシュしても、
/// 再起動後には直前の状態の戻ることができる。
pub fn journal_safe_enqueue(&mut self, safe_enqueue: bool) -> &mut Self {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

descriptionの状況2に対応するためのフラグ

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants