> [!abstract] 概要(abstract 日本語訳) > 多くのアプリケーション(ルーター、トラフィックモニター、ファイアウォール等)は、非常に高速なリンクでも線速でパケットを送受信する必要がある。本論文では netmap を紹介する。netmap は、カスタムハードウェアやアプリケーションの変更なしに、コモディティ OS が 1〜10 Gbit/s リンクを流れる毎秒数百万パケットを処理できるようにする新しいフレームワークである。 > > netmap の構築にあたり、パケット処理コストの主要な 3 つの原因を特定し、削減または排除に成功した。パケットごとの動的メモリ割り当ては、リソースをあらかじめ確保することで排除した。システムコールのオーバーヘッドは、大きなバッチで償却した。そしてメモリコピーは、カーネルとユーザー空間の間でバッファとメタデータを共有することで排除した。これはデバイスレジスタや他のカーネルメモリ領域へのアクセスを保護したままで実現している。これらの技法は個別には過去にも使われている。本提案の新規性は先行研究のほとんどの性能を上回るだけでなく、既存の OS プリミティブと緊密に統合され、特定ハードウェアに縛られず、使用・保守が容易なアーキテクチャを提供する点にある。 > > netmap は FreeBSD と Linux において複数の 1 Gbit/s および 10 Gbit/s ネットワークアダプタに実装された。プロトタイプでは、900 MHz で動作するシングルコアが 14.88 Mpps(10 Gbit/s リンクのピークパケットレート)を送受信できる。これは従来の API より 20 倍以上高速である。netmap 上で動作する libpcap エミュレーションライブラリを介した user-space Click などのパケット転送アプリケーションでも 5 倍以上の高速化が達成された。 ## 論文情報 - **タイトル**: netmap: A Novel Framework for Fast Packet I/O - **著者・所属**: Luigi Rizzo(Università di Pisa, Italy) - **助成**: EU FP7 project CHANGE (257422) - **媒体**: 2012 USENIX Annual Technical Conference (USENIX ATC '12) - **発表**: 2012-06、Boston, MA、pp.101--112 - **受賞**: Best Paper - **URL**: https://www.usenix.org/conference/atc12/technical-sessions/presentation/rizzo - **PDF**: `.raw/papers/atc12-final186.pdf` - **ホームページ**: http://info.iet.unipi.it/~luigi/netmap/ ## 問題設定 **入力と出力**: 汎用 OS 上でパケット処理アプリケーション(ルーター・モニター・ファイアウォール・トラフィックジェネレーター)が、1〜10 Gbit/s NIC のワイヤレート(= 10 Gbit/s で **14.88 Mpps**)を達成できるようにしたい。 **問題の核心**: 汎用 OS が提供する Raw ソケット・BPF・AF_SOCKET 系 API は毎秒数百万パケット規模を想定して設計されておらず、3 種類の根本的コストが積み重なっている。 | コスト | 内容 | |--------|------| | パケットごとの動的メモリ割り当て | `kmalloc`/`kfree` の繰り返し | | カーネル-ユーザー空間コピー | `copy_to_user` / `copy_from_user` | | システムコールのオーバーヘッド | 1 パケット(または少数バッチ)ごとに `sendto`/`recvmsg` | ### FreeBSD sendto() の実測プロファイル(Figure 2) FreeBSD HEAD 64-bit、i7-870 @ 2.93 GHz + Turboboost、Intel 10G NIC + ixgbe ドライバ上で `sendto()` を経路計測した結果(論文 Figure 2 より): | 処理階層 | 関数 | 累計時間 | 差分 | |----------|------|---------|------| | ユーザープログラム | `sendto` | 8 ns | — | | カーネル入口 | `sys_sendto` | 104 ns | 96 ns | | | `kern_sendit` | 118 ns | — | | ソケット層 | `sosend_dgram` | 146 ns | **137 ns**(mbuf 割り当て・copyin) | | UDP 層 | `udp_output` | 273 ns | 57 ns | | IP 層 | `ip_output` | 330 ns | **198 ns**(ルート探索・IP ヘッダ構築) | | Ethernet 層 | `ether_output` | 528 ns | **162 ns**(MAC ヘッダ探索・コピー) | | NIC ドライバ | `ixgbe_xmit` | 730 ns | **220 ns**(mbuf 操作・デバイスプログラミング) | | **ワイヤ到達** | — | **950 ns** | — | 950 ns/pkt = 1.05 Mpps が生の socket API での上限であり、10 Gbit/s 線速の 14.88 Mpps まで **14 倍以上**の余裕が必要。局所最適(ルートキャッシュ等)では焼け石に水で、全レイヤーの根本的改変が必要と Rizzo は示す。 ## 提案手法 ### 設計原則 性能を得るために OS の保護機構を捨てない。デバイスレジスタやカーネルの重要メモリ領域はユーザー空間から隔離する。netmap は「ゼロコピー」と「安全性」を両立させる点が先行研究との最大の差異。 ### アーキテクチャ概要(Figure 3) インタフェースを netmap モードに切り替えると、NIC はホストプロトコルスタックから**部分的に切り離される**。アプリケーションは共有メモリの環状キュー(netmap rings)を通じて NIC およびホストスタックとパケットを交換する。 **Figure 3: netmap モードのアーキテクチャ** ![[_attachments/atc12-final186/fig03-netmap-mode-architecture.png]] (Figure 3. NIC rings は netmap API を通じてアプリと通信し、ホストスタックへは別のリングペアで接続。OS は通常の管理機能(ifconfig 等)を継続して実行できる。Source: Rizzo 2012, ATC '12.) ### データ構造(Figure 4) 3 種類のユーザー可視オブジェクトを、カーネルが非ページアブル領域に確保した**単一の共有メモリ領域**に配置する。 **Figure 4: netmap の共有メモリ構造** ![[_attachments/atc12-final186/fig04-shared-memory-structures.png]] (Figure 4. netmap_if → netmap rings → pkt_buf の 3 層構造。全インタフェースのオブジェクトが単一リージョンに共存し、インタフェース間のゼロコピー転送を可能にする。Source: Rizzo 2012, ATC '12.) | オブジェクト | 説明 | |------------|------| | **パケットバッファ** | 固定サイズ(2 KB)。NIC とユーザープロセスが物理アドレスと仮想アドレスの両方で参照。DMA エンジンも同じバッファを使う | | **netmap ring** | NIC の環状キューのデバイス非依存レプリカ。`ring_size`・`cur`・`avail`・`buf_ofs`・`slots[]` から構成 | | **netmap_if** | インタフェース記述子。リング数とリングへのオフセット配列を保持 | すべての参照はポインタではなく**オフセット**(親データ構造からの相対距離)で実装する。複数プロセスが異なる仮想アドレス空間でマップしても正しく動作するため。 #### データ所有権規則 - `cur` から `cur + avail - 1` のバッファ → **ユーザー空間が所有**(アプリが読み書き可能) - それ以外のバッファ → **カーネル/NIC が所有**(アプリは触れてはならない) - システムコール実行中のみ、カーネルがリングを更新する。割り込みハンドラや他のカーネルスレッドは **netmap ring に触れない** ### API ```c /* 1. インタフェースを netmap モードへ */ fds.fd = open("/dev/netmap", O_RDWR); strcpy(nmr.nm_name, "ix0"); ioctl(fds.fd, NIOCREG, &nmr); /* モード切り替え */ p = mmap(0, nmr.memsize, fds.fd); /* 共有メモリをプロセスにマップ */ nifp = NETMAP_IF(p, nmr.offset); /* 2. 送受信ループ */ fds.events = POLLOUT; for (;;) { poll(fds, 1, -1); /* ブロック: バッファ空き待ち */ for (r = 0; r < nmr.num_queues; r++) { ring = NETMAP_TXRING(nifp, r); while (ring->avail-- > 0) { i = ring->cur; buf = NETMAP_BUF(ring, ring->slot[i].buf_index); /* buf にペイロードを書き込む */ ring->slot[i].len = /* パケット長 */; ring->cur = NETMAP_NEXT(ring, i); } } /* POLLOUT 後の poll() は NIOCTXSYNC を内包するため明示的 ioctl 不要 */ } ``` これがパケットジェネレーター(Section 5.2 の計測に使用)のコア。ユーザーランドライブラリ不要で、ヘッダファイル 200 行のマクロだけで完結する。 #### 主要 ioctl | ioctl | 方向 | 動作 | |-------|------|------| | `NIOCREG` | 設定 | インタフェースを netmap モードへ切り替え。リングを確保し共有メモリサイズを返す | | `NIOCTXSYNC` | 送信 | 新しい TX パケットを NIC に通知し、完了したスロットを解放。ノンブロッキング | | `NIOCRXSYNC` | 受信 | 新着パケット数を確認し `avail` を更新。ノンブロッキング | | `select`/`poll` | 待機 | `avail > 0` になるまでブロック。復帰時に `NIOC*SYNC` 相当の更新を実施 | NIOC*SYNC は**ノンブロッキング**で、ゼロコピー(スロット同期のみ)。カーネル側処理の主な仕事は「スロットのバリデーションと NIC リングとの同期」だけで、データ移動は発生しない。 #### マルチキュー対応 NIC が複数の TX/RX リングを持つ場合、ファイルディスクリプタを「全リング一括制御」モードか「単一リングペア制御」モードに設定できる。後者により複数スレッドが各自の fd で独立したリングを操作でき、標準の `setaffinity()` でスレッドを特定コアに紐付けるだけで並列処理が実現する。 ### ホストスタックとの通信 netmap モードでも OS はインタフェースを管理し続ける。ホストスタックからの/へのトラフィックは追加の netmap リングペア("host stack" リング)で仲介される。`NIOCTXSYNC` はこれらのバッファを mbuf にカプセル化してホストスタックに渡す。 ### ゼロコピー・パケット転送(Section 4.5) インタフェース間のパケット転送はバッファインデックスを**スワップ**するだけで完了する: ```c src = &src_nifp->slot[i]; dst = &dst_nifp->slot[j]; /* バッファインデックスを交換 */ tmp = dst->buf_index; dst->buf_index = src->buf_index; src->buf_index = tmp; /* 長さとフラグを更新 */ dst->len = src->len; dst->flags = src->flags = BUF_CHANGED; ``` コピーゼロで入力リングに空きバッファが補充され、同時に出力リングにパケットがエンキューされる。メモリアロケータは一切介在しない。 ### libpcap 互換レイヤー(Section 4.6) `pcap_dispatch()` / `pcap_inject()` を netmap 呼び出しに写像するプリロードライブラリ。**実装はわずか 20 行**。`select`/`poll` のような標準同期プリミティブを netmap が使うため、互換が容易。既存アプリ(tcpdump・Click・OpenvSwitch 等)をソース無改変で高速化できる。 ### 実装(Section 4.7) | 項目 | 内容 | |------|------| | FreeBSD 実装規模 | 約 2000 行(ioctl + select/poll + ドライバサポート) | | ユーザー空間ライブラリ | 不要。ヘッダ 200 行のマクロのみ | | Linux 版 | FreeBSD コアを再利用 + 小さな互換ラッパー | | ドライバ改変量 | 各 NIC ドライバあたり約 500 行の機械的変更 | | 対応 NIC | Intel 82599/ixgbe(10G)、Intel e1000(1G)、Realtek RTL8169 など | ドライバがシステムコールのコンテキストで動作するため(割り込みコンテキストではなく)、キャッシュ局所性が向上し、プロセスを特定コアに紐付けるリソース管理が容易になる。 ## 新規性——先行研究との対比(Section 3) **Figure 1: NIC データ構造(既存 OS の仕組み)** ![[_attachments/atc12-final186/fig01-nic-data-structures.png]] (Figure 1. NIC rings は物理アドレスでバッファを参照し、OS は mbuf という独自コンテナでバッファを管理する。この二重管理と変換コストが高速 I/O の障壁。Source: Rizzo 2012, ATC '12.) | 先行研究 | アプローチ | netmap との差異 | |---------|-----------|----------------| | **BPF / AF_PACKET** | Raw ソケット。1 パケットごとにコピーと syscall | コピー・per-packet syscall ともに残る | | **PF_RING + PACKET_MMAP** | 共有メモリでバッファをエクスポート | sk_buff ベースのコピーが残る。カスタムドライバ不要だが性能が限定的 | | **Kernel-mode Click** | アプリをカーネル内で動作させコピー排除 | カーネル環境は制約が多く fragile | | **UIO-IXGBE / DPDK / PF_RING-DNA** | NIC を完全にユーザー空間に渡す | デバイスレジスタ保護なし。IOMMU がなければ misbehaving クライアントがシステムをクラッシュさせうる | | **PacketShader/PSIOE** | カスタムドライバでプリアロケーションを採用 | 単一 NIC のみ対応。`select`/`poll` をサポートしないのでアプリ改変が必要 | | **Van Jacobson's NetChannels** | 技術的に類似(sk_buf 排除・バッファのユーザー空間マップ) | Linux 実装は既存カーネルアーキテクチャとの 100% 互換制約から性能限界に直面 | | **DAG / NetFPGA** | FPGA カスタムハードウェア | 特定ハードウェアへの依存 | **netmap の差別化**: 上記のうち「OS プリミティブと統合、特定ハードウェア非依存、安全で容易に使えること」を**同時に**満たすものがなかった。 ## 実験設定(Section 5) - **ハードウェア**: i7-870 4 コア @ 2.93 GHz(Turbo 3.2 GHz)、メモリ 1.33 GHz、デュアルポート Intel 82599(10 Gbit/s)NIC - **ソフトウェア**: FreeBSD HEAD/amd64 as of 2012-04 - **接続**: 2 台の同構成マシンを直結 - **パケットサイズ**: 64 バイト(60 B + 4 B CRC)を基本。per-packet オーバーヘッドが最も厳しいため - **計測対象**: アプリケーションコストを極小化した単純な送受信ループ(パケットジェネレーター + パケットカウンター) - **クロック可変**: 線速に達する前後の CPU 境界を特定するため 150 MHz 刻みで動作周波数を変更 - **再現性**: 全結果 2% 以内の誤差で高い再現性。信頼区間は省略 **評価指標の 2 分類**: - *Per-byte コスト*: データ移動の CPU サイクル(netmap はゼロ) - *Per-packet コスト*: NIC リングスロット更新・システムコール・メモリ割り当てなど(これが支配的) ## 実験結果(Section 5) ### 送信速度 vs クロック周波数(Figure 5) **Figure 5: 送信性能(64 バイトパケット)** ![[_attachments/atc12-final186/fig05-tx-speed-vs-clock.png]] (Figure 5. netmap 1/2/4 コアと、Linux pktgen(ピーク 4 Mpps)・FreeBSD netsend(ピーク 1.05 Mpps)との比較。netmap 1 コアは 900 MHz で線速に到達し、標準 API と比べて 1 桁以上高速。Source: Rizzo 2012, ATC '12.) - **netmap 1 コア**: 900 MHz で **14.88 Mpps**(線速)到達。60〜65 サイクル/パケット - このテストでの per-packet 作業: netmap リングスロットのバリデーション + NIC リングスロットとの同期のみ - キャッシュライン内に収まる複数ディスクリプタでキャッシュミスコストを償却 - 線速到達後はクロック上昇につれ CPU 使用率が低下(900 MHz: 100%、1.2 GHz: 80%、最高速: 55%) - **Linux/pktgen**(カーネル内特化ジェネレーター): 最高速でも ~4 Mpps - **FreeBSD/netsend**(Raw ソケット userspace): ピーク 1.05 Mpps(950 ns/pkt の生データは Figure 2 参照) ### 速度 vs パケットサイズ(Figure 6) **Figure 6: パケットサイズ別の送受信速度** ![[_attachments/atc12-final186/fig06-speed-vs-packet-size.png]] (Figure 6. 上が TX、下が RX。TX は 1/size の期待挙動。RX は 64 の倍数以外のサイズで 7.5 Mpps 前後に頭打ちになる(NIC/I/O ブリッジのリードモディファイライト問題)。Source: Rizzo 2012, ATC '12.) - **TX**: パケットサイズに反比例する 1/size の標準的挙動 - **RX の異常**: 64 の倍数でないサイズでは最高速に届かず 7.5 Mpps 前後に平坦化(Intel CPU)。原因は NIC や I/O ブリッジがキャッシュライン非整合の書き込みに対してリードモディファイライトサイクルを発行するため。CRC 除去モードに変えると "sweet spot" が 4 バイトシフトして 68, 132... に移動することで確認 ### バッチサイズの影響(Figure 7) **Figure 7: バッチサイズ vs 送信速度(1 コア、2.93 GHz)** ![[_attachments/atc12-final186/fig07-speed-vs-batch-size.png]] (Figure 7. バッチサイズ 1 では 2.45 Mpps(408 ns/pkt)。8 パケット以上で線速 14.88 Mpps に到達。poll() オーバーヘッド(~250 ns)の償却が不可欠。Source: Rizzo 2012, ATC '12.) | バッチサイズ | スループット | 1 パケットあたり | |------------|------------|---------------| | 1 | 2.45 Mpps | 408 ns | | 8 | **14.88 Mpps**(線速) | 67 ns | FreeBSD `poll()` のオーバーヘッドは約 250 ns。これを複数パケットで割ることができるかどうかが、線速到達の鍵。 ### パケット転送性能(Figure 8) **Figure 8: 転送性能(1 コア、2 つの 10G インタフェース)** ![[_attachments/atc12-final186/fig08-forwarding-performance.png]] (Figure 8. netmap ネイティブ転送は線速 14.88 Mpps。同一バイナリを标准 libpcap と netmap 互換 libpcap で動かした比較(click-fwd、openvswitch)では 4〜8 倍の改善。Source: Rizzo 2012, ATC '12.) | 構成 | Mpps | |------|------| | netmap-fwd(ゼロコピー、1.733 GHz) | **14.88**(線速) | | netmap-fwd + pcap エミュレーション | 7.50 | | click-fwd + netmap | 3.95 | | click-etherswitch + netmap | 3.10 | | **click-fwd + native pcap** | 0.49 | | openvswitch + netmap | 3.00 | | **openvswitch + native pcap** | 0.78 | | bsd-bridge(カーネル内) | 0.75 | - netmap は標準 API 比 4〜40 倍。libpcap を差し替えるだけで Click が 8 倍(0.49 → 3.95 Mpps)、OpenvSwitch が 3.8 倍(0.78 → 3.00 Mpps) ## 考察(Section 5.7) 各最適化の貢献を Figure 5/8 から分解できる: **メモリコピーの寄与**: netmap-fwd(14.88 Mpps)と netmap-fwd+pcap(7.50 Mpps)の差。コピーは有意なオーバーヘッドだが、それ単独では大幅な速度低下には至らず、7.5 Mpps という十分高い性能が pcap 互換レイヤーでも達成される。 **per-packet システムコールの寄与**: FreeBSD/netsend と Linux/pktgen の差分(pktgen はカーネル内なのでシステムコストがほぼゼロに近い)、またはバッチサイズ実験(Figure 7)から明確。バッチサイズ 1 と 8 以上の差が syscall 償却の効果。 **mbuf/sk_buf API の寄与**: pktgen(~250 ns/pkt)と netmap ジェネレーター(20〜30 ns/pkt、NIC プログラミングのみ)の差。バッファ管理の実態コストが浮き彫りになっている。 ### アプリケーション移植の実例(Section 5.8) **OpenvSwitch**: 元のユーザー空間/libpcap 転送モジュールで 70 Kpps 以下。libpcap を netmap 版に差し替えただけでは改善なし(イベントループ自体が問題)。イベントループ再構築 + 2 プロセス分割で native が 780 Kpps → netmap libpcap で **3 Mpps** へ。 **Click**: C++ アロケーターが固定サイズバッファプールより高価だった。アロケーター置き換えで native pcap: 0.40→0.495 Mpps、netmap: 1.3→**3.95 Mpps**。user-space Click はカーネル版(デバイスドライバ + sk_buf 管理のオーバーヘッドあり)を速度で上回った。 ## 強み / 弱点・課題 **強み**: - カーネル保護機構を維持しながらゼロコピーを達成(DPDK の完全バイパスとは本質的に異なる安全モデル) - 特定ハードウェアに依存しない(IOMMU 不要) - FreeBSD と Linux の両プラットフォームで動作し、ドライバ改変量が最小(500 行程度) - `select`/`poll` による既存同期プリミティブとの完全な互換性 - libpcap 互換レイヤーで既存アプリをソース無改変で高速化できる - 単一コア 900 MHz で 10 Gbit/s 線速到達という圧倒的な効率 **弱点・課題**: - misbehaving プロセスが他のプロセスの netmap リング/バッファを破壊できる(インタフェースごとにメモリ領域を分離することで解決可能で future work として言及) - 受信時、64 の倍数でないパケットサイズで RX 性能が 7.5 Mpps に平坦化する(NIC/I/O ブリッジの問題であり netmap 固有ではないが制限として現れる) - パケット処理アプリ側のボトルネック(イベントループ設計・メモリアロケーター)が libpcap 交換だけでは解消されないケースがある(Section 5.8 参照) - バーチャライゼーション環境(ハイパーバイザ内での NIC 仮想化)への応用は future work ## 関連 - [[Luigi Rizzo]] — 著者、Università di Pisa - [[netmap]] — 本論文が提案する概念のページ - [[カーネルバイパスネットワーキング]] — netmap が体現するアーキテクチャパターン - [[ゼロコピーネットワーキング]] — netmap の中核最適化手法 - [[RPS(Receive Packet Steering)]] — Linux カーネル内受信パケット分散(別レイヤーのアプローチ) - [[@1993__USENIX__The BSD Packet Filter A New Architecture for User-level Packet Capture]] — BPF (McCanne & Jacobson)。netmap の比較対象に挙がるユーザー空間パケット処理の源流 - [[@2015__yuuk.io__linux-networkstack-tuning-rfs]] — Linux ネットワークスタック RPS/RFS チューニングの実践