ソケットバッファとゼロコピー送受信
転送が遅い原因がアプリではなくカーネル内のコピーとバッファ設計にあると見抜ける。BDPに基づくバッファ設計とsendfile/spliceの原理を内部から押さえられる。
- 1.ソケットの送受信バッファはカーネル内のキューで、送信バッファはACK確認まで再送用にデータを保持し、受信バッファの空きがそのまま広告ウィンドウ(rwnd)になる。
- 2.高速・長RTT路で帯域を使い切るにはバッファをBDP(帯域×RTT)以上に取る必要があり、小さすぎるとrwndが頭打ちでスループットが落ち、大きすぎるとbufferbloatで遅延が膨らむ。
- 3.sendfile/spliceはデータをユーザー空間へ往復させずカーネル内で完結させ、コピー回数とコンテキストスイッチを削減してCPU律速のスループットを引き上げる。
ソケットバッファはカーネル内のキューである
アプリが send() を呼んでもデータが即座に回線へ出るわけではありません。データはまずカーネル内の 送信ソケットバッファ(send buffer) にコピーされ、send() はそこで返ります。実際の送出はTCPの輻輳制御・フロー制御に従ってカーネルが行います。受信側も対称で、NICが受け取ったデータは 受信ソケットバッファ(receive buffer) に積まれ、アプリが recv() で読み出すまでそこに滞留します。
この2つのバッファはソケットごとに独立して存在し、アプリの送出ペースと回線の送出ペースを decouple(分離) する緩衝材です。ソケットそのものの概念は ソケットとポート番号 を前提にします。
- 送信バッファ: アプリの書き込み速度 > 回線速度のときに溢れを吸収する。さらに 未ACKデータの再送用コピーを保持 する役割も持つ。
- 受信バッファ: 回線の到着速度 > アプリの読み出し速度のときに溢れを吸収する。空き容量がそのまま広告ウィンドウになる。
TCPは信頼転送のため、送ったデータをACKされるまで捨てられません。送信バッファ内のデータは回線へ送出した後も、相手のACKが届いて初めて解放されます。つまり送信バッファは「これから送る分」と「送ったがACK待ちの分(in-flight)」の両方を抱えます。だからバッファが小さいと、未ACK分で埋まった瞬間にアプリの send() がブロックします。
受信バッファと広告ウィンドウは直結している
ここが原理の核心です。受信バッファの 空き容量 が、ACKで相手に通知される広告ウィンドウ(rwnd)そのものになります。アプリが recv() で読み出すと空きが増え、rwndが拡大します。逆にアプリの処理が詰まって読み出しが遅れると、受信バッファが埋まり、rwndが縮み、ついには0(ゼロウィンドウ)になって送信側が止まります。
広告ウィンドウ(rwnd) ≒ 受信バッファサイズ − 未読データ量
したがって 受信バッファサイズの上限が、その方向で取れるrwndの上限 を決めます。送信側がいくら送れても、受信バッファが小さければrwndで頭打ちになる。フロー制御の全体像とゼロウィンドウの挙動は TCPのフロー制御とウィンドウスケーリング を参照してください。
BDPに基づくバッファサイズ設計
「ではバッファをいくつにすべきか」を決める唯一の正しい基準が 帯域遅延積(BDP, Bandwidth-Delay Product) です。
BDP = 帯域 × RTT
例: 1Gbps × 80ms = 約10MB
BDPは「ACKが返ってくる前に回線上を飛んでいられるデータ量」、すなわち回線を埋め尽くすために必要なin-flight量です。回線を100%使い切るには、送信側は最低でもBDP分を未ACKのまま流し続けられなければなりません。これは送信バッファ・受信バッファ・rwnd・cwndのいずれもがBDP以上である必要を意味します。帯域とRTTがスループットを規定する関係は 帯域・レイテンシ・スループット の通りです。
| バッファサイズ | rwndへの影響 | スループット | 遅延への影響 |
|---|---|---|---|
| BDP未満 | rwndがBDP未満で頭打ち | 回線を使い切れず低下 | 小さい |
| BDP前後 | 回線をちょうど満たすrwnd | 理論帯域に到達 | 最小限 |
| BDPより過大 | rwndは十分だが余剰滞留 | 頭打ち(これ以上伸びない) | bufferbloatで増大 |
小さすぎる害は明白です。BDPが10MBの経路でバッファが64KBなら、64KB送るたびにACKを待って停止し、回線の1%も使えません。一方、大きすぎても害がある のがポイントです。送信側がBDPを大きく超える量を流し込むと、経路上のキューに長く滞留し、遅延(とジッタ)が膨らむ。これが バッファブロートと AQM(CoDel・FQ-CoDel・PIE) で扱うbufferbloatです。バッファは「BDPを満たす最小限」が理想で、無限に大きくすればよいものではありません。
現代のLinuxは tcp_rmem / tcp_wmem の 最小,初期,最大 3値と自動調整で、観測したRTTと輻輳状況からバッファを動的に伸縮させます。tcp_moderate_rcvbuf が有効なら受信バッファは必要に応じBDP相当まで自動拡大します。注意点は、setsockopt で SO_RCVBUF/SO_SNDBUF を 手動固定すると自動調整が無効化 されること。固定値が小さいと高BDP路で取りこぼし、固定したことを忘れた将来の障害になります。明確な理由がなければ自動調整に任せるのが定石です。
ゼロコピーがスループットを上げる原理
バッファサイズが「窓を開ける」話なら、ゼロコピーは「同じCPUでより多く流す」話です。両者は別の軸の最適化です。
ファイルをそのままソケットへ送る典型的な処理 read() + send() を考えます。カーネルはディスクのデータを一度カーネル空間のページキャッシュに読み込み、それを ユーザー空間のバッファへコピー、アプリが send() を呼ぶとそれを 再びカーネルの送信バッファへコピー します。データはユーザー空間を素通りするだけなのに、CPUは無駄に2回コピーし、read/send で都度ユーザー・カーネル間の コンテキストスイッチ を起こします。
read()+send() の流れ(簡略):
ディスク → ページキャッシュ → ユーザーバッファ → ソケット送信バッファ → NIC
(DMA) (CPUコピー) (CPUコピー) (DMA)
コンテキストスイッチ: read で2回, send で2回 = 計4回
ゼロコピー は、このユーザー空間への往復を丸ごと省きます。
sendfile と splice
sendfile() は「ファイルディスクリプタからソケットへ」をカーネル内だけで完結させるシステムコールです。データはユーザー空間に一切現れません。
// out_fd(ソケット)へ in_fd(ファイル)の中身を直接転送
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
さらにNICがスキャッタギャザDMA(SG-DMA)に対応していれば、ページキャッシュからソケット送信バッファへのCPUコピーすら省け、NICがページキャッシュを直接読み出します。splice() はより汎用で、パイプを介して2つのfd間をカーネル内で繋ぎます。いずれも本質は「データに触れずに参照(ページ)だけを受け渡す」ことです。
| 方式 | CPUコピー回数 | コンテキストスイッチ | ユーザー空間を経由 |
|---|---|---|---|
| read()+write() | 2回(往+復) | 4回 | する |
| sendfile() | 1回(SG-DMAなら0回) | 2回 | しない |
| splice() / SG-DMA | 0回 | 2回 | しない |
効果は CPUとメモリ帯域の節約 です。10Gbps級の転送ではコピー自体がCPUとメモリバスを飽和させ、ここがスループットの上限になります。コピーを削れば同じCPUでより多くの接続・帯域をさばける。Webサーバーの静的ファイル配信やプロキシ、tee/cat 的なストリーミングが典型的な適用先です。
ゼロコピーはデータに触れないからこそ速い。逆に言えば 送信前にデータを変換する処理とは両立しません。TLS暗号化、圧縮、アプリ層での書き換えが入るとカーネルはバイト列を加工できず、ユーザー空間でのコピーが避けられません(カーネルTLSなどの例外を除く)。また sendfile の入力は基本的に通常ファイル(mmap可能なもの)に限られ、ソケットからソケットへは splice を使います。「無条件に速くなる魔法」ではなく、素通しできるデータにだけ効く 最適化です。
まとめ
ソケットバッファはアプリと回線のペース差を吸収するカーネル内キューで、送信バッファは未ACKデータの再送用保持を兼ね、受信バッファの空きはそのまま広告ウィンドウになります。だから高BDP路で帯域を使い切るにはバッファをBDP以上に取る必要があり、小さすぎればrwndで頭打ち、大きすぎればbufferbloatで遅延が膨らむ——最適は「BDPを満たす最小限」で、通常は自動調整に委ねます。そしてゼロコピー(sendfile/splice)はユーザー空間への往復コピーとコンテキストスイッチを削り、CPU律速のスループットを引き上げますが、データを加工しない素通し転送にのみ効きます。バッファ設計が「窓の広さ」を、ゼロコピーが「CPUあたりの送出量」を決める、と分けて捉えるのが要点です。
「受信バッファとrwndの関係」→ 空き容量がそのまま広告ウィンドウ、読み出し遅延でゼロウィンドウ。「バッファはいくつにすべきか」→ BDP(帯域×RTT)以上。小さいと頭打ち、大きいとbufferbloat。「sendfileはなぜ速いか」→ ユーザー空間への2回のコピーとコンテキストスイッチを省く、SG-DMAならCPUコピー0回。「ゼロコピーが使えない場面」→ TLSや圧縮などデータ加工が入るとき、SO_RCVBUF手動固定で自動調整が無効化される点も注意。
ネットワーク Article
ソケットバッファとゼロコピー送受信を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ソケット
比較で見る軸
難易度: advanced / カテゴリ: ネットワーク / タグ数: 5
導入後に効く点
高速・長RTT路で帯域を使い切るにはバッファをBDP(帯域×RTT)以上に取る必要があり、小さすぎるとrwndが頭打ちでスループットが落ち、大きすぎるとbufferbloatで遅延が膨らむ。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- ネットワーク
- タグ数
- 5
判断チェックリスト
- 自社の用途が「ソケット / ゼロコピー」に近いか確認する。
- 強みである「ソケットの送受信バッファはカーネル内のキューで、送信バッファはACK確認まで再送用にデータを保持し、受信バッファの空きがそのまま広告ウィンドウ(rwnd)になる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。