← ドキュメント一覧

ノードネットワーク設計思想

このドキュメントについて

このUSBには、複数のマシンが協調してgitリポジトリを保持する仕組みが含まれています。セットアップ手順は install.shtoki-push.sh のコメントに書いてあります。

このドキュメントはその「なぜ」を記録するものです。10年後にこのUSBを開いた方が、コマンドを写経するのではなく、設計の意図を理解して再構築できるように。


解いた問題

gitリポジトリを複数のマシンで共有したい。ただし:


最終的な構成

物理レイヤー    USB bare repo(オフライン完結)
デジタルレイヤー 各マシンの node.py(Tailscale Funnel で公開)
発見レイヤー    Cloudflare Pages の nodes/index.txt + nodes/{id}.json

pushするとき、toki-push.sh は次の順で試みます:

  1. 現在の remote に push
  2. 失敗したら Pages から nodes/index.txt を取得
  3. 各ノードの nodes/{id}.json を取得して /api/health を probe
  4. 生きているノードに remote を切り替えて push

USBが手元にある場合は、USB内の nodes/ をオフラインフォールバックとして使います。


捨てた選択肢とその理由

Cloudflare D1(データベース)

最初、オンラインノードの一覧をD1に持つ設計を作りました。各ノードが5分ごとに「生きている」ことをD1に登録し、Workerがその一覧を返します。

捨てた理由:D1はWorkerからしか書けません。つまりWorkerへの依存が生まれます。ノード発見のためだけにWorkerとDBを維持するのは過剰でした。

WebSocket

ノードの切断をリアルタイムに検知するため、WebSocketでWorkerに接続し続ける設計を検討しました。

捨てた理由:git syncはリアルタイム性を必要としません。pushが失敗したときに別のノードへ切り替える方式で十分でした。WebSocketは常時起動のマシンか、Cloudflare Durable Objects(有料)を前提にします。どちらも依存が重くなります。

Durable Objects

WebSocketの状態を集約するためにDurable Objectsを検討しました。

捨てた理由:有料($5/月〜)です。また、状態を持つサーバーが必要という問題を解決するなら、手元のミニPCをそのまま使えます。外部サービスを使う前に「手元で同じことができないか」を先に考えます。

常時起動のレジストリマシン

ミニPCをWebSocketレジストリとして動かし、他のノードが接続するP2P構成を検討しました。

捨てた理由:「常時起動のマシンがある」を前提にすると、そのマシンが落ちたとき全体が止まります。前提をなくした設計の方が堅牢でした。


nodes/index.txt の設計変遷

ノード一覧の管理方法について3案を経ました。

第1案(失敗)nodes.json に全ノードをまとめて管理。

問題:複数マシンが同時に install.sh を実行すると、両者が同じファイルを上書きし、片方の登録が消えます。

第2案(不十分)nodes/index.txt(1行1ノードID)に各ノードが行を追記。gitが行単位でマージします。

問題:同じ「末尾」に追記する場合、gitでもconflictが発生します。

第3案(採用)nodes/index.txt はgit管理外にします。nodes/{id}.json(各ノードが自分のファイルだけ書く)をgit管理します。index.txt はデプロイ直前に ls nodes/*.json | sort で生成します。

ls nodes/*.json | xargs -I{} basename {} .json | sort > nodes/index.txt
wrangler pages deploy ...

これで:

「落ちることはない。タイミングのズレは自己修復する。」これで十分でした。


エラーを隠さない

install.sh はwranglerが入っていない環境では、デプロイを自動実行しません。代わりに実行すべきコマンドを表示して終わります。

echo "note: run 'wrangler pages deploy ...' to publish nodes/"

複雑なリトライロジックを入れるより、人間に見せて判断させる方が堅牢なケースがあります。エラーを握りつぶして「成功したように見せる」実装は、後で原因がわからなくなります。

これはゼロ円主義の設計原則「シンプルを保つ」と同じ発想です。


残した依存

サービス用途代替可能か
Cloudflare Pagesnodes/ の配信ほかの静的ホスティングでも可
Tailscaleノード間通信WireGuardを自前で立てることも可

Cloudflare Pagesはgitリポジトリを丸ごとpushすれば動く静的ホスティングです。仮にCloudflareがサービスを終了しても、同じ構成は別の静的ホスティングで再現できます。

Tailscaleはゼロ設定でVPNを張れるサービスです。オープンソース版(Headscale)を使えばTailscale社への依存も外せます。

どちらも「なければ別のもので代替できる」依存です。「これがないと動かない」依存ではありません。


この設計が目指したもの

依存先を減らせるだけ減らすことです。

常時起動のサーバーが必要な設計は、そのサーバーが落ちたとき全体が止まります。DBが必要な設計は、DBのコストと管理が発生します。WebSocketが必要な設計は、接続を維持するインフラが必要になります。

ひとつひとつの依存に「本当に必要か」を問い続けた結果、残ったのはファイルのリストとP2Pの通信だけでした。

物理(USB)とデジタル(Tailscaleノード)の二重化は、どちらか一方が使えないときにもう一方で動くことを保証します。どちらも落ちたとき初めてgit syncが止まります。それで十分です。