メインコンテンツへスキップ
見出し画像

UDPは「投げて終わり」では足りない――QUICがlibuvに求めた新しい情報

    導入:UDPはデータを投げるだけではないの?

    💚ずんだもん「UDPって、相手と接続しているかを確認せず、データをぽんっと投げる仕組みなのだ。TCPより単純で、細かいことは気にしなくていいのだ?」


    🎀めたん「基本的な理解としては間違っていませんわ。従来のlibuvのUDP APIも、受信したデータと送信元アドレスを受け取る用途には十分でしたの」


    💚ずんだもん「届いたデータと、誰が送ったかが分かる。それで返信もできそうなのだ」


    🎀めたん「簡単なUDPアプリケーションなら、それで足りますわ。しかし、UDPの上にQUICのようなトランスポートプロトコルを実装すると、データ本体だけでは判断できないことが増えますの」


    ⚔️つるぎ「James M. Snell氏が提案しているのは、libuvへQUICの処理を組み込むことではない。OSが持っている混雑情報、宛先情報、経路情報、高速化機能を、上位の実装から利用できるようにすることだ。なお、この提案はlibuv v2向けのプルリクエストとして現在もレビュー中であり、APIの形は今後変わる可能性がある」

    UDPの仕組みが急に複雑になったのではない。UDPの上で動かすものが複雑になった。

    パケットが消える前に混雑を知りたい

    💚ずんだもん「ネットワークが混雑しているかどうかは、パケットが消えてから分かるのではないか? 届かなかったら、送りすぎたと判断するのだ」


    🎀めたん「パケット損失は重要な手掛かりですが、損失が起きる前に混雑を知らせる仕組みもありますわ。ECN、明示的輻輳通知ですの」


    💚ずんだもん「ルーターが『そろそろ混んできたのだ』と教えてくれるのだ?」


    🎀めたん「その理解で十分ですわ。送信側がECNを利用できる印をパケットに付け、経路上のルーターが、破棄する代わりに混雑の印を付けます。受信側はその情報を読み取り、送信側へ報告しますの」


    ⚔️つるぎ「libuvの役割は、送信時にECNを設定でき、受信時にコードポイントを取得できるようにすることだ。その情報を見て送信量を減らすかどうかは、QUIC実装の輻輳制御が判断する」

    libuvが混雑を判断するのではない。判断材料をQUICへ渡す。

    安全に送れるパケットサイズは経路によって違う

    💚ずんだもん「パケットが大きいと問題になるなら、最初から小さいパケットだけ送れば安全なのだ」


    🎀めたん「安全ではありますが、小さすぎるパケットを使い続けると、同じデータを送るためのパケット数が増えますわ。ヘッダー処理や送受信処理の回数も増え、効率が落ちますの」


    💚ずんだもん「では、できるだけ大きくすればいいのだ?」


    🎀めたん「大きすぎてもいけませんわ。経路には、断片化せず安全に通せる最大サイズがあります。それが経路MTUですの。経路によって値が違うため、送信側は実際に観測しながら適切なサイズを探す必要があります」


    ⚔️つるぎ「QUICではIPフラグメンテーションを避ける必要がある。大きすぎる送信が失敗したときのエラーや、OSが保持するMTU情報を上位層が取得できなければ、QUIC実装は経路に合わせてパケットサイズを調整できない」


    🎀めたん「libuvが最適なサイズを決めるのではありませんわ。探索に必要な送信エラーやMTU情報を隠さず、QUIC実装へ渡すことが重要ですの」

    安全側へ倒すだけでは、正しくても効率が悪い。経路を観測して調整する必要がある。

    どこから来たかだけでなく、どこに届いたか

    💚ずんだもん「送信元アドレスが分かれば、その相手へ返信できるのだ。それ以外に何が必要なのだ?」


    🎀めたん「サーバーが一つのIPアドレスしか持たないとは限りませんわ。複数のネットワークインターフェイスや、複数のIPv4・IPv6アドレスを持つことがありますの」


    ⚔️つるぎ「サーバーが0.0.0.0や::で待ち受けると、複数のローカルアドレス宛てのパケットを同じソケットで受信できる。だが、従来の単純な受信APIでは、送信元は分かっても、どのローカルアドレス宛てに届いたのかが分からない」


    💚ずんだもん「相手の住所は分かるけれど、自分のどの玄関に届いたのか分からない状態なのだ」


    🎀めたん「そのために使うのがpktinfoですわ。パケットが届いたローカルIPアドレスや、ネットワークインターフェイス番号を取得できますの。応答を適切なローカルアドレスから返すためにも必要です」


    ⚔️つるぎ「QUICでは、送信元と宛先の組み合わせが通信経路の識別に関係する。クライアントがWi-Fiからモバイル回線へ切り替わるような接続移行を扱ううえでも、パケットがどの経路を通ってきたかという情報は重要だ」

    通信では「相手が誰か」だけでなく、「自分のどこへ届いたか」も重要になる。

    UDPパケットを一個ずつ処理しない

    💚ずんだもん「UDPはパケット単位の通信なのだ。一個ずつ送って、一個ずつ受け取るのが普通ではないか?」


    🎀めたん「通信の意味としてはパケット単位ですわ。ただし、アプリケーションとOSがパケットごとに何度も往復する必要はありませんの」


    ⚔️つるぎ「パケットごとにシステムコールを行えば、そのたびにユーザー空間からカーネルへ処理を渡す。高負荷なサーバーでは、データを処理する時間より、この往復コストが目立つことがある」


    🎀めたん「送信側のGSOは、複数パケット分のデータを大きなバッファーとしてカーネルへ渡し、カーネルやNICに個別のUDPパケットへ分割させますわ。一回の送信処理で、複数のパケットを送り出せますの」


    💚ずんだもん「まとめて渡して、最後の仕分けをOSに任せるのだ」


    🎀めたん「受信側のGROはその反対ですわ。複数のUDPパケットをまとめてユーザー空間へ渡します。システムコールやコールバックの回数を減らせるため、大量のパケットを扱うQUICサーバーでは大きな効果が期待できますの」


    ⚔️つるぎ「ただし、提案されているGSOとGROは主にLinuxの機能だ。他の環境で同じ高速化が利用できるとは限らない。共通APIを設けても、各OSの能力差まで消えるわけではない」

    高速化とは、回線速度を上げることだけではない。OSとの往復回数を減らすことでもある。

    新しいUDP型を作らず、既存のAPIを拡張する

    💚ずんだもん「そんなに機能が増えるなら、uv_udp2_tみたいな新しい型を作ったほうが分かりやすいのではないか?」


    ⚔️つるぎ「実際、最初の構想では新しいUDPハンドルを作る案だった。従来の単純なUDPと、高度なUDPを型で分ければ、利用者から見た違いは明確になる。分かりやすさという利点はある」


    🎀めたん「でも、似たUDP型が二つ並ぶと、どちらを使うべきか判断する必要が生まれますわ。内部実装、ドキュメント、関連ライブラリも二系統になりかねませんの」


    ⚔️つるぎ「そこで現在の提案は、既存のuv_udp_tを維持する方向へ変更された。必要な機能だけをフラグで有効にし、詳しいメタデータが必要な利用者には、新しい受信APIを用意する」


    💚ずんだもん「普通のUDPを使う人は、今までどおりでいいのだ?」


    ⚔️つるぎ「そのとおりだ。新しいフラグや受信APIを使わなければ、従来のコードへ変更を要求しない。高度な機能を追加しながら、その複雑さを全利用者へ押し付けない設計になっている」

    新機能の設計では、何を追加するかだけでなく、既存利用者へ何を要求しないかも重要になる。

    libuvはQUICライブラリになるわけではない

    💚ずんだもん「ECNや経路MTUまで扱うなら、libuvが少しずつQUICライブラリになっていくのだ?」


    ⚔️つるぎ「ならない。責務を分けて考える必要がある」


    ⚔️つるぎ「OSは、パケットの送受信、ECNの設定と取得、MTU情報、GSOやGROといった機能を提供する。libuvはOSごとのAPI差を吸収し、共通のI/O APIとして公開する」


    🎀めたん「その上にあるQUIC実装が、輻輳制御、再送、経路管理、接続状態の管理を行いますの。さらにNode.jsが、それらをJavaScriptから扱えるAPIとして公開しますわ」


    💚ずんだもん「libuvは情報を運ぶけれど、その意味を決めるところまでは踏み込まないのだ」


    ⚔️つるぎ「そうだ。libuvが『混雑しているから送信量を三割減らす』と判断してはいけない。それはQUIC実装の責務だ」


    🎀めたん「抽象化は、下位層の情報をすべて隠すことではありませんの。上位層が自分の責務を果たすために必要な情報まで消してしまえば、その抽象化が実装を妨げますわ」

    抽象化とは、情報をすべて消すことではない。判断する層へ、必要な情報を安定した形で渡すことである。

    QUIC専用の改善ではない

    💚ずんだもん「でも、ここまでの話はほとんどQUICのためなのだ。結局、QUIC専用の機能ではないか?」


    🎀めたん「QUICが直接の動機であることは確かですわ。でも、追加される情報や機能はQUICだけのものではありませんの」


    🎀めたん「ECNはWebRTCやRTPのようなリアルタイム通信で、パケット損失を待たずに送信量や品質を調整する材料になります。pktinfoは、複数のアドレスを持つDNSサーバーやオンラインゲームのサーバーでも役立ちますわ」


    ⚔️つるぎ「GSOやGROは、ログ転送、メトリクス収集、高負荷な独自UDPプロトコルにも利用できる。Path MTU Discoveryも、断片化を避けながら効率的なパケットサイズを選びたいプロトコル全般に関係する」


    💚ずんだもん「QUICだけが特別な情報を欲しがったのではなく、QUICが今まで隠れていた不足を目立たせたのだ」


    ⚔️つるぎ「その整理が正しい。単純な用途では問題にならなかった不足が、高度なUDPプロトコルを実装したことで表面化した」

    新しいプロトコルは、新しい機能を求めるだけでなく、既存の抽象化が隠していた不足を見つける。

    まとめ:UDPが変わったのではなく、要求が変わった

    💚ずんだもん「従来のlibuvのUDP APIが間違っていたわけではないのだ。データと送信元が分かれば足りる用途には、ちゃんと役立ってきたのだ」


    🎀めたん「ところがQUICでは、パケット本体に加えて、混雑を示すECN、経路に合ったサイズを探るMTU情報、自分のどこへ届いたかを示すpktinfo、大量処理を効率化するGSOやGROが必要になりましたの」


    ⚔️つるぎ「そして重要なのは、libuvが輻輳制御や経路管理を引き受けないことだ。OSが提供する情報と機能を共通APIとして公開し、プロトコル上の判断はQUIC実装へ残す」


    🎀めたん「既存のuv_udp_tを維持し、必要な利用者だけが新しい機能を有効にする設計も大切ですわ。機能を増やすことと、既存利用者を守ることを両立させていますの」


    💚ずんだもん「QUICがlibuvをQUIC専用に変えるのではなく、UDPの能力をもっと正確に見えるようにしたのだ」


    ⚔️つるぎ「libuvに必要なのは、QUICを知ることではない。QUICが判断するためのUDP情報を、隠さず渡すことだ」

    UDPは単純なままでも、その上に構築するプロトコルは単純ではない。だからI/O基盤には、上位層が判断できるだけの観測手段が必要になる。

    参考記事


     
     

    Masaki

     
     
    ChatGPT による記事生成の実験アカウントです。記事に登場するずんだもん、四国めたん、中部つるぎは「東北ずん子・ずんだもんプロジェクト」のキャラであり、VOICEVOX の音声モデルです。添付画像は公式及び坂本アヒル氏のイラストをもとに AI で加工しています。

    あなたへのおすすめ