インフラ・DevOps
SOCKMAP - 未来のTCPスプライシング
SOCKMAP - TCP splicing of the future (blog.cloudflare.com)
要約
Cloudflareのブログ記事は、リバースプロキシにおけるデータ転送の効率を劇的に向上させるLinuxカーネルのSOCKMAPインフラストラクチャについて紹介しています。SOCKMAPはeBPFを活用し、TCPソケット間でデータをコピーすることなく直接転送することを可能にし、ユーザー空間プロセスの負荷やシステムコール、データコピーのコストを削減します。これにより、従来のTCPスプライシング手法が抱える課題を解決し、データ量の多いアプリケーションのアーキテクチャに革新をもたらす可能性を秘めています。
全文翻訳
SOCKMAP - 未来のTCPスプライシング
2019-02-18
マレク・マイコフスキ
7分読了
最近、私たちはリバースプロキシの聖杯であるTCPソケットスプライシングAPIを発見しました。ご存知の通り、私たちはリバースプロキシサービスのグローバルネットワークを運営しているため、これは私たちの注目を集めました。適切なTCPソケットスプライシングは、ユーザー空間プロセスの負荷を軽減し、より効率的なデータ転送を可能にします。私たちは、LinuxカーネルのSOCKMAPインフラストラクチャをこの目的のために再利用できることに気づきました。SOCKMAPは非常に有望なAPIであり、ソフトウェアプロキシのようなデータ量の多いアプリケーションのアーキテクチャに大きな変革をもたらす可能性があります。
Image by Mustad Marine public domain
しかし、少し巻き戻しましょう。L7プロキシの産みの苦しみ
ユーザー空間から大量のデータを転送することは非効率です。Linuxには、この問題に対処することを目的としたいくつかの特殊なシステムコールが用意されています。例えば、sendfile(2)システムコール(Linusは好まない)は、ディスクからソケットへ大量のファイルを転送する速度を上げるために使用できます。次に、従来のプロキシが2つのTCPソケット間でデータを転送するために使用するsplice(2)があります。最後に、vmspliceはメモリバッファをコピーなしでパイプに貼り付けるために使用できますが、正しく使用するのが非常に難しいです。
残念ながら、sendfile、splice、vmspliceは非常に特殊で同期しており、問題の一部しか解決していません。つまり、データをユーザー空間にコピーするのを避けるだけです。他の効率性の問題は未解決のままです。
between avoid user-space memory zero-copy
sendfile disk file --> socket yes no
splice pipe <--> socket yes yes?
vmsplice memory region --> pipe no yes
大量のデータを転送するプロセスは、3つの問題に直面します。
システムコールコスト: 転送されるパケットごとに複数のシステムコールを行うのはコストがかかります。
ウェイクアップ遅延: ユーザー空間プロセスは、データを転送するために頻繁にウェイクアップされる必要があります。スケジューラによっては、これによりテールレイテンシが低下する可能性があります。
コピーコスト: データがカーネルからユーザー空間へ、そしてすぐにカーネルへコピーされるのは無料ではなく、積み重なって測定可能なコストとなります。
多くの試み
TCPソケット間のデータ転送は一般的な慣行です。これは次のような場合に必要です。
Squidのような透過型フォワードHTTPプロキシ。
VarnishやNGINXのようなリバースキャッシュHTTPプロキシ。
HAProxy、Pen、Relaydのようなロードバランサー。
長年にわたり、Linux上でTCPソケット間の単純なデータ転送のコストを削減するための多くの試みがなされてきました。この問題は一般的に「TCPスプライシング」、「L7スプライシング」、または「ソケットスプライシング」と呼ばれています。
TCPスプライシングの一般的な方法を比較してみましょう。問題を単純化するために、リッチなレイヤー7 TCPプロキシを書く代わりに、トリビアルなTCPエコーサーバーを書きます。
これは冗談ではありません。エコーサーバーはTCPソケットスプライシングをうまく説明できます。ご存知の通り、「エコー」は基本的にソケットを…それ自身とスプライスします!
素朴な方法: read-writeループ
```
while data:
data = read(sd, 4096)
writeall(sd, data)
```
これ以上簡単なものはありません。ブロッキングソケットでは、これは完全に有効なプログラムであり、問題なく動作します。完全なコードはこちらに用意しました。
Splice: 特殊なシステムコール
Linuxには素晴らしいsplice(2)システムコールがあります。これは、ソケット上のTCPバッファとパイプ上のバッファの間でデータを移動するようにカーネルに指示できます。データはカーネル側のバッファ内に残ります。これにより、ユーザー空間とカーネル空間の間でデータを無駄にコピーする問題が解決されます。SPLICE_F_MOVEフラグを使用すると、カーネルはデータを全くコピーせずに済む場合があります!
splice()を使用するプログラムは次のようになります。
```
pipe_rd, pipe_wr = pipe()
fcnl(pipe_rd, F_SETPIPE_SZ, 4096);
while n:
n = splice(sd, pipe_wr, 4096)
splice(pipe_rd, sd, n)
```
ユーザー空間プログラムをウェイクアップし、任意のデータを転送するために2つのシステムコールを実行する必要がありますが、少なくともすべてのコピーを避けることができます。完全なソース。
io_submit: Linux AIO APIの使用
以前のio_submit()に関するブログ記事で、私たちはネットワークソケットでAIOインターフェースを使用することを提案しました。詳細についてはブログ記事を参照してください。しかし、ここでは、単一のシステムコールだけでエコーサーバーループが実装されたプログラムを示します。
Image by jrsnchzhrs By-Nd 2.0
SOCKMAP: 究極の武器
近年、LinuxカーネルはeBPF仮想マシンを導入しました。これにより、ユーザー空間プログラムは、パケットフィルタリングからポリシー施行まで、数十のユースケースで特殊な非チューリング完全バイトコードをカーネルコンテキストで実行できます。
カーネル4.14以降、Linuxはソケットスプライシングに使用できる新しいeBPFメカニズムであるSOCKMAPを取得しました。これはCilium.ioのJohn Fastabendによって作成され、StrparserインターフェースをeBPFプログラムに公開しています。Ciliumはレイヤー7のポリシー施行にSOCKMAPを使用しており、それが使用するすべてのロジックはeBPFプログラムに埋め込まれています。このAPIは十分に文書化されておらず、root権限を必要とし、私たちの経験では少しバグがあります。しかし、非常に有望です。
詳細はこちら:
LPC2018 - Combining kTLS and BPF for Introspection and Policy Enforcement Paper Video SlidesOriginal SOCKMAP commit
SOCKMAPの使い方は次のとおりです。
SOCKMAP、または具体的には「BPF_MAP_TYPE_SOCKMAP」は、eBPFマップの一種です。このマップは「配列」であり、インデックスは整数です。これはすべてかなり標準的です。魔法はマップの値にあります。これらはTCPソケットディスクリプタである必要があります。
このマップは非常に特殊で、2つのeBPFプログラムがアタッチされています。その通り、eBPFプログラムは通常のようにソケット、cgroup、ネットワークインターフェースにアタッチされるのではなく、マップにアタッチされます。ユーザープログラムでSOCKMAPを設定する方法は次のとおりです。
```
sock_map = bpf_create_map(BPF_MAP_TYPE_SOCKMAP, sizeof(int), sizeof(int), 2, 0)
prog_parser = bpf_load_program(BPF_PROG_TYPE_SK_SKB, ...)
prog_verdict = bpf_load_program(BPF_PROG_TYPE_SK_SKB, ...)
bpf_prog_attach(prog_parser, sock_map, BPF_SK_SKB_STREAM_PARSER)
bpf_prog_attach(prog_verdict, sock_map, BPF_SK_SKB_STREAM_VERDICT)
```
Ta-da!この時点で、parserとverdictの2つのeBPFプログラムがアタッチされたsock_map eBPFマップが確立されました。次のステップは、このマップにTCPソケットディスクリプタを追加することです。これ以上簡単なことはありません。
```
int idx = 0;
int val = sd;
bpf_map_update_elem(sock_map, &idx, &val, BPF_ANY);
```
この時点で魔法が起こります。これ以降、ソケットsdがパケットを受信するたびに、prog_parserとprog_verdictが呼び出されます。それらのセマンティクスはstrparser.txtと導入的なSOCKMAPコミットに記述されています。簡単にするために、私たちのトリビアルなエコーサーバーは最小限のスタブのみを必要とします。これがeBPFコードです。
```
SEC("prog_parser")
int _prog_parser(struct __sk_buff *skb) {
return skb->len;
}
SEC("prog_verdict")
int _prog_verdict(struct __sk_buff *skb) {
uint32_t idx = 0;
return bpf_sk_redirect_map(skb, &sock_map, idx, 0);
}
```
補足: このテストプログラムのために、私は最小限のeBPFローダーを作成しました。これは依存関係がなく(bcc、libelf、libbpfもなし)、基本的な再配置(上記のsock_mapシンボルの解決など)を行うことができます。コードを参照してください。
bpf_sk_redirect_mapへの呼び出しがすべての作業を行っています。これはカーネルに次のように指示します。受信したパケットを、あるソケットの受信キューから、sock_mapのインデックス0にあるソケットの送信キューにリダイレクトしてください。私たちの場合、これらは同じソケットです!ここで、エコーサーバーが実行すべきことを、純粋にeBPFで実現しました。
この技術には複数の利点があります。第一に、データがユーザー空間にコピーされることはありません。第二に、ユーザー空間プログラムをウェイクアップする必要がありません。すべての操作はカーネル内で行われます。かなりクールですよね?
ソケットが閉じられるまでユーザー空間プログラムを待機させるためのコードがもう一つ必要です。これは古き良きpoll(2)で最もよく行われます。
```
/* Wait for the socket to close. Let SOCKMAP do the magic. */
struct pollfd fds[1] = { {.fd = sd, .events = POLLRDHUP}, };
poll(fds, 1, -1);
```
完全なコード。
ベンチマーク
この段階で、4つのシンプルなTCPエコーサーバーを紹介しました。
素朴なread-writeループ
splice
io_submit
SOCKMAP
要約すると、測定しているのは以下の3つのコストです。
システムコールコスト
ウェイクアップ遅延(主にテールレイテンシとして現れる)
データコピーのコスト
理論的には、SOCKMAPが他のすべてを上回るはずです。
syscall cost waking up userspace copying cost
read write loop2 syscalls yes 2 copies
splice 2 syscalls yes 0 copy (?)
io_submit 1 syscall yes 2 copies
SOCKMAP none no 0 copies
数値を見せてください
これがその部分です。