Sans-IO: ネットワークサービスに効くRustの秘訣
(firezone.dev)- Firezoneでは Rust を使い、Androidスマートフォン、macOSコンピュータ、Linuxサーバー上でスケーラブルかつ安全なリモートアクセスを構築している
connlibという接続ライブラリを使って、ネットワーク接続と WireGuard トンネルを管理している- 何度もの反復の末に sans-IO という設計にたどり着き、高速で徹底したテスト、深いカスタマイズ性、高い信頼性を実現した
connlib は Rust で書かれており、sans-IO 設計に従っている
- Rust の速度とメモリ安全性のおかげで、ネットワークサービスの構築に適している
tokioランタイム、tungsteniteWebSockets、boringtunWireGuard 実装、rustlsによる API トラフィックの暗号化などを利用- sans-IO 設計では、あちこちでソケットを通じてバイト列を送受信する代わりに、純粋な状態機械としてプロトコルを実装する
Rust の非同期モデルと「関数の色付け」論争
- 非同期関数は、他の非同期関数からしか呼び出せない
- 非同期関数の深い階層にある関数は、それを呼び出すすべての関数も非同期関数にしなければならない
- このため、依存関係が非同期かどうかに無関心なコードを書きたい人にとっては問題になりうる
sans-IO の紹介
- sans-IO の中核となる考え方は、OOP の世界における依存性逆転の原則に似ている
- ポリシー(何をするか)は、実装の詳細(どうやるか)に依存すべきではない
Transmit構造体を使ってデータを送信する代わりに、Transmitを放出する
依存性逆転の適用
Transmit構造体を使ってデータを送信する代わりに、Transmitを放出する- イベントループは副作用を実装し、実際に
UdpSocket::sendを呼び出す
状態機械
- STUN バインディング要求の状態機械ダイアグラムは、
SentとReceivedの2つの状態を持つ StunBinding構造体と関連関数を定義して、状態機械を実装する
イベントループ
- イベントループは状態機械を駆動し、
poll_transmitとhandle_inputを使ってデータを処理する
時間の抽象化
poll_timeoutとhandle_timeoutAPI を使って、時間ベースの要件を処理する
sans-IO の前提
- sans-IO 設計は、依存関係が非同期かどうかの判断をアプリケーション側に委ねる
- sans-IO 設計は組み合わせやすく、柔軟な API を提供し、テストしやすく、Rust の機能ともよく合う
組み合わせやすさ
StunBindingの API は、ほとんどのネットワークプロトコルに適用できる- Firezone の
snownetライブラリは、ICE と WireGuard を組み合わせることで、ネットワーク設定に関係なく動作する「魔法のような」IP トンネルを提供する
柔軟な API
- イベントループを自分で書くことで、コードのチューニングが可能になり、保守もしやすくなる
高速なテスト
- sans-IO のコードには副作用がないため、テストが非常に容易
- Firezone では参照用の状態機械を実装し、
connlibの実際の状態と比較するテストを行っている
エッジケースと IO 失敗
- sans-IO 設計は、プロトコル実装を実際の IO 副作用から分離し、エッジケースやエラー処理を容易にする
Rust + sans-IO: ベストマッチ?
- Rust は所有権と可変性を明示的にモデル化するため、sans-IO 設計と相性がよい
- sans-IO 設計では
&mutを自由に使って状態変更を表現し、asyncRust とは異なり同期 API だけを使う
欠点
- イベントループを自分で書くと、微妙なバグが入り込むことがある
- 順次的なワークフローでは、より多くのコードが必要になることがある
- Rust コミュニティでは、sans-IO 設計はまだ広く使われていない
まとめ
- sans-IO コードは最初こそ見慣れないが、慣れると非常に楽しい
- Rust は状態機械をモデル化するための優れた道具を提供する
- sans-IO 設計は、エラー処理を入力処理の一部として強制するため、ネットワーキングコードを書く正しい方法のように感じられる
GN⁺の意見
- sans-IO 設計は Rust の所有権モデルとよく合い、ネットワークプロトコル実装に非常に適している
- イベントループを自分で書くことで、コードの柔軟性と保守性が高まる
- テストしやすいため、安定したコードを書くうえで大いに役立つ
- ただし Rust コミュニティで広く使われていないため、関連ライブラリが不足している可能性がある
- 新しい技術を導入する際は、学習コストとコミュニティの支援を考慮する必要がある
1件のコメント
Hacker Newsの意見
Rustにasync/await構文が導入される前は、手動で状態機械を実装していた
VT100ライブラリを書きながら、Rustのカプセル化パターンの問題に気づいた
チャネルを使ってデータを送る設計との比較
Haskellエコシステムには、ロジックと実行を分離するという考え方がある
tokio::select!呼び出しをどのようにカプセル化したのかは言及されていないRustのasync関数は状態機械にコンパイルされる
状態を公開すれば、async関数は「純粋」になりうる
Firezoneは驚くべきツールだ
コンパイラがasyncコードをsans ioへ自動変換できればよいのにと思う
記事とコメントを読んで、hexagonalまたはports/adaptersアーキテクチャスタイルを再発明したように思えた
実際のトラフィックがゲートウェイを通過するのか、それとも接続設定にだけ使われるのか気になる