chubby

いまさらながら、読んでみてのメモ。

信頼性と可用性が命、ファイルシステムっぽい使いやすいセマンティクスの提供、性能と格納可能容量は二の次。advisoryロックとファイルを全部読み書きするのと、ファイルの変更をお知らせする機能を提供。同期処理に利用してもらうのと、ネットワーク分断時のリーダ選出機能を提供する。

chubbyには、通常5つcell(というサーバプロセス)があって、そのうち3つは少なくとも稼動してなければならない。chubbyには、クライアントとサーバがある、もう一つプロキシというのもある。cellの中から、投票によって過半数を得たらサーバが、リーダとなる。一度選出されると、master leaseと呼ばれる期間は、リーダで在り続ける。master leaseは、一定期間で更新されるので、存在していれば同じサーバがリーダになる。masterだけが、databaseへのread/writeの要求を受け付ける。他のレプリカをもつサーバは、データベースのコピーを持っている。

リーダに書き込み届くと、協調プロトコルに従って、すべてのサーバに送られる。過半数のサーバに書き込み要求が届くと、リーダからクライアントに成功が返る。

ユーザからは、ファイルシステムの様に見えるが、機能として抜いているものもある(ディレクトリへのアクセス権限、最終アクセスタイムなど)。ファイルのことをnodeと呼ぶ。nodeには、永続的と一時的の二つがある。一時的なnodeでは、nodeをオープンしているクライアントがいなくなる、かつ、ファイルが空だと削除される。

各nodeは、インクリメンタルな4つの64-bit正整数をもっている、目的は変化に気づくため。

  • インスタンス数:同じ名前なら、以前のどのnodeよりも大きい。
  • コンテンツ生成数:中身が変わるとインクリメント。
  • ロック生成数: ロックを取得するとインクリメント。
  • ACL生成数; ACLが付加されると、インクリメント。

チェックサムも持っている、他のレプリカが持っているそれと差分が分かる。あと、handleっていう、ファイルディスクリプタみたいなものがある。

reader-writerロックを提供して、adovisoryロックであるので、ロックは獲得しなくてもファイルにはアクセスできる。分散環境でロックを提供するのは難しいが、virtual time や virtual synchrony に解決作がある。これは、メッセージの到着順序の逆転を抑えるという話。

シーケンス番号を全てのメッセージのやりとりで導入すると大変なので、ロックに関わる部分にだけ導入する。シーケンサは、バイトストリングで、ロック名、ロックモード(share/exclusive)、ロック生成番号が記述される。クライアントは、ロックがかかっていると思うなら、シーケンサをアプリサーバに送る。アプリサーバは、シーケンサの妥当性をチェックして適当に処理する。アプリサーバは、Chubbyのキャッシュか、chubbyに聞きに行き、最新のシーケンサをゲットして、シーケンサの妥当性をチェックする。

ロックの遅延や到着順序の逆転は痛恨なので、Chubbyでは、以下のような対策をうつ。クライアントが、ロックの解放を通常の手続きで行えば、その結果は他のクライアントに瞬時につたわる。ただし、セッションが切れた等のばあいは、lock delayと呼ばれる期間だけ他のクライアントの要求をリジェクトする。

ファイルに対して、イベント通知のお願いができる。

  • ファイルの内容変更
  • ロック要求
  • ロックの衝突
  • ノードの追加、削除、変更
  • リーダのフェールオーバ
  • セッションハンドルの無効通知

APIは、まあいいや、色いろあるということで。compare-and-swap的なものがあるのと、atomic性が保証された更新がある。

クライアントライブラリには、ライトスルーキャッシュがある。リースのメカニズムに基づいて、リーダは、更新があると妥当でないことを通知、この通知が完了するまでリーダは更新を止める。このやりとりは、KeepAliveプロトコル上に乗っけている。クライアントから、ackがないと、リーダはuncachableという状態にする。こうすることで、read要求を止めなくて済む。止めてしまうと、uncachableではなくなったときに、要求がバーストする。他にもやり用はあるが、採用しない。openやロックに関しても、キャッシュを利用出来るところはりようして、リーダには聞きに行かない。

Chubby サーバとChubbyクライアントをつなげる関係のことをセッションと呼ぶ。有効期限があって、KeepAliveプロトコルに従って、一定期間ごとに延長する。セッションが保持されている限り、クライアントのもつ状態は、サーバと同期していることを保証する。セッショには、リースがあって、ある未来時刻までは、サーバが一方的にセッションを切らない。リーダがリース延長を行うのは、以下の場合。

  • セッション作成時。
  • リーダのフェールオーバ発生時
  • クライアントから、KeepAlive RPCの呼び出しがあったとき

KeepAlive RPCの応答はすぐに返却せず、リース切れギリまでまって、かつ、リース延長をして(約12sec)返却する。RPCの応答が来たら、即効でクライアントは、RPCを発行する。キャッシュの無効化にあわせて、RPCを即効で返却してもよい。ということで、無効化のAckをKeepAlive RPCに乗っけて処理する。こんな風にすることで、クライアントはシンプルに、かつ、一方向のfirewallをかけることができる。

クライアントは、ローカルリース切れという概念を持っている。もし、これが切れたら、サーバがセッションを維持しているかどうかが怪しいため、キャッシュが利用できないようにし、jeopardyという状態に遷移する。クライアントは、grace periodと呼ばれる期間待つ(デフォルト45sec)。セッションが生きていると確認出来れば、そのまま処理を続行する。タイムアウトしたら、再接続しない限り、APIはエラーを返却する。アプリケーションには、jeopardy->safe、jeopardy->invalidate のいずれかのイベントが返却される。

リーダに障害が発生すると、リーダが管理している情報は消える。次に選ばれたリーダは、リース情報を持っていないので、実質的にリース期間が延期されることになる。クライアントのローカルリース切れを生じる前に、新たなリーダは、クライアントとのセッション処理を始めないといけない。が、リーダ選出が遅いと、クライアントは、jeopardyの状態に落ちいる。grace periodは、フェールオーバのことも考えて設定される必要がある。図による説明は省略。ひとつだけちょっとよくわからないのは、(4)の時に新リーダから何が返却されるのか。多分、新リーダの経過時間(これは、何をするものなのだろう?)。

サーバのデータベースには、セッション情報、ロック情報、一時的ファイルの情報がある。セッションに含まれる、ハンドルの情報はロック情報が無い限り持たない、セッションリース切れの情報もない。さっきの疑問の答えは、予想どおりだった。最初に断るのは、以前のマスタとのやりとりを一度、リセットするため。ただし、リーダが誰か聞いてきたら、答えてあげる。リーダは、メモリ上に構築するべきものを構築し、KeepAlive RPCを受け入れ、フェールオーバイベント送り、ackを待って、APIを開放し、事後処理。

サーバのデータベースは、BDB。レプリケーションロジックが、Chubbyの思想にマッチ。が、ちょっとBDBの開発者にとって、レプリケーション機能が新しく、コミニティでの重要度を考えると、やめておいたほうが良いという結論になる。バックアップは、GFS上にとっている。

スケールさせるには、cellを増やす、セッションリース間隔を大きくする、ユーザのキャッシュ、プロキシによる要求の集約、パーティション分割などがある。Javaクライアントは、一筋縄ではいかない(JNI重い、pure javaのキャッシュなしだと、サーバが重たい、ので、イベント通知なしのプロキシ)。

色々、飛ばして…

リーダのフェールオーバが生じた後、新リーダに対してクライアントからバースト的に接続が来たとき、BDBベースであったときにはセッション情報のデータベースへの書き込み要求が捌けなかった。そこで、セッション接続時ではなくて、最初のnode更新の時にデータベースに書きこむようにした。でも、こうすると、読み込みするだけのクライアントの接続情報はなくなるので、次のフェールオーバ時に困る。ので、現状のような接続情報を再作成する設計とした。まあ、クライアントの接続がきれていると、リースタイムアウトまで待たされることになるが、よしとする。

chubbyを各サービスごとに立ち上げるのは、合理的ではない。ので、セルをアプリケーションで共有することになるが、この時あるユーザの行いが他のユーザに迷惑をかけないようにすることが必要。まあ、色々あるけど、事前に利用の仕方についてヒアリングし、アドバイスする。十分というわけではないけど(あるプロダクトの担当が変わったら、プロダクトも変わるので)。あと、そもそも大きなデータを入れることを目的にしていないし、クオータもない。ある時、1.5Mのファイルを書きこまれたり、読み込まれたりして帯域がうまったことがあるので、ファイルサイズの制限を256KBに変えた。