ひらめいた:P2P分散ファイルシステム

ちょっとひらめいた。


基本的なアイディアは、プライマリノードとセカンダリノードがまったく同じデータを持っていて、プライマリが落ちたときにセカンダリがプライマリに昇格するというごく普通なもの。ただし、プライマリが落ちたとき、プライマリでもセカンダリでもないノード全員が、昇格したプライマリに、「セカンダリいますか?」と聞きに行く。昇格したプライマリは、一番最初に受け取った「セカンダリいますか?」を送ってきたノードに、「セカンダリやれ」と送る。2つ目以降の「セカンダリいますか?」には「もういるよ」と返す。
これで次々にフェイルオーバーができる。それにノードの参加と離脱がないときには、プライマリが単なるファイルサーバーになるだけだから、ロックは特に問題なく処理できる。


あとはFUSEをかぶせれば分散ファイルシステムになる。


これだけでも良いのだけど、これだけだとプライマリノードとセカンダリノードにデータが集中してしまう。負荷が集中することはさほど問題ではないとしても、データが集中するのは、書き込みを全部RAMに行うVIVER的に良くない。それからフェイルオーバーするときに、次期セカンダリにデータをコピーするトラフィックが大きくなってしまう。


そこで、プライマリ/セカンダリをグループ分けする。各グループにそれぞれプライマリとセカンダリがいる。これでデータが分散できる。



というわけで、まず第一段階の実装。

  • 用語:
グループ
1つのグループには、1台のプライマリノードと、1台のセカンダリノードと、多数の参加ノードがいる。(ネットワーク全体の中にグループが2つあれば、プライマリノードは2台存在する)
ノード
複数のグループに参加することができる。
プライマリノード
グループのデータを管理しているノード。セカンダリノードとデータを同期している(セカンダリノードがいるとき)。書き込み要求(Write)と読み込み要求(Read)を受け付け、応答する。Writeの場合はセカンダリノードにもデータを送る(SyncPush)。
セカンダリノード
グループのプライマリノードとまったく同じデータを持っているノード。プライマリノードがダウンしたときに、プライマリノードに昇格する。
参加ノード
次期セカンダリノードの候補。
  • プロトコル:
Discovery
UDP。基本的にはブロードキャストする。ノードがネットワークに参加するときに、自分が参加するグループのプライマリノードのIPアドレスを得るために使う。送信するデータは、自分のIPアドレスと、参加したいグループ一覧。
DiscoveryACK
UDP。Discoveryを受け取ったときに、受信したグループ一覧の中に自分がプライマリノードをしているグループが入っていたら、DiscoveryACKをDiscoveryを送信してきたノードに送り返す。送信するデータは、自分のIPアドレスと、自分がプライマリノードをしているグループ名。プライマリノードをしているグループが複数あれば、DiscoveryACKを複数送り返す。
SyncPull
TCP。Discoveryでプライマリノードを見つけたら、そのプライマリノードに送る。SyncPullを受け取ったプライマリノードは、既にセカンダリノードがいるならそのセカンダリノードのIPアドレスを、いないなら自分が持っているデータを全部送る。送信するデータはグループ名。受信するデータはセカンダリノードがいるか否かと、いるならそのセカンダリノードのIPアドレス、いないならプライマリノードが持っているデータ。
Read
TCP。プライマリノードにのみ送信できる。送信するデータは、グループ名、要求するファイルのパス、ファイルのシーク位置、長さ。受信するデータは要求したデータ。
Write
TCP。プライマリノードにのみ送信できる。送信するデータは、グループ名、ファイルのパス、シーク位置、長さ、データ。受信するデータは成功またはエラー。
SyncPush
TCP。プライマリノードがWriteを受け取ったとき、プライマリノードがセカンダリノードに送信する。送信するデータは、グループ名、ファイルのパス、シーク位置、長さ、データ。できれば2相コミットでトランザクション管理する。

V-FIELDと連携して使うので、JoinやNotify Upなどは無くて良い。ノードの参加や離脱もV-FIELDから教えてもらえる(教えてもらえるようにV-FIELDを改良する)。




以上が第1段階。第2段階は、動的にグループに参加したり離脱したりできるようにして、グループ分けを自動で行う。第3段階では、FUSEをかぶせてファイルシステムにする。第1段階でファイルシステムにするには、トップレベルのディレクトリを書き込み不可にして、トップレベルにグループ名と同じディレクトリを置く。



RUNESでは、適用するプラグイン名と同じ名前のグループに属することにする。(プラグイン名がUDPのパケット1つに収まらないとDiscoveryで問題が発生しそう…)
これでロック付きのクラス変数がサポートできる。FUSEをかぶせれば、DHCPサーバーのリースファイル(dhcpd.leases)をこのP2P分散ファイルシステムに直接置いたりと、データの引き継ぎが簡単に(と言うか何も考えないでも)できるようになる。データベースのデータ本体をこのP2P分散ファイルシステムに置けば、レプリケーションが簡単にできる。Webサーバーのデータをこのファイルシステムに置けば、CGIを含んだ動的なWebサービスでも簡単にデータが引き継げる。


このP2P分散ファイルシステムは、フェイルオーバーに必要な機能の内で、データの引き継ぎしかやらないけど、これにkeepalivedを組み合わせれば、IPアドレスの引き継ぎとサーバーの起動もできる。keepalivedが提供する機能の内、ホストがダウンしたことの通知はV-FIELDを使ってできるので、RUNESがノードの参加と離脱をプラグインに通知するようにすれば、プラグイン側で必要のはvrrpdだけで良くなる。さらにRUNESメッセージバスに対応したvrrpdプラグインを作ってしまえば、フェイルオーバーに対応したプラグインは簡単に作れるようになると。


VIVER+V-FIELDと連携するというのが結構ポイント。V-FIELDはVIVERでディスクイメージを共有するのに使うので、ディスクIOが発生するごとにノード一覧の更新が行われる。なのでノード一覧をメンテナンスするために余分なトラフィックが発生しない。

うーむ。V-FIELDは使える。



なんか、このP2P分散ファイルシステム単体で十分使えるんじゃないか。VIVER要らない?(笑




V-FIELDが要らないんじゃないかともふと思うかもしれないけど、V-FIELDはブロックデバイスレベルなので、レイヤーが違って住み分けになる。ファイルシステムレベルだとデータの単位がファイルになるので、分散の粒度が大きくなってしまう。このP2P分散ファイルシステムは、巨大ファイルがどーんとあるような場合は、まったく分散されない。(メタデータとデータを別にすればこの分散も可能。ただ実装が複雑になる。メタデータとデータを別にしているのがLustreだったりGfarmFSだったりするけど、これらはデータの分散とカスケード・フェイルオーバー、ノードの動的な追加の機能が無い)
それからV-FIELDは最初から書き込みが無いこと前提にしているので、キャッシュの更新を無視できて、Read要求を多数のノードに並列して行える。よって速い。(V-FIELDに書き込みサポートを追加しようとすると、キャッシュの更新を同期的に行わないといけなくて、スケーラビリティが非常に低くなってしまう。Readは高速だけどWriteは遅い)


最高なのは、データをV-FIELD的に分散して、メタデータをこのP2P分散ファイルシステム的に管理する方法か。Readは高速でWriteは普通。ただ多重化度に応じてWrite時のトラフィックが増えるのはしょうがないか。プライマリから1台目のセカンダリにデータを送るときは同期的にやって、2台目以降は非同期にすれば、連続書き換え時のトラフィックは減らせる&高速化できる。


V-FIELDの分散アルゴリズムもいろいろと思うところがあったりして。今はとにかく多重化度を平均化するだけ。これはまた書く。

P2P分散ファイルシステム

↑のアイディアを閃く前の話↓

昨日のエントリに書いたRUNESの話で、メッセージの送受と高信頼性マルチキャストは、間違いなく実現可能。メッセージの送受は単にRUNESをデーモンとして動かすだけで良くて、後はプラグインがメッセージを受け取るインターフェースと、メッセージを送るAPIがあればOK。高信頼性マルチキャストは、V-FIELDと連携すれば何の問題も無くできる。


実現方法が分からないのが、クラス変数とノード間のロック。変数は全ノードに全データを持たせれば良いとしても、どうやってそれをノード間で同期するか。同期すると言うことは要するにロックに帰着するわけですが、サーバーに依存しないロック方法、またはサーバーがカスケード・フェイルオーバーできるロック方法が必要。さらに用途から考えて、ノードが動的に出て行ったり入ってきたりする。

このような構成をサポートした分散ファイルシステムがあれば、それをバックエンドに使えば良いかなーと思っていたのですが、どうにもそういうモノが見つからない…。

NFS+LifeKeeperはプロプライエタリなのでダメ。GFSとOCFSはデータの保持をやってくれない&ノードの動的な追加ができない(実はできる?)。V-FIELDはデータの保持しかしない&書き込みができない。その他は論文レベルで実装がまだ無い(そのあたりはこの資料(Chordプロトコルを活用したシステム開発の実際)が詳しい)、というわけで、見つからないのです。


サーバーに依存しない分散ロック自体は、OpenDLMという実装があるのですが、どうもこれはノードの動的な追加ができない気がする。(動的な離脱はできる)GFS(RedHat)もOCFS2も実は中でOpenDLMを使っている。




と、ここまでいろいろ調べた結果、どうも分散ファイルシステムをバックエンドに使う方法は現時点ではできない気がするので、別の方法を考える。

まずサーバーをカスケード・フェイルオーバーするのと、サーバーに依存しないでP2Pで行くのと、どっちが楽なのか。


少し考えるとカスケード・フェイルオーバーの方が楽っぽいのだけど、データの保持をどうするかが問題。待機サーバーとアクティブなサーバーとでまったく同じデータを持っていれば良くて、たとえばブロックデバイスレベルでやるなら、DRBDが使えそう…あれ…DRBDはカスケード・フェイルオーバーできないっぽいな…。
DRBDを使わずに、DeviceMapperとNBDを使う?うーん…それはどうなんだ…


ファイルシステムレベルでやるなら…ふとi-notify+rsync+NFS+VRRPという汚い方法が思いつく…これは無いな。完全に同期しないからいろいろ問題が出そう。


…DeviceMapper+NBD+VRRP+NFSと言う方法が最終手段かもしれない。DRBDをあえて使わない点がポイント。待機サーバーとアクティブサーバーの間でのデータの同期は、NBDをDeviceMapperでミラーリングして行う。(Multipathなぞ織り交ぜると面白そうだと思ったけど面白いだけで無意味)フロンとエンドはNFSで、VRRP冗長化する。
バックエンドにDeviceMapper+NBDでフロントエンドにNFSというのは何とも不格好だけど、これをSQLサーバー+レプリケーション(pgpoolとかMySQL Cluster!とか。カスケード・フェイルオーバーができるか怪しい)にするとすごく格好いいな。まぁ実際にはファイルシステムの方が都合が良さそう。(psqlfsとかPgfsなんてPostgreSQLファイルシステムにするなんてモノがあるけど…)



CodaFSはどうやらサーバーの複製を自前でやってくれるらしい。前にCodaは使おうとしたことがあるけど、サッパリ分からなくてあきらめた記憶がある。
設定自体は http://japan.internet.com/linuxtutorial/20030620/1.html あたりを読めば分かりそうだけど、複雑なのはイヤだなぁ。ユーザーランドで走るプログラムが多いのもイヤだ。それだけディストリビューションに依存しやすくなる。(実際NFSもあまり使いたくなくて、9P2000あたりを使いたい)