はじめに
この記事はBBSakuraNetworksアドベントカレンダー2025の24日目の記事です。
皆さんこんにちは。takemioIOです。 今年も会社のアドベントカレンダーに参加しています。
今回取り上げる内容は、所属企業とは無関係に個人で開発した XDPerf という高性能トラフィックジェネレーターについてです。所属に関係しないため、自分のブログで書いてお送りしようと思います。
このツールの説明
XDPerf は XDP の仕組みを利用した高性能なネットワークトラフィック生成ツールです。 send モードと recv モードの両方に対応し、ネットワークスループットやパケットレートの測定に活用できます。Go で単一バイナリとして実装されているため依存関係が少なく、容易に配布できます。
さらに、XDPerf はWASM ベースのプラグインを備え、任意のパケットを生成できます。この仕組みにより、Python ベースのツールでありがちな依存関係の問題を避けられます。
機能面から見ると、XDPerf は TRex と iperf の中間を狙ったような位置付けです。主な利点は次のとおりです。
- TRex のような DPDK ほどの性能は出ない可能性がありますが、DPDK によくあるような Network Interface の占有が発生せず、ARP の解決などを Linux のレイヤで確認できます。
- iperf のように状態遷移を持つTCP や輻輳制御などはありませんが、手軽に高い性能で負荷をかけることができ、(自作プラグイン経由で)任意のパケット送信することが利用できます。
- TRex のような Python SDK は不要であり、WASM により任意のパケット生成ロジックを記述できるため、古い Python やライブラリを強制されることがありません。
使い方
次のワンライナーでインストールできます。
curl -fsSL https://raw.githubusercontent.com/takehaya/xdperf/main/scripts/install_xdperf.sh | sudo bash
任意パケットの PPS を手軽に測定する例は次のとおりです。 これは特定インタフェースから 10 万パケットを 4 並列で送信し、設定パラメーターに応じた UDP パケットを送信する例です。
sudo xdperf run --device eth0 --count 100k --parallelism 4 --plugin simpleudp.go \
--cfg '{"dst_port": 10001, "src_ip": "192.168.1.1", "dst_ip": "192.168.1.2"}'
プラグインを差し替えることで動作を拡張できます。 次は imix パケットを送信する例です。
sudo xdperf run --device eth0 --count 100k --parallelism 4 --plugin imixudp.go \
--cfg '{"dst_port": 10001, "src_ip": "192.168.1.1", "dst_ip": "192.168.1.2", "payload_size": 64}'
技術的背景
BPF_F_TEST_XDP_LIVE_FRAMES
XDPerfではXDPのBPF_PROG_RUNとBPF_F_TEST_XDP_LIVE_FRAMESを活用しています。BPF_F_TEST_XDP_LIVE_FRAMESを使用すると、高速パケット処理基盤である eBPF/XDP を通じてパケット送信のみを高速に実行できます。
利用例は次のようになります。
xdpmd := &sys.XdpMd{
DataEnd: uint32(len(pkt)),
IngressIfindex: uint32(iface.Index),
}
ret, err := objs.XdpProgTx.Run(&ebpf.RunOptions{
Data: pkt,
Repeat: uint32(repeat),
Flags: unix.BPF_F_TEST_XDP_LIVE_FRAMES,
Context: xdpmd,
BatchSize: uint32(batchSize),
})
実際にGo言語でやってみたい場合は、cilium/ebpfの ebpf/examples/xdp_live_frame が参考になります。
なお、この例は cilium/ebpf において BatchSize パラメーターが未サポートだった際、筆者が PR を提出したことを契機として取り込まれたものです。 github.com
また、BPF_F_TEST_XDP_LIVE_FRAMES の性能については開発者である tohojo 氏のブログで詳しく紹介されています。 1 コアで 8 Mpps、5 コアで 40 Mpps 程度の性能を発揮できる可能性があるとされています。
より詳しい技術情報は Linux カーネルのドキュメントを参照してください。 docs.kernel.org
なお、XDPerf に付属のprobeコマンドを使うと対象インタフェースがXDPとともにBPF_F_TEST_XDP_LIVE_FRAMESをサポートしているか確認できます。
wazero
XDPerf は wazero という Go 製の WASM ランタイムを利用しています。 github.com
パケット生成部分をプラグインとして切り出しており、これを WASM バイナリとして実行するためです。
XDPerfで使うWASM プラグインは次の 3 つの関数を必ず実装します。
//go:wasmexport plugin_init func plugin_init(inputPtr, inputLen, outputPtr, outputMaxLen uint32) int32 { ... } //go:wasmexport plugin_process func plugin_process(inputPtr, inputLen, outputPtr, outputMaxLen uint32) int32 { ... } //go:wasmexport plugin_cleanup func plugin_cleanup(inputPtr, inputLen, outputPtr, outputMaxLen uint32) int32 { ... }
このうち plugin_process においてベースパケットと動的に変化させる値を定義し、XDPerf 本体に返すことで処理を実現します。 そのため、任意の言語でパケット生成ロジックを記述できます。
XDPerfのアイディア
BPF_F_TEST_XDP_LIVE_FRAMES の制約に対応する仕組み
BPF_F_TEST_XDP_LIVE_FRAMES は単純に利用すると、登録した 1 種類のパケットを連続して送信するだけであり、多様なパケットを送出する用途には適しません。マルチフローのパケットを生成できないため実用性が限られます。
これに対して XDPerf では eBPF Map を利用し、送信したいパケットを動的に切り替える方式を採用しています。PerCPU Map を利用することで CPU 間の競合を避けつつ高速に処理できます。ただし、この方式では送信順序が前後する可能性があるため注意が必要です。
任意のパケットを投げる仕組み
先述のとおり、ベースパケットと変化させる変数を組み合わせる仕組みをテンプレートジェネレーターと呼んでいます。
例えば、これはsimpleudp.go から抜粋したものです。呼んでみるとわかりますがベースとなるパケットがあり、そこに対して変数が定義されています。ソースポートを動的に尚且つシーケンシャルに変更できるようにしてたり、パケットの長さをシーケンシャルに変更可能にしていたりします。これによって1つのベースのパケットを動的に変えながらパケットを送ることができるようになってます。
res := guest.GeneratorProcessResponse{
TemplateType: guest.GeneratorTemplateTypeVariable,
VariablePacketTemplate: guest.PacketVariantSet{
Variants: []guest.PacketVariant{
{
Base: guest.BasePacket{
Data: packetBytes,
Length: maxLen,
},
Params: []guest.VariableParams{
{
ByteStart: srcPortOffset,
ByteSize: 2,
ByteRange: guest.TemplateRange{Start: 1024, End: 1124},
PatternType: guest.ValuePatternTypeSequential,
},
{
ByteStart: guest.ByteStartPacketLength,
ByteSize: 0,
ByteRange: guest.TemplateRange{Start: 64, End: 84},
PatternType: guest.ValuePatternTypeSequential,
},
},
},
},
Pattern: guest.VariantSelectionModeSequential,
},
}
これは単一パケットだけではなく、複数のパケットを選択したときはそれらの比率も定義可能です。
例えばこれは imixudp.go の例です。3種類のパケットをウェイトを決めてMixしています。
// create response // imix pattern with 3 variants // Variant A: Short packets (64-84 bytes), SrcPort 1024-1124, Weight=60% // Variant B: Mid packets (500-600 bytes), SrcPort 2000-2100, Weight=34% // Variant C: Large Packets with varying source IP (4 bytes, 1500 bytes), Weight=6% // Total Weight = 100% // can be overridden by req.IMIXRatio // hint: https://github.com/cisco-system-traffic-generator/trex-core/blob/master/scripts/stl/imix.py res := guest.GeneratorProcessResponse{ TemplateType: guest.GeneratorTemplateTypeVariable, VariablePacketTemplate: guest.PacketVariantSet{ Variants: []guest.PacketVariant{ // Variant A: Short packets (64-84 bytes), SrcPort 1024-1124, Weight=60% { Base: guest.BasePacket{ Data: packetBytes, Length: maxLen, }, Params: []guest.VariableParams{ { ByteStart: srcPortOffset, ByteSize: 2, ByteRange: guest.TemplateRange{Start: 1024, End: 1124}, PatternType: guest.ValuePatternTypeSequential, }, { ByteStart: guest.ByteStartPacketLength, ByteSize: 0, ByteRange: guest.TemplateRange{Start: 64, End: 84}, // Short: 64-84 bytes PatternType: guest.ValuePatternTypeSequential, }, }, Weight: uint32(req.IMIXRatio[0]), }, // Variant B: Mid packets (500-600 bytes), SrcPort 2000-2100, Weight=34% { Base: guest.BasePacket{ Data: packetBytes, Length: maxLen, }, Params: []guest.VariableParams{ { ByteStart: srcPortOffset, ByteSize: 2, ByteRange: guest.TemplateRange{Start: 2000, End: 2100}, PatternType: guest.ValuePatternTypeSequential, }, { ByteStart: guest.ByteStartPacketLength, ByteSize: 0, ByteRange: guest.TemplateRange{Start: 500, End: 600}, PatternType: guest.ValuePatternTypeSequential, }, }, Weight: uint32(req.IMIXRatio[1]), }, // Variant C: Varying source IP (4 bytes, 1500 bytes), Weight=6% { Base: guest.BasePacket{ Data: packetBytes, Length: maxLen, }, Params: []guest.VariableParams{ { ByteStart: v4srcIPOffset, ByteSize: 4, ByteRange: guest.TemplateRange{ // 192.168.1.1 - 192.168.1.254 Start: uint64(IPv4ToUint32(net.ParseIP("192.168.1.1"))), End: uint64(IPv4ToUint32(net.ParseIP("192.168.1.254"))), }, PatternType: guest.ValuePatternTypeSequential, }, { ByteStart: guest.ByteStartPacketLength, ByteSize: 0, ByteRange: guest.TemplateRange{Start: 1500, End: 1500}, PatternType: guest.ValuePatternTypeSequential, }, }, Weight: uint32(req.IMIXRatio[2]), }, }, // VariantSelectionModeMixed: weighted selection (A 60%, B 34%, C 6%) Pattern: guest.VariantSelectionModeMixed, }, }
このエンジンで使えるパラメーターに関しては、詳しくはこちらをみてください。 github.com
終わりに
今後の展望
実はこのツールの最適化はまだまだできそうな感じです。 自分がvirtio_netで実験した限りでは50Mppsぐらいまでは出すことができましたが、memcpyのオーバーヘッドがかなりあることも色々調査をしてわかりました。 詳しくはここに書いています。 github.com
最適化方法としては、例えばパケットの割り当てアルゴリズムを事前生成ではなくXDPのレイヤで行うことでメモリ使用量を小さくできると考えています。また差分更新にすることでメモリコピーを小さくできるのでそこにも高速化の余地があります。また、sched_ext を使えば、SMP 環境における CPU 割り当てをより最大限に引き上げることができ、さらなる 性能Enhance も可能だと考えています。
また機能性やPluginの充実も狙っていきたいと思っています。もしもこの様なプラグインが欲しいなど、機能リクエストがありましたら自分までお問い合わせください。開発検討をしようと思います。良さげな機能の PR も歓迎します。
とりあえずSRv6やGTP-Uなどが投げれるようにしたいなと思ったりしています。 あとは過去に作った(企画した)拙作の xk6-gtp, xk6-diameterとか....GoodPutはわからないけど、そういうのが移植できる余地があってもいいかも知れません。
余談
このツールは eBPF Summit に併設された Hackathon をきっかけに、妻とともに開発を進めたものです。
アイデアやデザイン、フィジビリティ確認は筆者が行いましたが、この記事で紹介したテンプレートジェネレーターの実装などは妻の大きな貢献によるものでした、この場を借りてコントリビューションに感謝します。
また、XDPerfをHackathonの為に宣伝した際にリポジトリにスターをつけてくれた方にもこの場を借りて御礼申し上げます。
なお、Hackathonについては残念ながら賞は取れずでした。とほほ....ぜひまた機会があればHackathonに作品を出したいなと思っています。