読者です 読者をやめる 読者になる 読者になる

案2: P2P分散ファイルシステム

VIVER

まずアクセス権限と安全性について。
普通に考えると、分散ファイルシステムで読み取り禁止を実現するには、データを暗号化しないといけなくなる。POSIXなアクセス制限はファイル(とディレクトリ)ごとに設定するするから、ファイルごとに鍵を変えないといけなくて、そうすると鍵の数が大変なことになる。
それから、鍵を管理するノードは必ずデータを読み取れることになるけど、POSIXでは誰にも読み取れないアクセス権限を設定することも可能だから、誰も鍵を持てなくなってしまう。
あと、ユーザーごとの権限とノード(正確にはマシンの上で動くP2P分散ファイルシステムの実装)ごとの権限の区別が分からない。ノードごとのアクセス制限なら、ファイアウォールを使えば良い。
でもPOSIXなアクセス制限はユーザーごとに設定するから、たとえばユーザーAは読み取れて、ユーザーBには読み取れないようにしたいファイルFがあったとする。そしてあるノードKが、Fを復号化する鍵を管理している(とりあえず読み込みだけで書き込みは考えないので、Fに暗号化する鍵(=書き換える鍵)が別(公開鍵を使うとか)でも話は同じ)。で、あるノードNが、ユーザーAの権限でファイルを読み取ろうとする。ユーザーAはファイルを読み取れないといけないので、ノードKはノードNに鍵を渡さないといけない。渡す方法がないといけない…この時点で何かおかしいなぁ。ノードNは鍵を手に入れたので、ユーザーBに鍵を渡せてしまう。



…と、ごちゃごちゃになって思考停止してしまうわけですが、要するにノード(ファイルシステムの実装)を信用しないアクセス制限は非常に難しいと思うわけです。その方向性でファイルシステムを作ろうとすると、それだけのためにがんばって思考しないといけないので、パスです。POSIX互換のアクセス権限を無視する方法もあるかもしれないわけですが、何というか、無いと思います。
で、今回の場合、RUNESで使うことを考えると、ノードを信頼するアクセス制限でも良いと思います。なぜならVIVERで使うからで、VIVERはDHCPに頼ってネットワークブートする&V-FIELDには認証が無いので、この時点でIPネットワーク以下のレイヤーでセキュリティが確保されていることを前提にしている。
間違えて書き換えてしまった!と言うようなことについては、NFSのように、各ノード間の紳士協定によるアクセス権限をサポートすれば良いと思う。これは、それができるようにプログラムを作れば良いだけなので、できるはず。もちろんセキュリティは無いので、見られてはマズイデータを置く場合は、IPネットワークより下のレイヤーでセキュリティを確保しておかないといけない。











で、次に思いついた実装の第二案について。

データとメタデータの分離と、n台でのデータのフェイルオーバー。(メタデータは、とりあえずは2台でフェイルオーバー。n台でも良い)



データを書き込めるノードは1台でないといけない。ネットワーク中で書き込み権限を持っているのは1台でないといけない。というわけで、プライマリノードはデータの書き込み権の管理さえすれば良い。データは別のノードが持つ。

  • グループ分けを除いたプロトコル
GetWriteAccess
TCP。プライマリノードに送信できる。送信するデータは、ファイル名。受信するデータは、そのファイルを構成するデータ断片を持っているノードのリスト。GetWriteAccessが成功した後は、StripWriteAccessを受け取るまで書き込み権を保持し続けられる。プライマリノードは、GetWriteAccessを受け取ったとき、まずその時点で書き込み権限を持っているノードにStripWriteAccessを送って、書き込み権限を取り戻す。続いてどのノードが書き込み権限を持っていったかを記録し、その記録をセカンダリノードにもコピーする(SyncPush)。セカンダリノードに記録をコピーし終わったら、このGetWriteAccessに応答する。
StripWriteAccess
TCP。プライマリノードが、GetWriteAccessで書き込み権を持っていったノードに送信する。送信するデータはファイル名。受信するデータは無し。StripWriteAccessを受け取ったときにデータの書き込み中だったら、書き込み終わった後で(WriteCommitを送った後で)このStripWriteAccessに応答する。
GetDataNodes
TCP。GetWriteAccessと対応させるならGetReadAccess。ただ読み取り権限は同時に複数のノードが持っていても良い。プライマリノードに送信できる。送信するデータは、ファイル名。受信するデータは、そのファイルを構成するデータ断片を持っているノードのリスト。
Write
TCP。データ断片を持っているノードに送信できる。送信するデータは、ファイル名、ファイルのシーク位置、長さ、データ。受信するデータは、エラーか成功か。ここで、Writeを受け取ったノードは、この時点ではデータを書き込まないで、書き込もうとしているファイルにWritePendingフラグをセットする。
WriteCommit
TCP。データ断片を持っているノードに送信できる。Writeを送ったノード群すべてから成功が帰ってきたら、そのノード群すべてに送る。送信するデータはファイル名。受信するデータは無し。WriteCommitを受信したノードは、実際にデータの書き込み反映させ、WritePendingフラグを解除する。(要するにトランザクション管理したい)
Read
TCP。データ断片を持っているノードに送信できる。送信するデータは、ファイル名、ファイルのシーク位置、長さ。受信するデータは、要求したデータ。Readを受け取ったノードは、要求されたファイル名にWritePendingフラグがセットされている場合は、フラグが解除されるまで(WriteCommitを受け取るまで)ブロックする(ブロック時間が長引いたらkeepaliveが必要かも)
Discovery
UDP。プライマリノードを見つけるために使う。
DiscoveryACK
UDP。自分がプライマリノードで、Discoveryを受け取ったら、Discoveryを送信してきたノードに送る。
SyncPull
TCP。Discoveryでプライマリノードを見つけたら、そのプライマリノードに送る。「セカンダリなら足りている」とそっけなく返答されるか、またはそのプライマリノードが記録している書き込み権限情報のコピーをもらう。
SyncPush
TCP。GetWriteAccessを受け取ったプライマリノードが、セカンダリノードに送る。
JoinDataFragment
TCP。自分が「あそこのデータ保持したい!」と思ったら、そのデータの書き込み権を管理しているプライマリノードに送る。このプロトコルでは、まずGetWriteAccessと同じ手順を踏んで書き込み権を得る。そしてReadを使って既に他のノードからデータ断片をもらってくる(このときこのデータ断片にWritePendingフラグがセットされていることはあり得ないので、デッドロックはしない)。Readが成功したら、プライマリに「このデータ保持をしているのでよろしく」と送る。
PendingReset
TCP。書き込み権限を持っているノードがダウンしたことをプライマリノードが検知したとき、データ断片を持っているノードに送る。送信するデータは、ファイル名。データ断片を持っているノードは、指定されたファイルにWritePendingフラグがセットされていたら、書き込もうとしていたデータを捨てて、WritePendingフラグを解除し、プライマリノードに「解除できたよ」と応答する。プライマリノードは書き込み権限を自分にセットする。

この方法のポイントは、同じノードが連続して書き込むときは、前処理なしでWriteを直に送って書き込みができて、遅延が小さいこと。
それからReadは複数のノードに並列して要求できるので速い。Readの時は必ずしもGetDataNodesをプライマリに送る必要が無くて、前回のGetDataNodesからノードの増減がなければ、直にReadを送っても良い。
さらにプライマリとセカンダリが同期するのは、どのノードが書き込み権限を持っているかという情報だけなので、データ本体に比べると圧倒的にサイズが小さい。フェイルオーバーにかかる時間が短くなるので、情報が消失する可能性が低くなる。あるいは、セカンダリを複数台にしてもトラフィックが少なく済む。


一方で問題は、自分が書き込み権限を持っていないときに、書き込み権限を得るまで必要なパケット数が多くて遅延が大きくなること。
それから、1つのファイルに付き、1.どのノードが書き込み権限を持っているか 2.どのノードがこのファイルを構成するデータ断片を持っているか という情報を付加しないといけないので、ファイル数が多くなるとプライマリノードのRAMの使用量がどんどん増える。グループ分けでプライマリの仕事を分散すれば乗り切れるかも。でもどうグループ分けするのか?という問題がある。うまく分散できたとしても、ノード数に対してファイル数が凄まじく多いと困る。IPv6に対応したくないと思うとき。


あと、ノードがネットワークに参加するとき、DiscoveryとJoinDataFragmentを送ることになるけど、Discoveryは良いとして、JoinDataFragmentを送るとき、どこのデータを保持するか決めるアルゴリズムが必要。これは現時点のV-FIELDと同じように、データの多重化度をひたすら平均化するように決定する方法が1つ。この方法が最も信頼性が高い。パフォーマンスを重視すると、自分がアクセスしそうなところを優先して持つ方法がある。これを拡張して、自分がアクセスすることが多いことが分かったらどんどん持ち替える方法もある。要するにV-FIELDと同じチューニングができる。

ちょっとマズイ問題は、データを多重化しているノード間の整合性を保つことは、書き込み権限を持っているノードに一任されている点と、Writeを受け取るノードは、Writeを送ってきたノードが書き込み権限を持っているかどうか、確かめない点(後者は、それならプライマリノードに確かめに行けば良いじゃないかという話ではある)。で、ここに空気の読めないユーザーが変なWriteを送ったりすると、簡単にデータの整合性が壊れる。要改善か。



たぶん、このプロトコルで、ファイル数を1つに制限して、書き込み権限の管理を無くすとV-FIELDになる。