
近年インターネットを流れるネットワークトラフィックは急激に増加しており、さらに低遅延も求められます。サーバ用途としてLinuxが一般的に用いられますが、カーネルのネットワークスタックは10GbEレベルに増加するワイヤースピードに対応することが難しくなっています。専用のネットワーク機器を用いればパフォーマンスを期待できますが、インフラが高価になってしまうためやはり汎用OS(Linux)、汎用マシンで高スループットを出したいという需要があります。
これを実現するためにカーネルバイパスという技術が提案され、DPDKもカーネルバイパス技術を使ったソリューションの1つです。ざっくり言うとData Plane Development Kit (DPDK) とはC言語で書かれた、パケットを高速に処理するためのオープンソースライブラリです。LinuxやFreeBSDで使え、x86だけでなくARMでも動作します。
この記事ではDPDKを使うことでどのようにパケットが処理され、なぜ高速になるのかということに焦点を当ててお話しします。
Contents
1. Linuxカーネルのネットワークスタックは重たい
DPDKについて説明する前に、そもそもカーネルによるパケット処理はどういう仕組みなんでしょうか?図を使って処理の流れを見ていきます。

- NICがパケットを受信
パケットを受信したNICは自身が持つメモリ(キュー)にパケットを保存する。 - ハードウェア割り込み
NICはパケットを受信したことをホストに知らせるためCPUに対しハードウェア割り込みを発生させる。割り込みが入ったCPUはNICのキューに保存されているパケットをカーネルが管理するリングバッファ(ホストのメモリ)にコピーする。実際にはパケットのデータはCPUを通らず、DMAによってNICから直接カーネルメモリに保存され、そのポインタがカーネルに渡されると思う。 - プロトコルスタック処理
カーネルはリングバッファからパケットを取り出しIP, TCPなどのプロトコルスタックを走らせる。 - アプリケーション処理
プロトコル処理が施されたヘッダやペイロードのデータがソケットに書き込まれ、ユーザアプリケーションが受け取る。
1つ1つの処理は特別重たいわけではありませんが、新しいパケットが来るたびにハードウェア割り込み処理とソケットを呼び出すシステムコールによるコンテキストスイッチが発生します。ラインスピードが大きく頻繁に割り込み処理が発生するとCPUは他の仕事を実行する余裕がなくなっていきます。
また、Linuxカーネルはパケットを処理する際 sk_buff と呼ばれる構造体を割り当てます。sk_buff 構造体は各パケットに対し割り当てられ、パケットが処理されてデータがユーザスペースに渡ると解放されます。この sk_buff 構造体の確保、パケットの処理、sk_buff 構造体の解放、という命令にCPUとメモリ間で多くのバスサイクルを割きます。
sk_buff 構造体には他の問題もあります。仕方のないことですが、汎用性を高めるべく様々なネットワークプロトコルに対応するように作られているため構造体が非常に大きいのです。当然この複雑な構造体の処理には時間がかかります。
2. DPDKの動作概要
カーネルのネットワークスタックは汎用性に優れるものの、高速なパケット処理には向かないことが分かりました。じゃあDPDKはどうするのかというと、その重たい処理部分であるカーネルを介さずにNICとやり取りし、複雑な処理から回避してしまうのです(どけいカーネル!おれがやる!)。これをカーネルバイパスと言います。
DPDKの有無によってパケット処理の流れがどう変わるか図で確認しましょう。

左側は一般的なLinuxカーネルのパケット処理を、右側はDPDKによるパケット処理を示しています。DPDKを用いるとカーネルのネットワークドライバを全く通らないことが分かります。LinuxにはUser Space I/O (UIO) という、ユーザがカーネルを通さずにハードウェアを制御することができる仕組みがあります。DPDKはNICをカーネルの管理下から外し、UIOを用いてユーザースペースから直接やり取りするわけです。
DPDKを動かすには、まずNICをカーネル上のネットワークドライバからunbind(割り当て解除)します。次に、vfio_pci, igb_uio, uio_pci_genericと呼ばれるUIOモジュールの内いずれか1つにNICをbind(割り当て)します。これでDPDKのライブラリがユーザースペースからデバイスを制御できるようになります。
次はパケットがどのように送受信、処理されるか見ていきましょう。
3. Poll Mode Driver (PMD) によるパケット監視
DPDKアプリケーションがUIOモジュールによって初期化&制御されたNICとやり取りする際はPoll Mode Driver (PMD) というドライバを用います。カーネルがパケット処理に時間がかかっていたのは割り込み処理が原因の1つでした。DPDKでは割り込み処理を発生させないためにNICがパケットを受信したかどうかをポーリングで監視します。NICがパケットを受信しキューに蓄えると、PMDはパケットデータをそのキューから直接ユーザースペースに移します。当然カーネルをバイパスするため余計なオーバーヘッドなしにアプリケーションにデータが渡ります。
ポーリングによるパケット監視は確かに高速ですが、常駐するためCPUコアの使用率はずっと100%になります。(ファンがうるさい)
4. Hugepagesの活用
さて、PMDのおかげでパケットがアプリケーションに直接渡されることが分かりましたが、正確に言うとPMDが取得したパケットデータはHugepages(ヒュージページ)に蓄えられます。Hugepagesとは何でしょうか?Hugepagesを理解するためにまずPage(ページ)について軽く解説します。
Linuxはシステムメモリをページ と呼ばれる通常4KBのデータに分割して管理します。カーネルが発行するページはページテーブルと呼ばれるデータ構造に記載されており、どの仮想アドレスがどの物理アドレスに対応するか把握しています。
CPUがメモリにアクセスするときは、仮想アドレスをキーとして最初にCPU内部に保存されているTranslation Lookaside Buffer (TLB) を検索します。TLB上に目的のアドレスに対応するエントリがあれば物理アドレスが返ってきます(TLBヒット)。もしTLBにエントリがなかった場合(TLBミス)は仮想アドレスを変換するためにページテーブルを参照します。ページテーブルから物理アドレスを得るにはメモリに複数アクセスしなければならないため時間がかかります。物理アドレスを入手したらその仮想アドレスと物理アドレスのマッピングをTLBにキャッシュとして保存します。
あるプロセスが大量のメモリを消費する場合、管理するページ数が多くTLBのリソースをたくさん消費します。するとTLBミスが増えアクセスの速度が低下してしまいます。

Hugepagesとは、その名の通り4KBよりも大きなサイズのページのことで、2MBや1GBなど任意のサイズを指定することができます。ページサイズが大きくなってもTLBエントリは1つしか消費しないためTLBミスを削減することができます。ただ、メモリ空間をより大きな単位で仕切るためメモリの使用効率は下がる可能性があります。DPDKアプリケーションはあらかじめメモリを大量に消費するのでHugepagesを利用しできるだけTLBミスを減らし高速化を図ります。
5. ゼロコピー
DPDKはカーネルバイパスによってゼロコピーによるパケット処理を実現します。ゼロコピーという言葉はDPDKに限らず使われますが、コピーせずにデータが処理されることを言います。
受信したパケットデータはPMDによってHugepagesに書き込まれ、そのポインタがDPDKアプリケーションに与えられます。ユーザーはパケットの先頭からポインタをたどっていけばパケットの各フィールドにアクセスすることができます。送信するときはパケットのポインタをAPIに渡してあげればAPIがコピーなしでよしなに送信してくれます。楽ちん!
6. 論理コアの割り当て
DPDKアプリケーションはlogical core(lcore、論理コア)というスレッドを並列実行してパケットを処理します。lcoreは通常CPUの物理コアと1:1に対応させます。1コア=1スレッドとすることでできるだけコンテキストスイッチを減らしたいからですね。
lcoreに割り当てる処理内容は主に受信(Rx)、パケット処理(Processing)、送信(Tx)の3種類です。Processingを担当するlcoreのことをWorker coreと言うことがあります。lcoreの割り当て方には大きく2つ、run to completionモデルとpipelineモデルがあります。

run to completionモデルは1つのlcoreでRx, Processing, Txを実行するモデルです。物理コアのリソースをフルに使うことができますが、Processingの処理が複雑になると性能が落ちます。シンプルなパケット加工を行うアプリケーションに有効です。

ファイアウォール+ルーター+ロードバランサーなど、複数の機能を実装する場合は次のpipelineモデルが適しています。pipelineモデルは機能ごとにlcoreを割り当てます。もしRx, Txに加えProcessingが3種類ある場合は計5つのlcoreを割り当てて1つのパイプラインとします。Xeon Phi などのコアが多いプロセッサを使うとパイプラインがたくさん作れて気持ち良くなれます。
でも、Hyperthreading(ハイパースレッド)が有効になっている場合はどうすればいいんだ?
気になって調べたところ、メーリングリストにヒントが書いてありました。ハイパースレッドによって増えて見えるコアは、物理コアのリソースの一部を静的に分割しているようです。例えばRxとProcessingを1物理コア=2ハイパースレッドコアに割り当てると、1ハイパースレッドに仕事が集中して物理コアのリソースをうまく使いきれない可能性があります。
また、1物理コア内のハイパースレッドはL2、L3キャッシュを共有します。したがって、1物理コア=2ハイパースレッドで処理内容を分けるとキャッシュの奪い合いが起きる可能性があります。物理コアの数が足りない場合はハイパースレッドを用いてアプリケーションを動作させると良いと思いますが、基本的には1物理コア=1lcoreとしたほうが性能を引き出せるでしょう。
7. その他の注意点
DPDKはあくまでパケット処理のためのライブラリなので、プロトコルスタックを持っていません。したがって、ヘッダの解析やMACアドレスの書き変えなどのパケット処理はすべて自分で書く必要があります。自分で書くとはいえ、各ヘッダとフィールドへのアクセス、チェックサムの計算、パケットの先頭/末尾に対するデータのappendとtrimなどの操作はライブラリがサポートしているので、プログラムはそこまで難しくありません。
DPDKのサンプルアプリケーションにはL2スイッチやルーター、ロードバランサーなどがあり、かなり充実しています。最初はサンプルコードに手を加えていくと理解が深まるでしょう。頑張れば完全独自のプロトコルを走らせることもできます。
まとめ
DPDKによるパケット処理高速化の要点をまとめると以下のようになります。
- カーネルバイパスによるゼロコピー処理
- 割り込みではなくポーリング(PMD)
- HugepagesによるTLBキャッシュミスの削減
- スレッドを特定のコアに割り当てコンテキストスイッチを抑制
特にカーネルバイパスとゼロコピーはパケット処理において重要な考え方だと思います。
以上、長くなりましたがDPDKの仕組みについて解説しました。読んでくださりありがとうございました。