HTTP/3をUDPロードバランサで分散するときの問題点 (AWS NLBで試してみた)

目次

はじめに

HTTP/3はQUICというトランスポートプロトコルを利用しています。QUICはUDPを利用していますが、QUIC自体はステートフルなプロトコルです。

ステートフルなQUICを、QUICを解釈しないUDPロードバランサでバランシングしようとするにはいくつかの注意問題点があります。今回は簡単に説明し、NLBでも実験をしてみました。


QUICの用語などは以前書いた記事を参照
asnokaze.hatenablog.com

UDPロードバランサ

QUICはステートフルですので、おなじQUICコネクションのUDPパケットは同じサーバに割り振ってやる必要があります

ロードバランサの分散アルゴリズムにはいくつかありますが、下記をとりあげます

  • ラウンドロビン: 来たパケットを順々に各サーバに振り分けます
  • ハッシュ: 送信元IP, 送信元ポート, 送信先IP, 送信先ポート などをもとにハッシュを計算し、振り分け先を決定します

ラウンドロビン方式

ステートレスなUDPパケットを順々に各サーバに振り分けます。
QUICコネクションを処理するサーバが変わってしまうため、ハンドシェイクすら成功しないでしょう。

ハッシュ方式

送信元IP, 送信元ポート が一緒であれば、同じサーバに振り分けられるため、一見うまくいくように見えます。

しかし、いくつかの注意点があります

ハッシュの再計算

1つめはハッシュの再計算が起こる点です。
UDPロードバランサは、特定のタイミングでハッシュの再計算が発生しえます。例えば、振り分け先のサーバが増減した際や、設定を更新した際に振り分け表がクリアされるロードバランサもあります(*)

ハッシュの計算が変わった場合、既存の通信の振り分け先が変わってしまいます。運良く再計算する前と同じサーバに振り分けられればいいですが、そうでなければStateless resetとなります(QUICパケットは暗号化する必要があり、鍵を失った際の切断方法)。実際は振り分け先の台数に依存しますが、現在扱っている接続のうち多くの接続が切断されることになります。

f:id:ASnoKaze:20191230200921p:plain

コネクションマイグレーションが出来ない

2つめはコネクションマイグレーションが出来ない点です。QUICのコネクションマイグレーションは以前書いたとおりです。
asnokaze.hatenablog.com

QUICでは送信元IPや送信元ポートが変わっても通信をそのまま継続できます。そのため、クライアントはそれらが変わったとしてもそのまま通信を継続しようとします。しかし、送信元IPや送信元ポートが変わるとUDPロードバランサによって振り分けられるサーバが変わるため、コネクションを維持できずStateless resetを行うことになります。

クライアントが開始するコネクションマイグレーションはPath validationの失敗するだけで通信は維持できますが、NATリバインディングなどの中間装置によって送信元ポートが変わる場合は先述の通りStateless resetとなります。(追記) NATの変換テーブルから消えてる場合ですので、通信がない時間がある場合に起こります。再接続で問題ないケースが多いでしょう。PINGフレームを送ることで回避もできそうです。

その他の懸念事項

その他にもいくつかの懸念事項があります

例えばUDPロードバランサは、コネクションクローズタイミングが分かりません。QUICはコネクションをクローズしたつもりはないのに、UDPロードバランサが割当表からエントリを先に削除してしまうと、別のサーバに割り振られてしまいます。これはPINGフレームを定期的に送ることで回避できます。

NLBを用いたハッシュの再計算の実験

AWS NLBでUDPロードバランサはハッシュを用いて振り分けを行っています(Docs)、ハッシュの再計算が発生するか確認します。

今回は簡易的に、HTTP/3ではなく単純にUDPを使用して実験を行います。

  • client: UDPで指定されたNLBに定期的にメッセージを送ります。(今回は送信元ポートを5つ用意し、並列にメッセージを投げる)
  • server: UDPでリッスンし、UDPパケットが届くと、自身のホスト名を返します。

(コード: https://gist.github.com/flano-yuki/4e70d4d38da87018d4ed8c035d27b765


今回はEIP付きのNLB(リスナープロトコルUDP)で、5つのインスタンスをターゲットグループに登録します (手抜きで、全部unhealtyで均等に処理させる)

  • ip-10-xxx-yyy-232
  • ip-10-xxx-yyy-181
  • ip-10-xxx-yyy-225
  • ip-10-xxx-yyy-74
  • ip-10-xxx-yyy-119

clientを実行すると、5つの送信元ポートでメッセージを送信し受信するのを、数秒おきに行います。5つの送信元ポートは同じ順序で使うため、縦に同じホストが表示されます。

しかし、インスタンスの増減を行ったタイミングで、ハッシュ計算が代わり振り分け先ホストが変わるのが確認できます。

$ cat ./client.rb 
ip-10-xxx-yyy-225 ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-181 
ip-10-xxx-yyy-225 ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-181 
ip-10-xxx-yyy-225 ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-181 
ip-10-xxx-yyy-225 ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-181 
ip-10-xxx-yyy-225 ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-181 
(timeout)               (timeout)               (timeout)              ip-10-xxx-yyy-74  (timeout)          
ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-74 ip-10-xxx-yyy-74 
ip-10-xxx-yyy-232 ip-10-xxx-yyy-181 ip-10-xxx-yyy-74 ip-10-xxx-yyy-74 ip-10-xxx-yyy-74 

ですので、HTTP/3 (QUIC)をNLBで負荷分散するのは適切とは言えません。

QUICの負荷分散について

QUICのロードバランシングについてはIETFでも議論が行われています。

基本的にはQUICを解釈してロードバランスを行います。
QUICのコネクションIDにサーバの識別子を暗号化して入れておき、それを見て適切なサーバに振り分けるなどの方法が考えられています。

その他にも、間違ったサーバに振り分けられた際に、自分宛てでないパケットを適切なサーバに転送するなども考えられますが、構成が複雑になってしまいそうですね。

ということで、通常はHTTP/3を終端しバックエンドにはHTTP/1.1で振り分けとかを行う構成が普通かなとは思います。(ゆくゆくはマネージドロードバランサも対応してくるでしょう)

QUIC-LBの提案仕様や、コネクションID-awareなNATに関するドキュメントを読むとより深く理解が出来ると思います(読めてない)


(*): 一般的なUDPロードバランサすべてに当てはまるわけではないので、表現を変更しました。
以前「UDPロードバランサは、特定のタイミングでハッシュの再計算を行います。例えば、振り分け先のサーバが増減した際や、設定を更新した際にハッシュの再計算が起こります。」
以後「UDPロードバランサは、特定のタイミングでハッシュの再計算が発生するものもあります。例えば、振り分け先のサーバが増減した際や、設定を更新した際に振り分け表がクリアされるロードバランサもあります(*)」