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

issue28のパッチ #31

Closed
wants to merge 3 commits into from
Closed

Conversation

yuezato
Copy link
Member

@yuezato yuezato commented Apr 12, 2019

PRの状態

  • アルゴリズムの実装
  • コードレベルコメント(ドキュメントコメントも含め)の追加
  • ここでのアルゴリズムの説明
  • ここでのアルゴリズムの正当性の説明
  • テストの追加
  • ジャーナル領域センシティブな既存テストコードの修正と既存テストコードの修正が必要な理由
  • 議論しておくべき点の洗い出し
  • 実行時間に関する計測結果
  • リソース使用量に関する計測結果

このPRの狙い

Cannylsのissue #28 を解決する。

アルゴリズム

アイディア

リソースの解放要求を、現時点の実装(Storageでdeleteを行ったタイミングでアロケータに解放要求を送る)より遅らせる。

より正確には、Storageでdeleteを行いdata portion pをlump indexから取り外した際に、アロケータにpの解放要求を送らず、それを貯めておき、対応するDeleteレコードがストレージに永続化されたことが確認できたタイミングでアロケータにpの解放要求を送る。同様のことをdelete_rangeに対しても行う。

制約

  • 可能な限りメモリ使用量を抑える
  • ストレージフォーマットは極力変更しない

制約を考えない場合の最も素朴な実装は

  1. storage::deleteまたはstorage::delete_range実行時にジャーナル領域に書き出すDelete及びDeleteRangeレコードに単調増加するidを付与する;
  2. そのidとdata portionをHashMapなどで紐付けてメモリ上に保持する;
  3. 永続化が確定したDelete及びDeleteRangeを見つける度に、そのidから削除対象のdata portionを辿りアロケータに解放要求を送る;

といったものが考えられる。ただしこの方法ではストレージフォーマットを変更しなくてはならず、一時記憶が避けられないdata portionに加えてid達も追加で覚えなくてはならず、最適とは言い難い。

実装

  • Storage層でdelete、delete_range及び上書きputをした際にlump indexから外れたdata portionを記憶する。
    • DelayedReleaseInfo構造体のinsert_data_portionsメソッドを用いる。
  • Delete及びDeleteRangeレコードがストレージ上に永続化されたかどうかを、GCキューにエンキューするタイミングを用いて確認する。
    • 新たに永続化が確定したレコードの個数だけをDelayedReleaseInfo::detect_new_synced_delete_recordsで記録する。
  • insert_data_portionsでバッファしているdata portion達について、detect_new_synced_delete_recordsされる度に先頭からその分だけ解放可能な領域としてアロケータに解放要求を送る。

正当性

このPRの実装では、Deleteレコード及びDeleteRangeレコードに個別にidを割り振っていない。

ジャーナル領域がring bufferすなわちFIFO構造をなすので、先にジャーナル領域に追加されたDeleteレコードまたはDeleteRangeレコードでは、先にデキューされたGCキューに登録されるからである。

これはDelayedReleaseInfo構造体にバッファする場合でも同様であるから、幾つのレコードが新たに安全に解放可能になったかという個数の情報だけが必要になる。

挙動の図式的な説明

最後に簡単な例を用いて実際の挙動を説明する。以下では簡単のために削除レコードだけを記載する。

ジャーナル領域の初期状態 [DEL(0)][DEL_RANGE(10, 20)][DEL(100)] 

起動時に判明する情報として、まだGCキューに入っていない永続化済み削除レコードの数として「3」を記憶する。適当な操作のあと、以下の状態に遷移したとする:

region: [DEL(0)][DEL_RANGE(10, 20)][DEL(100)][DEL(42)][DEL_RANGE(1000, 1010)][DEL(43)]

二つの削除操作がジャーナル領域open後に行われ、次の順番で削除候補data portionをバッファしているとする:

buffer: [<0x1234>][<0x4310, 0x4311, 0x4312, ..., 0x431a>][<0x1235>]

さて、GCキューへの追加によりDEL(42)までがエンキューされたとする:

queue: [DEL(0)][DEL_RANGE(10, 20)][DEL(100)][DEL(42)]
region: [DEL(43)][DEL_RANGE(1000, 1010)]

エンキューの過程で永続化が確定している削除レコード4つに遭遇したことになる。このうち3つのレコードは初期状態から存在していたものであるから、実際には4-3=1個のレコードがlump indexからdata portionを除外した本質的な削除操作ということになる。従って、バッファの先頭に存在する0x1234 が安全に解放できるということになる。

更に処理が進み

queue: [DEL(0)][DEL_RANGE(10, 20)][DEL(100)][DEL(42)][DEL_RANGE(1000, 1010)]
region: [DEL(43)]

の状態では5-3=2個のレコードが永続化された本質的な削除操作であることが分かり、新たに<0x4310, 0x4311, 0x4312, ..., 0x431a>の10個が一挙に安全に解放可能になることを意味する。

最後に

queue: [DEL(0)][DEL_RANGE(10, 20)][DEL(100)][DEL(42)][DEL_RANGE(1000, 1010)][DEL(43)]
region: ∅

となり、6-3=3個のレコードが永続化された本質的な削除操作であることが分かり、0x1235が新たに安全に解放可能になることを意味する。

このPRで発生する問題

  • 使用可能なデータ領域が減ってみえる
    • (現実装と比べると)GCキューにエンキューされるまで解放されず、GCキューは空になるまでエンキューされない実装となっているので、キューの長さによっては長い期間、アロケータから見える利用可能領域が現象する。
  • 使用するジャーナル領域が増える
  • メモリ使用量が増える
    • 解放要求を遅延するため、解放対象となるDataPortionを覚えるためのメモリ空間を最低限必要とする(DataPortionだけでは正しい実装が不可能な場合には、更に追加でメモリ空間が必要となる)。

既存のテストコードの修正について

storage::mod::tests::fullに対する修正の理由

https://github.com/frugalos/cannyls/pull/31/files#diff-a2828e4f71806f3e4623d048057803d4R672

  • このテストは暗黙にアロケータの挙動に依存していた。同じデータサイズで上書きPUTすると即座に再利用されるため、たまたまテストに成功していた。
  • 遅延解放を採用したので、明示的に解放処理を追加する必要が新たに生じた。

storage::mode::tests::confirm_that_the_problem_of_pr23_is_resolvedに対する修正の理由

https://github.com/frugalos/cannyls/pull/31/files#diff-a2828e4f71806f3e4623d048057803d4R1005

  • 遅延解放の実装のために、上書きPUT時にはDELETEレコードをジャーナル領域に明示的に書き出す変更を行った。これに伴い、 ジャーナル領域上の位置を表す各種ポインタの取る値が変動したためである。

議論しておくべき点

実際の解放処理をどこで行うか

現在は run_side_job_once が呼ばれる度に、その段階で安全に解放可能なデータポーションを全てアロケータに通知して解放するようにしている。
https://github.com/frugalos/cannyls/pull/31/files#diff-a2828e4f71806f3e4623d048057803d4R316

  • 解放のスパンが長くなりすぎていないか?
  • 一回に全てを解放して良いか(解放処理はフリーリストのマージなどを行うため、大量に行ってしまうと無視できない時間スレッドをブロックする可能性がある)

リソース使用量に関する議論

GCが行われるまでDelayedReleaseInfoDataPortionU64を格納し続けることになるため、大量のメモリが必要になる。これは以下の簡単な計算で確認できる:

  1. 8TBのHDDをデフォルトの設定で用いるとする。このとき、1%がジャーナル空間に割り当たるため、80GBがジャーナル空間となる。
  2. GCはストレージ層ではジャーナル空間の半分を超えてはじめて開始される。
    if self.gc_queue.is_empty() && self.ring_buffer.capacity() < self.ring_buffer.usage() * 2 {
    // 空き領域が半分を切った場合には、`run_side_job_once()`以外でもGCを開始する
    // ("半分"という閾値に深い意味はない)
    track!(self.fill_gc_queue())?;
    }
  3. 従って、40GB以上ジャーナル空間が使われるまではメモリに解放することのできないDataPortionU64が格納されることになる。例えば PUTの直後にDELETE を繰り返す場合は、40GBのジャーナル空間にPUTとDELETEをそれぞれ3.5億(1件30バイトで計算)エントリ追加できる。
  4. すなわち、メモリには 8バイト*3.5億=2.8GBの空間が必要になる。

以上の計算はデバイス層を無視してストレージ層を直接使った場合の話で、デバイス層では一定時間コマンドが発行されずにアイドリングが続いた時(デフォルトでは100ms)にGCが呼び出される ため、一般的な使用状況では緩和されることになる。
ただし、100ms以内に常にデータが飛んでくる場合には、結局の所ストレージ層単体の計算となり、大量のメモリが必要になることに変わりはない。

なお一回のGCではデフォルトで4096エンキューされる、すなわち最大で4096エントリ解放可能になるだけであり、3.5億エントリある状況では焼け石に水である。

対策

DelayedReleaseInfoにサイズ上限を設けたほうが良いだろう。
このサイズ上限にリーチした場合にはGCを行わなくてはならないとする。
ただし、GCを行ったとしても解放できるとは限らないことから、あまり抜本的な対策にはなっていない。

パフォーマンス測定

実行時間に関する測定

アロケータの挙動が変わってしまうため、フェアな測定が難しい。

メモリ使用量について

valgrind --tool=massifを用いて測定できる。

@yuezato yuezato changed the title [WIP] issue28のパッチ issue28のパッチ Apr 18, 2019
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.

1 participant