NICからの割り込みを適切なCPUに振り分けるための、IPヘッダオプション

NICからの割り込みを適切なCPUに振り分けられるようなHintをIPヘッダオプションとして定義する「Multiple Core Performance Hint Option」という提案が提出されています。(特許としても申請中とのこと)

ざっくり眺めて見ようかと思います。ここらへん詳しいわけではないので、間違いがあればご指摘ください。

背景

10Gbpsなどのトラフィックを処理する場合、NICからの割り込みを一つのCPUコアで処理しきれないことがあります。

NICからの割り込みを複数のCPUコアで分散する仕組みとして、Linuxでは「Receive Side Scaling (RSS)」、「Receive Packet Steering (RPS)」、「Receive Flow Steering (RFS)」などの仕組みがあります。

このとき、同じ通信フローの処理は同じコアで処理するのが好ましいです。TCPなど、処理するパケット順番が変わってしまうと、Head-of-Line-Blockingにつながります。

同一フローを同一コアに割り振るために、割り振りの方法として、送信元IP・送信先IP・送信元ポート・送信先ポートのハッシュから割当先を決める方法があるようです。


Multiple Core Performance Hint Option

先述の通り、CPUコアへの割り振りにL4の送信元ポート・送信先ポートを使用します。しかし、それらの値が取得できないケースがあります。例えば次の場合です

  • パケットがフラグメントしてる場合
  • IPSecを使用している場合

Multiple Core Performance Hint Option」では、そのような場合でも同一の通信フローを適切に割り振れるようにIPヘッダオプションを定義します。

MCPHINT

新しいIPヘッダのオプションとして、MCPHINTを定義します。
MCPHINTはIPv4, v6ともで利用でき、2バイトのデータ領域 (Differentiation Data)を持ちます。

このDifferentiation Dataを用いてフローを識別し、CPUに割り振ることを想定しています。
(送信側がフロー毎に異なるDifferentiation Dataを格納するという仕組み)

感想

実用性がどこまであるのかちょっと実感がないが、面白い提案だなとおもった。

NIC割り込み処理の割り振り先を決定するためのヒントとして、IPヘッダにオプションを新しく定義するとは、なるほど。

QUICのマルチキャスト拡張

QUICを使ってマルチキャスト配信を可能にする「Multicast Extension for QUIC」という仕様が、AkamaiのJake Holland氏らによって提案されています。

マルチキャストでデータを配信することで、よりネットワーク帯域を効率用使うことが出来ます。

まだ最初の提案であり、まだTBDとなってるところもあり、これから変わりそうだが一旦目を通しておく。

背景/関連動向

まず、この「Multicast Extension for QUIC」登場と、関連状況について簡単に説明します。

AkamaiのJake Holland氏はWebでマルチキャストを使うために幅広く活動されています。今回の「Multicast Extension for QUIC」もWebでコンテンツを配信するための流れでの提案となっています。(もちろんただのQUIC拡張ですので、用途はそれに限りません)

W3Cでは「Multicast Community Group」を設立しています。
asnokaze.hatenablog.com

また、実際にブラウザ(chromiumのfork)でマルチキャストデータを受信可能にするAPIを実装していたりします。
github.com

関連仕様

関連する仕様は次のとおりです

これらに、マルチキャストでアプリケーションデータを配信するためのQUIC拡張が追加されたというながれです。

Multicast Extension for QUICの仕様

Multicast Extension for QUIC」の仕様について眺めていきます。

ユニキャストで通常のQUICコネクションを確立した後に、マルチキャストチャネルとしてマルチキャストでデータ(1-RTTパケット)を送信できるようになります。もちろんマルチキャストチャネルはサーバ側からの一方向でしかQUICパケットを送れません。なお、マルチキャストの識別にはRFC4607で定義される(S,G)を用います。(マルチキャストルーティングについては下位層の話なので、この仕様では言及されません)

特徴としてはざっくりこんな感じ

  • QUICコネクションはRFC9000で定義された通り
  • マルチキャストデータは暗号化される(マルチキャストチャネル用の対象鍵により、他の参加者も同じデータを受信し処理できる)
  • マルチキャストチャネルのデータがパケットロスした場合は、QUICコネクション経由で再送してもらえる (なお、DATAGRAMフレームも使える)

この仕様では、拡張機能ネゴシエーション、クライアントのマルチキャストチャネルの参加/離脱、フロー制御などについて定義しています。またそのために、新しい拡張フレームやエラーコードを定義します(MC_で始まる一連の拡張フレーム)。

大まかな流れ

マルチキャストでデータ受信の大まかな流れ

  • クライアントは、QUICコネクション確立時にmulticast_client_paramsトランスポートパラメータで初期値をサーバに伝える
  • サーバは、MC_CHANNEL_ANNOUNCEフレームを送信し今存在するマルチキャストチャネルの情報をクライアントに送信します。
  • サーバは、MC_CHANNEL_JOINフレームでマルチキャストチャネルに参加を呼びかけます
  • クライアントは、サーバからの要求に対しMC_CLIENT_CHANNEL_STATEフレームを返します。MC_CLIENT_CHANNEL_STATEフレームは"参加"、"拒否"、"離脱"をサーバに伝えることが出来ます
  • クライアントは、マルチキャストチャネルでのデータ受信を開始します
  • その後サーバがMC_CHANNEL_LEAVE でクライアントに離脱を要求するか、もしくはクライアントが自発的に離脱するとマルチキャストチャネルのデータ受信が完了します
パケロス

マルチキャストチャネルのパケロスに対して、クライアントはユニキャストのQUICコネクションで再送を要求します。

MC_CHANNEL_ACKをクライアントから送ることで、パケロスしたパケットをサーバに伝えます。サーバは、ユニキャストのQUICコネクションで該当パケットのデータを再送します (QUICでは、パケロスしてもそのパケットをそのまま送り直すのではなく、再送が必要なデータのみを送ります)。

マルチキャストチャネルで受信可能なフレーム

マルチキャストチャネルで受信可能なフレームは次のとおりです。

  • PADDING Frames ([RFC9000] Section 19.1)
  • PING Frames ([RFC9000] Section 19.2)
  • RESET_STREAM Frames ([RFC9000] Section 19.4)
  • STREAM Frames ([RFC9000] Section 19.8)
  • DATAGRAM Frames (both types) ([RFC9221] Section 4)
  • PATH_CHALLENGE Frames ([RFC9000] Section 19.17)
  • MC_CHANNEL_PROPERTIES
  • MC_CHANNEL_LEAVE (however, join must come over unicast?)
  • MC_CHANNEL_INTEGRITY (not for this channel, only for another)
  • MC_STREAM_BOUNDARY_OFFSET
  • MC_CHANNEL_RETIRE

おわりに

今回はほんの一部の紹介でしたが、また機会があればフレームレベルで紹介したいと思います。

Webでのマルチキャスト配信は夢がありつつ、まだまだ熱量が高い領域ではありません。今後の動向が気になるところです。
技術的には大変おもしろいので追っていきたい。

40年越しにTCPの仕様(RFC793)が改訂される

TCPのコア部分の仕様は1981年に発行された「RFC793 TRANSMISSION CONTROL PROTOCOL」で標準化されています。

この、RFC793の改訂版となる「Transmission Control Protocol (TCP) Specification」は、2013年からIETFのTCPM WGで議論されてきましたが、4月4日にIESGによって承認されました(参考URL)。現在はRFC出版の準備に入っています(新しいRFC番号はこの後正式に決まります)

www.ietf.org

改めてTCPの仕様を読みたい場合はこのドキュメントを読むのが良さそう。

概要

この改訂版の仕様(通称 rfc793bis)は、RFC793が公開されて以来断片的に公開されたTCPに関連する追加のRFCを一つにまとめ直しています。

ですので、プロトコルとしてこの改訂により大きな変更があるわけではなく、すでに標準化されているものが整理される形になります。

例えばTCPの基本的な機能について述べた次のRFCが取り込まれます

その他にも「RFC5961 Improving TCP's Robustness to Blind In-Window Attacks」の内容を、このrfc793bisで更新します。

その他にも、すでに報告されていた仕様上のエラッタなどが修正されている他、新しく公開された輻輳制御アルゴリズム(RFC2581, RFC5681, RFC6298)など多くのドキュメントを参照するようアップデートされています。

RFC793からの変更点

rfc793bisでは「5. Changes from RFC 793」に、RFC793からの変更点がまとめられています。(先にあげたRFCの取り込みとかぶりますが、かいつまんで紹介)

  • RFC793のエラッタの修正
  • RFC1011、RFC1122、RFC6093に記載されている緊急ポインタの仕様の変更の取り込み
  • RTOの説明がRFC6298を参照するように更新
  • RFC6528の初期シーケンス番号生成アルゴリズムの取り込み
  • RFC6429で記述されるている持続的接続のリソース消費に関する考慮事項について注記
  • 輻輳制御の実装上の説明を追加

TCPのRSTパケットに拒否理由をつける提案仕様

TCPの接続拒否理由を通知する「TCP RST Diagnostic Paylaod」という提案仕様がIETFに提出されています。

TCPでは様々な理由により接続拒否を示すRSTパケットが送信されます。NATやファイアウォール、リッスンされてないポートへの接続時などです。

しかしRSTパケットの受信者は、RSTパケットが返された理由を知る手段はありませんでした。それを可能にするのが「TCP RST Diagnostic Paylaod」です。

TCP RST Diagnostic Paylaod

TCP RST Diagnostic Paylaod」ではRSTパケットに接続拒否理由の格納方法を指定します

もともと、RSTパケットにデータを含めることができます。TCPの仕様の改訂版である「rfc793bis」でもSHOULDで指定されています。

TCP implementations SHOULD allow a received RST segment to include data (SHLD-2)

そこに次のような情報をCBOR形式で情報を格納します。例えば次のとおりです (例はCDDLで記述します)

[
  12345,
  {
    "reason-code": 2, 
    "reason-description": "brief human-readable description"
  }
]
  • 12345: この仕様のデータが含まれていることを示すマジックナンバー (後で正式に決定)
  • reason-code: 接続拒否理由を示す番号
  • reason-description: 人間が読める理由を示す文字列
reason-code

reason-codeとして次のものが定義されています

  • 1: クローズ後にデータを受信した
  • 2: リッスン状態なのにACKを受信した
  • 3: 不正なメッセージ
  • 4: 許可されていない
  • 5: リソースの超過
  • 6: ネットワークの障害 (中継装置などで利用)
  • 7: ピアによってリセットを受信した (中継装置などで利用)
  • 8: ピアへの到達性がない (中継装置などで利用)
  • 9: コネクションタイムアウト (中継装置などで利用)
Diagnostic Payloadの扱い

このデータはアプリケーションに渡すことが許可されています。デバックに使用することができます。

Memcachedのproxy機能を触ってみる

Memcashedでは、1月にリリースされた version 1.6.13から実験的にProxy機能が搭載されています。

公式のドキュメントはコチラ
github.com

概要

主な通信の流れは次の通りです
f:id:ASnoKaze:20220404003718p:plain
(通信内容は簡易的に記載するが、実際は通常のmemcacheプロトコルです)

  • memcachedをproxyモードで起動する際、グループ毎にサーバプールを指定する (例: foo, bar)
  • Clinetから値をsetする際は、keyにサーバプールを示すプレフィックスを付ける (例: /foo/, /bar/)
  • Proxyは、サーバプールを示すプレフィックスを取り除いたものを、バックエンドのmemcachedサーバにsetする
    • サーバプールに複数サーバがいる場合は、Keyのハッシュ計算に基づいて振り分け先が決まる (例: /foo/key1, /foo/key2)。

上記はsetの例だが、getも同様に動作する

サーバプールの指定

サーバプールの指定は、luaスクリプトで記述する。
サーバプール毎にnameを指定し、バックエンドとなるmemcachedサーバを指定するだけです。

local s = require("lib/simple")

-- by default, sends "/foo/*" to "foo" and "/bar/*" to "bar"
s.pool{
    name = "foo",
    backends = {"127.0.0.1:11212", "127.0.0.1:11213"},
}

s.pool{
    name = "bar",
    backends = {"127.0.0.1:11214", "127.0.0.1:11215"},
}

起動時に proxy_configを読み込む

./memcached -o proxy_config=./example.lua
stats proxy

proxyモードのmemcachedは "stats proxy" コマンドで状態を統計情報を取得できる

$ echo "stats proxy" |nc  localhost 11211
STAT user_simple_failovers 0
STAT cmd_mg 0
STAT cmd_ms 0
STAT cmd_md 0
STAT cmd_mn 0
STAT cmd_ma 0
STAT cmd_me 0
STAT cmd_get 1
STAT cmd_gat 0
STAT cmd_set 3
(略)
END

ビルド

現状この機能を使うには、自分でビルドすることになる。手順は公式ドキュメントの通りです
github.com

終わりに

まだ実装は実験段階だが面白い機能です。
luaスクリプト弄ったらもっといろいろ出来るかもしれないので触ってみる。単純にrepolicationとか出来ると嬉しいかも。

(最近はほぼマネージドサービス使っちゃうので、10年くらい早く欲しかった...)

Sec-CH-IP-Geoヘッダの仕様

The IP Geolocation HTTP Client Hint」という提案仕様が、AppleGoogleの方の共著でIETFに提出されています。

簡単に見ます。

背景

ユーザの位置情報はプライバシーの課題がありつつも、VPNサービスなどによりIPジオロケーションが取れないと困ることもありそうです。権利などの問題で配信できる国が限定されているといったこともあるかと思います。

そのために、例えばPrivate Relayでは IPジオロケーションを公開しています(URL)。

サーバとしても、VPNサービスのIPジオロケーションリストを網羅するのは難しいように思われます。そこで、クライアントからIPジオロケーション情報を通知できるようになると便利そうです。

The IP Geolocation HTTP Client Hint

この仕様では、クライアントの位置情報をHTTP Client Hintの仕組みでサーバに送信します。

サーバは、Accept-CHレスポンスヘッダでSec-CH-IP-Geoを受け入れる意思を伝えます。

    Accept-CH: Sec-CH-IP-Geo

クライアントは、Sec-CH-IP-Geoリクエストヘッダで位置情報をサーバに通知します。

    Sec-CH-IP-Geo = "US,US-AL,Alabaster"

Sec-CH-IP-Geoヘッダの位置情報表記は 「RFC 8805 A Format for Self-Published IP Geolocation Feeds」の仕様の通りになります。

おまけ

クライアントにジオロケーション情報を送ってもらう方式として、もともとAppleの方はGeoHashを用いる提案をしていました。議論のすえ、今回のような仕様に変更されました。

仕様の共同著者らがその点についてgithubで議論してたりもします。参考までに。
github.com

HTTP DatagramsとCapsuleプロトコルについて

HTTP Datagrams and the Capsule Protocol」という提案仕様が出されている。長く議論されている仕様ではあるが、動きがあったので眺めておく

背景

トランスポートプロトコルであるQUICでは、パケットロスがあったとしてもアプリケーションプロトコルのデータは再送してピアに届けられます。

「RFC9221 An Unreliable Datagram Extension to QUIC」という仕様で定義される、QUIC DATAGRAMフレーム拡張では、信頼性のないアプリケーションデータの送受信が可能になります。DATAGRAMフレームのデータはパケットロスで失われても再送する必要はありません。

DATAGRAMフレームは受信したものから処理することができます、アプリケーションの要件によりデータの再送が必要な場合はアプリケーションレイヤで再送信を行うこともできます。

f:id:ASnoKaze:20220331001525p:plain

WebTransportやMASQUE(CONNECT-UDP)といった仕様では、このQUIC DATAGRAM拡張をHTTPで利用します。ただし、普通のQUIC DATAGRAMフレームはストリーム上で送信されません。HTTPでDATAGRAMを送受信するには、HTTPメッセージと関連付ける必要があります。それを定義するのが「HTTP Datagrams and the Capsule Protocol」です。

もともとHTTP/3を前提にしていましたが、途中にProxyが居るケースなどを想定しHTTP/1.1やHTTP/2も想定したCapsuleプロトコルとともに標準化が進められています。

なお、WebTransportやMASQUE(CONNECT-UDP)については過去に書いた記事を御覧ください

(HTTP DatagramはWebTransportなどで利用されます。通常のHTTPリクエスト・レスポンスで使う想定は今のところありません)

HTTP DatagramsとCapsuleプロトコル

HTTP/3, HTTP/2, HTTP/1の場合でそれぞれ、データの送り方が違います。

  • HTTP/3では、QUIC DATAGRAMフレームを利用します
  • HTTP/2では、拡張CONNECTでトンネルを確立し、CAPSULEプロトコルを使ってデータを送信します。(信頼性のない通信はできません)
  • HTTP/1では、HTTP Upgradeの手順でトンネルを確立し、CAPSULEプロトコルを使ってデータを送信します。(信頼性のない通信はできません)

常にHTTP/3(QUIC)が使えるとは限りません、QUICがブロックされている環境では、HTTP/2やHTTP/1.1でDatagramメッセージを送れる必要があります。このときCAPSULEプロトコルという仕組みでは、TCPなHTTPでDatagramメッセージをやりとりできるようになります。

HTTP/3

HTTP/3では、QUIC DATAGRAMフレームに以下のデータを格納します
f:id:ASnoKaze:20220331002111p:plain

  • Quarter Stream IDは、このDatagramが関連付けられれたHTTPリクエストのストリームIDが格納されます (クライアントが開始したストリームが前提なので4で割った数値で十分)
  • HTTP Datagram Payloadは、アプリケーションのデータです

ご覧の通り、以前まであった、一つのストリームに複数のDatagaram フローを関連付ける仕組みはなくなりました (CONNECT-UDPといったより上位のプロトコルで定義してたりします)。

なおこの拡張を利用するには、H3_DATAGRAM SETTINGSパラメータが有効になっている必要があります。

HTTP/1.1とHTTP/2におけるコネクションの確率

DatagramメッセージをTCPなHTTPプロトコルで送信できるようにします。

HTTP/1.1の場合は Upgradeの手順をもって通信を一旦確立します。その後、そのコネクション上でデータをやり取りします
```
GET https://proxy.example.org/.well-known/masque/udp/192.0.2.42/443/ HTTP/1.1
Host: proxy.example.org
Connection: upgrade
Upgrade: connect-udp
```

HTTP/2の場合は拡張CONNECTの手順をもって通信を一旦確立します。その後、そのコネクション上でデータをやり取りします。
```
HEADERS
:method = CONNECT
:protocol = connect-udp
:scheme = https
:path = /.well-known/masque/udp/192.0.2.42/443/
:authority = proxy.example.org
```

やりとりするデータはCAPSULEプロトコルに則ります

CAPSULEプロトコル

CAPSULEプロトコルでは確立された通信路で、次の形式でメッセージを送受信します。ここに、Datagramデータが入ることになります。
f:id:ASnoKaze:20220331003315p:plain

(なお、CAPSULEプロトコルはType, Length, Valueの形式をしており、独自のTypeを定義し任意のメッセージをカプセル化できます)

おまけ、あるいは動向

HTTP Datagrams and the Capsule Protocol」ではあくまでHTTP DatagramsとCapsuleプロトコルを定義しているだけで、上位のプロトコルがどのように使うかは定義されていません。

WebTransportやCONNECT-UDPの仕様を見るとよりイメージが湧くかと思います。

先日行われたIETF113では、WebTransport over HTTP/2でCAPSULEプロトコルを利用するのか議論がありましたが結論はでてません。まだまだ動きがありそうです。