この記事はRed Hat Developerの Achieving high-performance, low-latency networking with XDP: Part I を、許可をうけて翻訳したものです。
::: By Paolo Abeni December 6, 2018 :::
XDP:ゼロから14 Mpps*1
過去何年にもわたり、カーネルコミュニティはさまざまなアプローチを使用して、ネットワークパフォーマンスの改善を追求してきました。いくつかの分野で改善が見られますが、アーキテクチャ関連のセキュリティ問題とその対策がほとんどの利点を打ち消してしまい、パケット処理集中型ワークロードに対する純粋なカーネルソリューションはまだバイパスソリューションよりも遅れています。すなわち、データプレーン開発キット(DPDK)に対して、10倍近い差があります。
しかし、カーネルコミュニティは(ほとんど文字通り)眠ることなく、カーネルベースのネットワーキングパフォーマンスの聖杯をXDP:eXpress Data Pathの名の下に発見しました。 XDPはRed Hat Enterprise Linux 8で利用可能で、今すぐダウンロードして実行することができます。
このテクノロジを使用すると、カーネルパケット処理にカスタム拡張BPF(eBPF)プログラムをアタッチして新しいネットワーキング機能を実装したり(既存の機能を再実装したり)、ソケットバッファ(SKB)管理やパケット単位のメモリ管理のオーバーヘッドを削減でき、より効率的な一括処理を可能にします。XDP eBPFプログラムは、パケット操作およびパケット転送のためのヘルパー関数にアクセスできるため、新しいカーネル内コードを追加する必要なしに、しかも処理速度を向上しつつ、カーネルの動作を変更および拡張する機会がほぼ無制限に提供されます。XDPをサポートする最新のドライバは、14 Mpps以上を簡単に処理できます。
この機会には興奮しますが、未知のものなので怖いですよね?この記事はあなたが最初のXDPプログラムを書くためのガイドです。ゼロから実用的な例を構築し、あなたが高速ネットワークアプリケーションを構築するための準備をします。
XDPの概要
XDPでは、eBPFプログラムをカーネル内の下位レベルのフックにアタッチすることができます。このようなフックは、SKBが現在のパケットに割り当てられる前に、ingressトラフィック処理機能(通常はNAPI poll() メソッド)内のネットワークデバイスドライバによって実装されます。
プログラムのエントリ関数には引数が1つあります。それは現在のパケットを記述するコンテキストです。プログラムはそのパケットを任意の方法で操作することができますが、eBPFベリファイアによって課される制約に従う必要があります(これについては後でもう少し詳しく説明します)。最後に、そのようなプログラムは、デバイスドライバが処理されたパケットを次にどのように扱うべきかを指定する、制御アクションを返します。例えば上位層の処理に渡すかドロップするかを、所有するデバイスドライバに指示します。
eBPFベリファイアはあなたが書くことができるコードにいくつかの制限を課し、プログラムが以下の性質をもっていることを確認する厳密なチェックを実行します。
- ループを含まない
- 有効なメモリのみにアクセスします(たとえば、パケット境界を超えないようにします)。
- 限られた数のeBPF命令を使用します。Linux 4.14カーネルでは制限は128Kです。
ベリファイア自体はカーネル内で実行され、eBPFプログラムがeBPFシステムコールを介してロードされたときに実行されます。プログラムが正常にロードされると、任意の数のデバイスのXDPフックにアタッチできます。カーネルソースは、そのような作業を単純化するためのヘルパー関数をもつユーザ空間ライブラリ(libbpf)をバンドルしています。
冒険好きな(または上級)ユーザーは、eBPFアセンブラーを使用して直接eBPFプログラムを書くこともできますが、高級言語を使用し、コンパイラーにコードをeBPFに変換させる方がはるかに簡単です。eBPFコミュニティは、そのタスクにLLVMコンパイラとC言語を選択しました。ベリファイアはコンパイルされたコードを検証するので、コンパイラによって実行される最適化は結果に影響を与える可能性があります。たとえば、定数回のループは、コンパイラによって展開されます。これにより「ループなし」制約を回避できますが、生成されるeBPF命令の数も増えます。入れ子になったループは、128Kの制限に簡単に達する可能性があります。
XDPフレーバー
すべてのネットワークデバイスドライバがXDPフックを実装しているわけではありません。そのような場合は、コアカーネルによって実装され、特定のネットワークデバイスドライバ機能に関係なく利用可能な、一般的なXDPフックにフォールバックする場合があります。このようなフックはSKB割り当て後にネットワーキングスタックの後半で行われるため、そこでのパフォーマンスはドライバベースのXDPフックよりもはるかに低くなりますが、それでもXDPを試すことができます。
Linux 4.18以降でXDPフックをサポートするネットワークデバイスドライバは次のとおりです。
- bnxt
- thunder
- i40e
- ixgbe
- mlx4
- mlx5
- nfp
- qede
- tun
- virtio_net
このリストはカーネルのバージョンによって異なる可能性があるので、実行中のカーネルであなた自身のドライバが明示的にサポートされているかチェックする価値があります。その方法については後述します。
最後に、XDP / eBPFプログラムの振る舞いは、プログラムで定義・使用される多数のマップを介してユーザースペースから制御および検査することができます。それらのマップは、libbpfヘルパーを使用してユーザースペースからアクセスや変更も可能です。
"Hello world"
XDPはプログラミング言語ではありませんが、プログラミング言語(eBPF)を使用します。プログラミング関連のチュートリアルなので「Hello world」プログラムから始めます。デバッグメッセージをXDP / eBPFプログラムで出力することはでき、後で説明しますが、もっと単純なXDPの例から始めましょう。eBPFプログラムで、処理された各パケットをカーネルのスタックに渡すだけです。
// SPDX-License-Identifier:GPL-2.0 #define KBUILD_MODNAME "xdp_dummy" #include <uapi/linux/bpf.h> #include "bpf_helpers.h" SEC("proc") int xdp_dummy(struct xdp_md *ctx) { return XDP_PASS; } char _license[] SEC("license") = "GPL";
上のコードについて詳細を見てみましょう。C言語の構文を使用し、2つの外部ヘッダーを含みます。最初のものは、ほとんどのディストリビューションではkernel-headerパッケージで提供され、XDPプログラムのリターンコード(この例ではXDP_PASS)の定義を含みます。これは、カーネルがパケットをネットワーク処理に渡すことを意味します。他の利用可能な値はXDP_DROP(ドロップする)、XDP_TX(それを受け取ったインタフェースからパケットを送り返す)、XDP_REDIRECT(他のインタフェースからパケットを送る)です。
2番目のヘッダは、Linuxカーネルの一部なのですが、ほとんどのディストリビューションでは通常パッケージ化されていません。利用可能なeBPFヘルパー関数のリストと SEC()マクロの定義が含まれています。後者は、コンパイルされたオブジェクトのフラグメントを指定したELFセクションに配置するために使用されます。そのようなセクションはeBPFローダーよって検出・解釈され、たとえば、プログラムによって定義されたマップを検出する(およびそれらへのユーザースペースアクセスを許可する)ために利用されます。
最後の行は、このプログラムに関連するライセンスを正式に指定しています。一部のeBPFヘルパー関数はGPLプログラムでしかアクセスできないので、ベリファイアはこの情報を使用して制限をおこないます。
荒削りな点
ビルドして実行しましょう。LLVM /
Clangバージョン3.7以降(この記事の執筆にはバージョン6.0が使用されました。Fedora
28のデフォルトです)古すぎない版の make
(ここでは4.2.1)が必要です。以下のmakefileを利用します:
KDIR ?= /lib/modules/$(shell uname -r)/source CLANG ?= clang LLC ?= llc ARCH := $(subst x86_64,x86,$(shell arch)) BIN := xdp_dummy.o CLANG_FLAGS = -I. -I$(KDIR)/arch/$(ARCH)/include \ -I$(KDIR)/arch/$(ARCH)/include/generated \ -I$(KDIR)/include \ -I$(KDIR)/arch/$(ARCH)/include/uapi \ -I$(KDIR)/arch/$(ARCH)/include/generated/uapi \ -I$(KDIR)/include/uapi \ -I$(KDIR)/include/generated/uapi \ -include $(KDIR)/include/linux/kconfig.h \ -I$(KDIR)/tools/testing/selftests/bpf/ \ -D__KERNEL__ -D__BPF_TRACING__ -Wno-unused-value -Wno-pointer-sign \ -D__TARGET_ARCH_$(ARCH) -Wno-compare-distinct-pointer-types \ -Wno-gnu-variable-sized-type-not-at-end \ -Wno-address-of-packed-member -Wno-tautological-compare \ -Wno-unknown-warning-option \ -O2 -emit-llvm all: $(BIN) xdp_dummy.o: xdp_dummy.c $(CLANG) $(CLANG_FLAGS) -c $< -o - | \ $(LLC) -march=bpf -mcpu=$(CPU) -filetype=obj -o $@
注意すること:
プログラムはカーネルヘッダを使用するので、アーキテクチャ固有のヘッダ依存関係に対応する必要があります。上記のmakefileを使って任意のディストリビューションでmake
を呼び出すと、次のようになり、期待外れな結果となる場合があります。
clang -nostdinc -I. \ [...明快さのために長い引数リストは省略...] -O2 -emit-llvm -c xdp_dummy.c -o - | \ llc -march=bpf -mcpu= -filetype=obj -o xdp_dummy.o xdp_dummy.c:5:10: fatal error: 'bpf_helpers.h' file not found #include "bpf_helpers.h" ^~~~~~~~~~~~~~~ 1 error generated.
ここで必要なbpf_helper.h
を含む完全なカーネルソースが含まれているディストリビューションのパッケージはほとんどありません。現在の解決策は、残念なことに、完全なLinuxソースをダウンロードし、あなたのローカルディスクのどこかに解凍し、そして以下のようにmakeを起動することです:
KDIR= make
これでeBPF / XDPプログラムは完成しましたが、実行するにはカーネル内にロードする必要があります。それほど簡単ではない例では、ロードは通常制御用のユーザ空間プログラムによって行われます。 これは一部のマップを介してXDPプログラムを監視/対話します。簡単のために、代わりにiprouteを使います。
ip link set dev <net device name> xdp object xdp_dummy.o
上記のコードは私達のXDPプログラムを指定されたネットワークデバイスに接続し、デバイスフックを使用しようと試み、そうでなければ一般的なものにフォールバックします。勇敢でサポートされているデバイスドライバを持っている人は、ドライバ固有のフックの使用を強制するようxdp
をxdpdrv
に置き換えることもできます。
xdpdrv
が利用できない場合、アタッチは失敗します。
次回予告
今回作成したXDPプログラムを使用すると、XDPをサポートする最新のドライバで14 Mppsを容易に処理できるため、(イーサネットケーブルを抜いて全てをフィルタしてしまっているのでなければ)これまでにない速度でのパケットフィルタリングを経験できます。しかし、もうすこし意味があること、特定のパケットをドロップしたり、XDPプログラムのアクティビティに関する統計を収集するなどに興味があることでしょう。この連載の次回の記事では、パケットの解析、デバッグ、およびマップの使用法を紹介することによって、それほど簡単ではない例を扱います。
*1:訳注: 10GbEでの最大パケットレート