Category

Rustで作るL4 TCPプロキシ

Cloudflare pingoraのようなゲートウェイミドルウェアを参考に、Rust+tokioでL4 TCPプロキシをゼロから実装します。ロードバランシングやヘルスチェックなど、L4の範囲に絞った設計・実装を扱います。

14件の記事

HAProxy/nginxとのベンチマークで見えたSNI peekの実コストと、負荷テストで初めて表面化したJoinSetのメモリリーク

これまで作ってきたL4 TCPプロキシを、HAProxy・nginxと同条件のバックエンドに対してwrkでベンチマークします。CPU効率では本プロキシが最も高効率だった一方、素のスループットでは差が出ました。補足として、その差の大半を占めるSNI peekタイムアウトの設定コストと、負荷テスト中に見つけて修正したJoinSetのメモリリーク、学習用実装として端折っている箇所を扱います。

続きを読む

実障害と見分けがつかない障害注入の設計と、ラウンドロビンが健全なバックエンドだけを対象にすることで起きる注入の偏り

バックエンドを実際に落とさなくても、リトライ・サーキットブレーカー・パッシブヘルスチェックが正しく機能するかを検証できるよう、確率ベースの遅延・切断注入を実装します。注入した障害は実際の接続失敗と全く同じ経路を通るため、システム側からは区別がつきません。あわせて、ラウンドロビンの選択アルゴリズムが「健全なバックエンドだけ」を対象にしていることで、注入した障害が特定の1台に偏って集中する現象も扱います。

続きを読む

AtomicとRAIIガードで集計するPrometheusメトリクスと、closeで受信バッファが残っているとRSTが飛ぶ理由

L4プロキシとしてはこれまでHTTPに一切触れてきませんでしたが、メトリクスをPrometheus形式で公開するには最小限のHTTPレスポンダが必要になります。既存モジュールに`Atomic`のカウンタ/ゲージを埋め込み、`/metrics`だけに応答する自前のHTTPサーバーを実装します。あわせて、実際のバックエンドで動作確認する過程で明らかになった、TCPのクローズ処理に関するいくつかの事実も扱います。

続きを読む

peek()で覗くだけのSNIルーティングと、非TLS接続が毎回200ms待たされる理由

TLSを終端せず、ClientHelloの先頭バイト列だけを覗いてSNI(server_name)を取り出し、バックエンドを振り分けるTLSパススルーを実装します。tokioのTcpStream::peek()でバイト列を消費せずに読む仕組みと、ClientHelloを手でパースする実装、そして「まだ受信中」と「そもそもTLSではない」を区別できないことで生まれる非TLS接続への遅延という設計上のトレードオフを扱います。

続きを読む

Proxy Protocol v1/v2でクライアントIPを伝播する設計と、プールが「使い捨て」だから両立できた理由

L4プロキシを経由すると、バックエンドから見た接続元IPは常にプロキシ自身のIPになってしまいます。Proxy Protocolのv1(テキスト)/v2(バイナリ)を実装し、実データの転送前に本来のクライアントIPをバックエンドへ伝える方法を扱います。あわせて、この仕組みが「コネクションを毎回使い捨てる」既存のプーリング設計だからこそノーコストで成立している、という設計同士の噛み合わせを見ていきます。

続きを読む

トークンバケット×Semaphoreによる二段のレート制限と、accept()より前には効かないという限界

送信元IP単位のトークンバケットと、コネクション単位のtokio::sync::Semaphoreを組み合わせて二段のレート制限を実装します。安いチェックを先に置く判定順序の理由と、この方式がTCPハンドシェイク完了後にしか発動しない――つまりSYNフラッドのような下位レイヤーの攻撃には無力である、という設計上の限界を扱います。

続きを読む

接続失敗時のリトライと、バックオフを入れない設計がフリート規模で牙を剥く理由

バックエンドへの接続確立に失敗したら、別のバックエンドへ即座にフェイルオーバーするリトライを実装します。サーキットブレーカーの役割はすでにパッシブ/アクティブヘルスチェックが担っているため、今回はリトライに専念します。あわせて、リトライにバックオフを入れなかった設計判断と、その判断が小規模と大規模(Cloudflare級)のフリートで正反対の意味を持つ理由を扱います。

続きを読む

SIGTERM/Ctrl+Cによるグレースフルシャットダウンと、2回目のシグナルが効かなくなる罠

新規コネクションの受け付けだけを止め、転送中のコネクションは自然に終わるまで待つグレースフルシャットダウンを実装します。JoinSetによるコネクション追跡とtokio::select!の組み合わせに加えて、シグナルハンドラを一度登録すると2回目以降のシグナルが誰にも届かず消えてしまうという、実機検証で見つけた落とし穴を扱います。

続きを読む

L4プロキシにおけるコネクションプーリングの再定義と、事前ウォームアップによる接続レイテンシの排除

HTTPのkeep-aliveと違い、生のバイト列をそのまま転送するL4プロキシでは「使い終わった接続の再利用」は成立しません。代わりに、バックエンドへの接続をあらかじめ張っておき、新規クライアント接続がTCPハンドシェイクを待たずに済むようにする、事前ウォームアップ型のコネクションプールを実装します。

続きを読む

実トラフィックの失敗を数えるパッシブヘルスチェックと、接続失敗と転送エラーの切り分け

定期的なTCP接続確認とは別に、実際に転送したコネクションの成否からバックエンドの健全性を判定するパッシブヘルスチェックを実装します。「バックエンドへの接続失敗」と「転送中のI/Oエラー」を区別して後者をバックエンドの責任にしない設計判断と、Cloudflare pingoraの実装との比較を扱います。

続きを読む

TCP接続によるアクティブヘルスチェックと、ロードバランサーからの動的除外

バックエンドの生死を定期的なTCP接続で確認し、死んでいるバックエンドをロードバランサーの選択候補から動的に除外する仕組みをRust+tokioで実装します。生死フラグの共有方法、並行したタイムアウト付き接続確認、「選べないかもしれない」という変化をtraitシグネチャとlet-else構文でどう表現するかを扱います。

続きを読む

trait経由で切り替え可能なロードバランサー設計と、4種のアルゴリズム比較

TCPコネクションを複数のバックエンドへ振り分けるロードバランサーをRustで実装します。アルゴリズムを実行時に差し替えられるようトレイトオブジェクトで抽象化したうえで、Round Robin・Random・Least Connections・Weightedの4種類を実装し、それぞれのアルゴリズム特有の設計判断(同着時の挙動やMutexが必要になる条件など)を扱います。あわせてモジュール分割とインラインユニットテストの設計にも触れます。

続きを読む

Rustの所有権を緩める道具 Rc/Arc/RefCell/Mutexと、Send/Syncが表すもの

Rustの「値の持ち主は1人まで」という所有権ルールは、複数の場所から同じ値を参照・共有したい場面ではそのままでは足りません。この制約を緩めるRc/Arc(複数の持ち主)とRefCell/Mutex(内部可変性)、その安全性を支えるアトミック命令とSend/Sync、実行時に型を確定させるdynについて、それぞれが解決する問題を切り分けて整理します。

続きを読む

tokioの`copy_bidirectional`による非同期TCPバイト列転送とhalf-closeの伝播

TCPコネクションを別のTCPコネクションへ中継する処理をRust+tokioで実装します。tokioのfeatureを用途別に絞って導入する方針や、`tokio::io::copy_bidirectional`が単純な双方向コピーではなく、TCPの片方向クローズ(half-close)を個別に伝播する設計になっている点を扱います。

続きを読む