Skip to content

Instantly share code, notes, and snippets.

@cupnes
Last active February 16, 2023 06:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cupnes/3ef5423b21f87a8d9aff6d798fc5767e to your computer and use it in GitHub Desktop.
Save cupnes/3ef5423b21f87a8d9aff6d798fc5767e to your computer and use it in GitHub Desktop.
Ceph Code Walkthroughs - OSD Read Lease

Ceph Code Walkthroughs - OSD Read Lease 視聴メモ

https://tracker.ceph.com/projects/ceph/wiki/Code_Walkthroughs

2021-11-30 @ 10am PT: OSD Read Lease - Sage Weil - https://www.youtube.com/watch?v=9hxPwQvWN4s

  • 動画の残り15分程(残り37%程)の所から始まるPRを例に挙げた解説は未確認

Cephコード内ドキュメントstale_read.rstをベースに解説

冒頭

  • 技術的には「プライマリから読み取る」というだけ
  • プライマリが依然としてプライマリのままかOSDMapを見ているため通常は問題無い
    • プライマリでない者が読み取り要求を受けても、自身がプライマリでない事を認識しているため要求は無視される
  • ただし、ネットワークパーティションに陥っている等でプライマリの移動が起き、現在は他の誰かがプライマリだが、各OSDは「他の事をしていた」等でそれを認識しておらず、クライアント側は依然として移動する前のプライマリから読み取ってしまう可能性がある
    • このようなケースをトリガーするのはかなり難しい
    • ネットワークパーティションで分断された残りの部分とOSDを並べ替えて(?)、クライアントが上手いこと古い方から読み取る、といった事をテストとして発生させる必要がある
    • 基本的にブラックリストに登録することによって、それを行うユニットテストができる(?)
    • (引き起こし方について説明していたがよく分からず)
    • 最終的には修正された
  • 読み取りに関してリース間隔を設定するプロパティがある(read_lease_interval)

readable_until

  • readable until(読み取り可能である時間)は、以下の2つの値を追跡することで実現される
    • readable_until
      • リースが切れるまでに、どれくらいの時間リクエストを処理(読み取り)することができるかを示す値
    • readable_until_ub
      • acting set内の任意のOSDに対するreadable_untilの上限値
      • これは他の全てのレプリカで値が読み取れるまでの上限であるため、将来にどのくらいの期間かかる可能性があるかを示している
      • エポックでPGのプライマリが決定されるわけだが、readable_until_ubは、そのプライマリが読める上限
  • これらの値の更新はpg_lease_tメッセージを通じて行われる
    • 例えばリースを延長する際、プライマリが各レプリカにこのメッセージを送信することで全員のreadable_untilreadable_until_ubを更新する
    • 流れとしては以下の通り
      1. プライマリ自身のreadable_until_ubを更新
      2. 更新したreadable_until_ubを各レプリカへメッセージで共有する
      3. 全てのレプリカで新しいreadable_until_ubを確認できたら、プライマリ自身のreadable_untilを更新する
      4. 更新したreadable_untilを各レプリカへメッセージで共有する
    • (「シーケンスを正確に思い出せないが」との前置きで)、プライマリとレプリカの通信の流れは、基本的にレプリカにメッセージを送信すると、レプリカはプライマリにack応答し、プライマリはレプリカに別のメッセージを送信する
    • つまり、基本的に、全ての動作しているOSDが新しいreadable_until_ubを見たことを確認すると、プライマリは自身のreadable_untilを増加させる、という流れ
    • その結果、動作中のOSDのreadable_untilは常に動作中のOSDのreadable_until_ub以下であるという不変性が得られる
  • 質疑
    • Q.
      • acting setに登場するもののいずれかがプライマリになる可能性がどうのこうの・・
      • 回答から逆に推測すると、どうやらレプリカ側を読んでしまう事を危惧している模様
    • A.
      • 全てのレプリカはすべて、acting setのすべてが権利を得るまで読み取り可能(readable_until)以上のupper bandを持ち、レプリカも読み取り可能になるまで維持するため、通常、レプリカから読み取ることはない
      • readable_until <= readable_until_ub」が維持されている事により、レプリカから読み取る事は起きない、とのこと
      • (質問者が具体的にどういうケースを想定しているのか分からなかったので、この不変性が維持されている事でどのように「レプリカ側を読んでしまう」事を防げるのか不明)
      • レプリカが自身をプライマリと誤認しない、という事だと思われる
  • 各OSDでのクロックのずれ(クロックスキュー)を回避するため、NTP等ではなく、単調クロックによるモノトニッククロックを使用している
    • 理由としては、「NTPが何か迷惑な事をしている可能性がある」とのこと
    • (当然だが)この単調クロックは戻ることは無い
    • OSDの時計(時間設定)を変更しても、この単調クロックは変更されない
      • 単調なローカル時間のようなものだが、各OSDに独立して存在する

コードリーディング - OSDのハートビート処理について

Prior Intervals

  • (前置き)
    • 「Prior Interval」というのは、そういう名前のパラメータがある、というよりは、そういう「期間」の話をしている模様
    • そして、ここで言う「Prior」は、OSD同士のピアリングでreadable_untilが確定する前、という意味である模様
    • (元のドキュメントにも"The prior interval is ..."というような直接的な説明が無い・・)
  • 全てのOSDはreadable_untilの値の上限(readable_until_ub)を保持する必要がある
  • OSDマップの変更等でまだpingを行っていないOSDについては、そのピアリング中にreadable_until_ubpg_history_tの一部として共有される
  • pg_history_tこの辺りに定義されている
  • pg_history_tこの辺りの変数を見ればわかる通り、「何かを行ったエポック秒」といった歴史的な情報を持っている
  • また、prior_readable_until_ubというフィールドがある
    • これは、読み取り可能になるまでのインターバルの上限
    • 一般的には、インターバルのリースが切れるまで待ってから書き込み処理を開始することになる
    • また、書き込みが完了した後に、古いOSDの1つが読み出し処理を行い、読み書きの順序を乱すことがないようにしている、とのこと
  • そして、その下の方にはいくつかのヘルパー関数がある
    • (前置き)
      • 「Prior Readable」(先行読み出し)という考え方がある様で、ここではその読み出し期間についての関数が定義されている模様
    • refresh_prior_readable_until_ub()
      • ローカルOSDの現在の時間とその上限を持つメッセージ(おそらく最初に送信する正式な(?)メッセージの事?)を送信する前に呼び出される
      • そして、新しいdurationを計算するか、あるいは、既に上限を過ぎている場合は、古いリースが切れるまで読めるので(?)、ゼロを返す
        • それにより、前のインターバルが読めなくなったことがわかる(?)
      • (Prior Interval中であっても読み出し可能な期間というものをリフレッシュする関数である模様)
    • get_prior_readable_until_ub()
  • この様に、Prior Intervalsは、実時間の代わりに、このduration時間で表現される
  • (発表者曰く、)それらはほとんど迷惑なタイムスタンプ(?)で、2・3箇所くらいしか出てこないのでほとんど無視して構わない

PG "laggy"(遅延) state

  • pgはアクティブなのに、何らかの理由で最低限のリースが十分に速く更新されないと、ラグが発生する。その状態の話
  • この状態に陥る流れとしては以下の通り
    1. PGが読み出しと書き込みをサービスしている間に何らかの理由で読み出しが入ってくる
    2. それによって読み出し可能な期間の値が過去のものになる
      • 「書き込みを行っている間に読み出しが入ってきた」という話なので、書き込みを行う前に読み出した値については「過去のもの」ということを言っている?
    3. それによってその読み出しをサービスすることができなくなった場合、PGは遅延(laggy)状態になる
      • 一度「過去のもの」を読んでしまった事に気づいた時点で「ちょっと待とう」という事になる、ということかな
  • この状態になると、リースが延長されるか、何か他のことが起こって遅延状態が解除されるまで、すべての読み出しがキューに入れられる
  • この時、読み出しをサービスしないが、メッセージがネットワークを通過することでリースが成立するのであれば、その理由を検討し、サービスを提供することにしている
  • リース間隔が短すぎたり、遅いネットワークや高負荷のOSDを使用していない限り、ほとんどの場合、この現象は起こらないはず
  • 他にも、クラスタが高ストレスである、あるいはパーティション化されたOSDがプライマリとの通信を停止していることに気づかず、リースが削除されなかったり、プライマリがレプリカから応答リースを取得できなかったりしない限り、ほとんどの場合、この現象は起こらない
    • なお、その場合、パーティションが切れているので、いずれはラグが発生し、読み出しのサービスができなくなる

質疑

  • Q. この遅延したOSDにキューイングされていたリクエストはどうなるのか?どのように並べ替えられるのか?
  • A.
    • キューイングされたリクエストがどうなるのか?について
      • ラグ状態が解消されるか、OSDやPGリピーター(?)がすべてのキューを完全にドロップするまで、それらは永遠にそこでブロックされることになってしまう
      • そのため、一般的にインターバルが変更されると、そのPGのすべてのリクエストがドロップされ、OSDまたはクライアントがそれらを再送信する
      • したがって、このような、例えば実際にパーティション分割が起きてしまったような場合、最終的にOSDがそのOSDマップの変更を発見するまで、基本的にキューに入り、それらはドロップされることになる(?)
      • あるいは、一時的/短期的なパーティションの場合、例えばネットワークケーブルが5秒間ダウンし、その後復旧するというようなことはある
      • その場合、ネットワークが再接続され、すべてのメッセージが通過し、リースが更新され、そしてすべての読み取り要求が再キューされる
    • リクエストがどのように並べ替えられるのか?について
      • PGのコードのどこにブロックされたリクエストを再キューイングする関数があるのか、正確には忘れた
      • リクエストの並べ替えには非常に繊細な順序がある
      • リクエストのブロックには、リカバリ待ち、リリース待ち、出現待ち(?)、などなど、10個程の異なる理由がある
      • 加えて、リクエストをリリースする優先順位というのも、最も低いものから、最も高いものまである
      • そのような訳で、リクエストの順序付けは複雑
      • どこに書かれていたか忘れたが、それを行うその同じ関数の中で、すべて同じように処理される

PG "wait" state

  • Prior Intervalの上限がまだ未来にあるような(Prior Intervalがまだまだ続くような)「待機状態(wait state)」の場合、少し異なる
  • そのような場合は、ピアリングが完了するのを待つしかない
    • だから「wait state」という事か
  • そして、これの待機時間をユーザーに気づかれないように隠す基本的なトリックがある(?)

Dead OSDs

  • (OSDダウンに至るケースと、OSDダウンの扱いの話)
  • ピアリング中などで良く問題になるのがOSDダウン
  • OSDダウンの際の扱いとしては以下の通り
    1. まず、OSDが本当にダウンしたと判断する前に、10秒間、その状態のままpingを打ち続ける
    2. そうすると、リースは最短時間(8秒)でタイムアウトする
      • そしてOSDダウンという判定になる、という事を言っている、という理解
    3. OSDダウンという判定になると、リース間隔が、ハートビートタイムアウトと同様に常に短くなる、とのこと
  • なお、OSDはタイムアウトして失敗することもあれば、プロセスが終了したり停止したりすることもよくあること
  • その場合、8秒といったタイムアウト時間の間、ハートビートを待つ必要はない
  • そのため、OSDマップには新しいフィールドが追加されている

コードリーディング - ダウンと判定されるまでの流れ

  • https://github.com/ceph/ceph/blob/89bad98ff59da867142408ba268c9b8bafd1a911/src/osd/OSDMap.h#L96
    • このdead_epochフィールドはOctopusで追加されたもの
    • 基本的に、これは完全に死んだと確認された最後のエポック
    • そこに至るには2つの方法がある
    • 一つは、OSDがモニターにMarkMeDeadメッセージを送ることができ、明示的に「私がリクエストに応答しなくなった」と言うように、MarkMeDeadに遷移することができること
    • もうひとつの方法は、相手にメッセージを送っているときに切断され、再接続しようとする場合
      • その際、ECONNREFUSEDを取得する
  • https://github.com/ceph/ceph/blob/89bad98ff59da867142408ba268c9b8bafd1a911/src/osd/OSD.cc#L6486
    • これは、handle_refusedというメッセンジャーからのコールバックで、ECONNREFUSEDを取得した場合に発動される
    • 基本的にconnection refusedに陥った場合、それはネットワークが切断されたのではなく、プロセスが停止しているということが推測できるらしい
      • そうなると、タイムアウトになるなどして、反応しなくなる
  • https://github.com/ceph/ceph/blob/89bad98ff59da867142408ba268c9b8bafd1a911/src/osd/OSD.cc#L6511
    • その場合、モニターに送信する失敗メッセージには、「このOSDを直ちに停止して失敗とマークする」というフラグ(MOSDFailure::FLAG_IMMEDIATE)が付く
    • そして、これがdead_epochを設定するトリガーとなる
    • そうすると、OSDは本当にダウンしたと判断できる

ざっくばらんなコードリーディング

様々な変数定義の紹介

Prior周りの処理

build_prior関数

on_new_interval関数

check_prior_readable_down_osds関数

laggy周りの処理

recheck_readable関数

ピアリング処理に関するその他諸々

activate関数

update_history関数

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment