執筆者 : 田 幸一郎
1. はじめに
近年活発に開発が進んでいるAF_XDPを利用したアプリケーションは、(skbを持ち回してプロトコルスタックを駈け登る/駆け下りる)Linux Kernel Network Stackをバイパス出来るという観点で、DPDKを活用したアプリケーションと比較されることが少なからずあるかと思います。普通この2つの要素技術を比較検討する際、スループット性能に焦点が当たる事が多いと思いますが、最大遅延にフォーカスしてみる必要が出るユースケースもあるでしょう。そこで本記事では、AF_XDPアプリケーションにおける最大処理遅延を考える際の枠組みと抑え込み方法について考察してみたいと思います。
2. 環境
- Linux Kernel: bpf-next 2bab48c5bef0 ("Merge branch 'improve-bpf-tcp-cc-init'") (release: v5.9-rc1)
3. 前提
- 明示的に指定しない限り、rt-preempt kernelを想定しません。
- ユースケース次第では、リアルタイムカーネルは十分前提条件に出来るかもしれませんが、今回はあまり制約条件を設けないこととします。
- 想定するアプリケーションについて
- 簡単の為、アプリケーションはシンプルなl2fwdのような物を想定します。
- NIC AのHW RX Queue #Nで受信したパケットに対してXSKアプリケーションがプロトコル処理を施し、NIC BのHW TX Queue #Mから送信する、といった状況を想定します。
- 簡単の為、PipelineモデルではなくRun-to-completionモデルでの実装を想定します。
- いずれも送受信バッファはHugePageより割り当てることを想定します。
AF_XDPアプリケーション固有の前提事項
- NICに対応するkernel in-treeのドライバはXDPをサポートする物を使用する想定です(i.e., generic XDPを想定しない)。zerocopyモードです。
- 今回パフォーマンスの為、NIC A Queue #N、NIC B Queue #Mで1つのumemを共有する想定です。
なお、net_device或いはqueueを跨いだumem共有は、kernel v5.9では取り込まれるはずのfeatureです。(davem net-next treeにもマージ済)以下図で、重要なentityの対応関係の変化を示します。
本記事は、先述の項「環境」にあるように、より最新である上図下半分を前提としています。 ところで、2箇所の
1..N
、それぞれ以下のような意味合いです。XSK <-(1..N)--------(1)-> UMEM
の箇所: これは1つのqueue(つまり特定のnet_deviceの、特定のqueue)に対して、複数のXSKを作成出来るという意味合いです。Buffer Pool <-(1..N)--------(1)-> UMEM
の箇所: これは、異なるqueue、ひいては異なるnet_device間でUMEMを共有出来るという意味合いです。
暗に比較対象としている、DPDKアプリケーション固有の前提事項
- Port毎に、RX Burstに従事するlcoreを用意し、TX Burstは受信lcoreがそのままRun-to-completionで実施する状況を想定。 つまり、NIC次第ではTX_LOCKを必要とする状況。
- 非interruptモード、つまりポーリングモード(※典型的なDPDKアプリケーション)
4. AF_XDP (zerocopy) アプリケーション
4-1. アーキテクチャ
AF_XDPを利用したアプリケーションのアーキテクチャを、AF_XDP実装のソースコードに踏み込んで理解しようとするとき、xdp_sock
(XSK)は勿論のこと、4種のSPSC(Single-Producer/Single-Consumer)キュー(RX/TX/CQ/FQ)、xdp_umem
(UMEM)、xsk_buff_pool
(XSK buffer pool)が重要な存在です。
4種のSPSCキューに対するproduce/consumeを誰がいつ実行するのかを把握することで、典型的にはlibbpfを介して実装することになるであろうAF_XDPアプリケーションのアーキテクチャを俯瞰することが出来ます。以下図をご覧ください:
上図の [R1] ~ [R5] 、及び [T1] ~ [T5] を順に追って確認していきましょう:
- [R1]: FQに空きスペースを作り、driverに新しくパケット受信可能なDMA領域として認知させます。 XSKアプリケーション起動直後は全スロットを一気にproduceすることになりますが、 以後は後述の [R4] で読んだパケットがもう不要になったときに、FQにその旨まとめて伝える為のフェーズとなります。
- [R2]: 受信パス(NAPI poll on NET_RX SoftIRQ)で実行されたXDPプログラムを通じて XSKMAPのエントリのsocketにredirectする事が決定した場合、 FQに積まれたパケットのメモリ領域を指す新しいdescriptorをRX Queueに積みます。
- [R3]: 典型的にはNAPI poll内の、XSKへのredirect関連処理が一通り完了した段階での
xsk_map_flush
に合わせて、 まとめてRX Queueへのproduceを確定させます(ローカルのキャッシュポインタが指すインデックスを真のポインタに書き込むことで、 XSKアプリケーションに新しいパケットの存在を知らせます)。 - [R4]: XSKアプリケーションが未読のdescriptorを参照し、受信パケットを正しく読めるようになります。
- [R5]: 典型的には受信パスのSoftIRQにおけるNAPI Pollの延長線上で、 FQを消費しNICが受信パケットをDMA転送する先としてセットします。 FQを消費できるということは、XSKアプリケーションが [R1] で受信処理済みパケットに対応する不要UMEM領域を解放したことを示します。
- [T1]: XSKアプリケーションは送信したいパケットを指すdescriptorを生成し、 TX Queueに積みます。なお、sendmsg(2)を通じて送信パスを叩き起します(図中右端部分)。1
- [T2]: 送信パスのコンテキスト (典型的には受信パスを実行するNET_RX SoftIRQ) でTX Queueに積まれたdescriptorを読み込みます。
- [T3]: [T2] 後そのコンテキストの延長で、読み込んだdescriptorをCQに積みます。
- [T4]: [T3] 後そのコンテキストの延長で、TX Queueからの消費をまとめて確定します。
- [T5]: 送信完了割込み時 (IRQコンテキスト) 、不要になったパケット領域を解放すべくCQにproduceします。
なお、ここではスペースの都合上、1つのbuffer poolに対して1つのXSKしか作成しておらず、 またRX/TX Queue共にセットアップした状態を描いていますが、 本記事で想定しているl2fwdアプリケーションにおいては、1つのbuffer poolに対して2つのXSKを用意することは十分に考えられます。
4-2. レイテンシ特性
4-2-1. 特性
以下、pseudo-sequence diagramで示すように、AF_XDPアプリケーションのレイテンシ特性を考える時、(AF_XDPアプリケーションのuserlandのスレッドはisolcpus上でビジーに稼働させるであろうことを踏まえると)結局SoftIRQ処理実行開始タイミングの遅延を如何に抑え込むかが重要だと分かります。なお、スペースの都合上4種のSPSCキューは極力描いておらず、最低限流れを掴む為に描画すべきと思われたRX Queueのみ掲載しています。
DPDKアプリケーションは今更取り上げるまでもないと思われる為ここでは割愛します。 但し、上図のAF_XDPアプリケーションと比較する場合、アプリケーション実装含めて 全体をRun-to-completionモデルで実装することを想定しておくべきでしょう。 すなわち、RX Burst後そのまま同一lcoreによるTX Burstまでを一気通貫で行う 2 ことを想定下さい。
さて、以上よりdata plane処理を担うSoftIRQ処理の、最悪のケースにおける実行開始遅延を抑えることが重要と分かりますが、AF_XDPアプリケーション処理の最大レイテンシを抑え込む為には、複数の観点で注意深くチューニングを施していく必要があります。次項で整理していきます。
4-2-2. チューニング
(a). 一般的なirq_exit
からのSoftIRQ実行及びksoftirqdによる実行双方に共通する項目
SMP IRQ Affinity
data plane処理とは無関係の何らかのデバイスが一時的に大量のハード割込みを上げてきた場合においても、data plane処理が影響を受けないように、(環境依存の)SMP IRQ Affinity設定は注意深く行う。
各種SoftIRQの実行CPUを出来るだけ分離する
https://www.kernel.org/doc/Documentation/kernel-per-CPU-kthreads.txt
詳細は上記URL先の、ksoftirqd/%u 項の解説を参照しながら地道に対応すると良いでしょう。我々にとっては、data planeのNET_RX_SOFTIRQが特定のCPUコアにおいて、他種SoftIRQに邪魔されないように注意深く対応する事になります。ここでは単に和訳するだけになってしまいそうなので、割愛します。
---(該当箇所抜粋)--- Name: ksoftirqd/%u Purpose: Execute softirq handlers when threaded or when under heavy load. To reduce its OS jitter, each softirq vector must be handled separately as follows: TIMER_SOFTIRQ ---(以後、チューニング項目・手順が詳細に列記されています)---
各種SoftIRQの実行時間自体を出来るだけ抑える
- NET_RX_SOFTIRQ : 遅延しても問題の無いパケット受信(例えばmanagement portとしての役割を担うNIC queues)に関して、NAPIのbudgetを変更する事が出来るのであれば、取り得る最小の値に設定すると安心でしょう。
- NET_TX_SOFTIRQ : そもそもdata planeの受信処理(NAPI poll)を実行するCPUコアにおいて、NET_TX_SOFTIRQがraiseされる状況は、CPUアイソレーションの観点で問題があります。それでも念の為に、dev_weightを減らしてあげたり、そもそもQdiscを介する必要のない送信パスを担うインターフェースに関しては、
tc qdisc add dev ${iface} root noqueue
しておくなどしておいて損はないかもしれません。 - RCU_SOFTIRQ : RCU callbackは別途kernel thread(
rcuo
)に逃がしてあげると安心でしょう。
(b). ksoftirqdに限定される項目
スケジューリングポリシーの設定
ここでは、非rt-preempt kernel 3におけるksoftirqdの実行リアルタイム性を上げる為にどのスケジューリングポリシーを適用すべきか、DPDKアプリケーションとレイテンシの観点で対抗するという観点で考えてみます。
結論として、(アップストリームでは利用不可能ですが) SCHED_MICROQ
(Micro Quanta) を活用することを最善策として順に掲げています。
policy | 本ケースにおける筆者の選択優先順位 |
---|---|
SCHED_FIFO | ◯ |
SCHED_RR | △ |
SCHED_DEADLINE | x |
SCHED_MICROQ | ◎ |
SCHED_FIFO policy
isolcpus上のksoftirqd/Nは、isolcpusであるが故に 4 、rqのroot domainはデフォルトルートドメインとなります。
また後述でも述べますが、ksoftirqd/Nはper-cpu kthreadであり、PF_NO_SETAFFINITY
がセットされている為、cgroupsを介したroot domainの調整 (aka. partitioned scheduling) は出来ません。
よって少なくとも現時点では、load balanceはisolcpusの範囲に限定される他ありません。
加えてそもそも、runtime/period設定がグローバルである為に、実運用環境次第ではあるものの、あまり細かな帯域制御 (例. runtime=15us/period=20us) を設定出来る環境であるとも限りません。
以上の懸案をもってしても、後述の各scheduling policyと比較してマシな選択肢である為、相対的に ◯
としました。
SCHED_RR policy
もしksoftirqdを SCHED_RR
にするのならば、原理的にはグローバルな帯域制御に加えて、同一priorityの別タスクの最大数にtimesliceを掛け合わせた分だけksoftirqd実行が遅延し得ることに注意が必要でしょう。特に前者がどれほどになるかは環境依存である為、見積りが難しいです。
その為筆者は、今回のケースのような状況下では、 SCHED_RR
を試すくらいならまず SCHED_FIFO
で試行し、システムの安定性が損なわれるケースを拾い上げて対処していくことを優先すべきと考えます。
SCHED_DEADLINE policy
もしisolcpusを複数設定しているのなら、原則としてksoftirqd/Nに SCHED_DEADLINE
ポリシーを設定することは出来無いと考えた方が良いでしょう。
何故なら、ksoftirqd/Nは当root domainに紐づくいずれのCPU上でも実行可能である必要 5 がありますが、
- ksoftirqd/Nはper-cpu kthreadであり、
PF_NO_SETAFFINITY
がセットされている為、group単位のpartitioned schedulingでCPU#Nに対応するroot domainを限定させる事が出来無い - グローバルなadmission control自体を無効にする事は、deadline schedling classにおけるcappingだけでなく、rt scheduling classにおけるbandwidth cappingも無効化してしまう為、システムの安定性が損なわれる恐れがある
為です。DPDKアプリケーションと比較するようなシーンにおいては、DPDKアプリケーションがしばしば複数コアをisolcpusに設定するでしょうから、ここでも公平性の観点でisolcpusは複数指定することを想定すべきです。よって、基本的にdeadline schedling classはksoftirqdに対しては設定出来無い物として捉えます。
SCHED_MICROQ policy
MicroQuantaは、Snap の構成要素として提案されたLinux Kernelのscheduling classです。
本記事が想定する環境はLinux Kernel v5.9.0-rc1である為、当時のパッチはそのままでは適用出来ず、幾らか更新が必要であることに注意してください。 また、isolcpus上に稼働させるタスクのいずれに関しても、意図的に既存の最高優先度の-dlポリシーは適用しないよう注意してください。
upstream予定は無さそうですが、典型的な想定環境(複数isolcpus)においては、 既存の-dl/-rtタスク群に影響を与えずにper-cpu kthreadに対して軽量のdeadline schedulingを適用できる為に、便利だと感じます。
4-3. スループット特性
スループットに関してはDMA transferのスループットやその他環境依存の制約、 また実装の進歩次第で大きく変化すると考えられる為、実際に然るべき環境を用意し計測して比較し、 環境のボトルネックを考慮したモデル化を経ないと定性的に比較評価することが難しいです。そこで本記事では触れません。
5. まとめ
- AF_XDPの場合、依然GPOSならではのIRQ+SoftIRQ処理を介す形を取る為に、 レイテンシを抑え込むにはSoftIRQをどうコントロールするかが肝になります。 rt-preempt kernel (-> irq forced threading) を使用できない条件下においては、特に注意が必要でしょう。
- その他の点では、限り無くDPDKアプリケーションに類似したアーキテクチャ、性能特性のアプリケーションを作れるようになってきているかと感じます。 特に本記事冒頭の前提で取り上げている"shared umem"のパッチに含まれるsample application "xsk_fwd"を見れば、より強くそう感じるでしょう。
-
当該仕様は公式ドキュメント(https://www.kernel.org/doc/html/latest/networking/af_xdp.html)において
[...] To start the transfer a sendmsg() system call is required. This might be relaxed in the future. [...]
と言及されていることに念の為触れておきます。↩ -
複数lcoreからの同時TX Burstの為に、NIC次第ではTX Lockが必要になるということを暗に意味しています。
!!dev_info.tx_offload_capa & DEV_TX_OFFLOAD_MT_LOCKFREE == 1
の場合は不要です。↩ -
rt-preempt kernelであれば、
CONFIG_IRQ_FORCED_THREADING=y
である為、常にksoftirqdのプロセスコンテキストでSoftIRQワークロードが捌かれます。 その為、先述の「(a). 一般的なirq_exit
からのSoftIRQ実行及びksoftirqdによる実行双方に共通する項目」は考える必要がなくなり、(b)のコントロールだけで済むので、その意味でもdeterministic性が高まります。↩ -
より正確には、
CONFIG_CPU_ISOLATION=y
で且つ、isolcpusに一切フラグを渡していないか、明示的に"domain"フラグを渡すことで、housekeeping_flagsにHK_FLAG_DOMAIN
が設定されている場合に限ります。↩ -
言い換えると、
task_struct.cpus_ptr
がroot_domain->span
全てをカバーしている必要があります。deadline scheduling classのEDF (Earliest Deadline First) 実装をそのまま適用してしまえば、該当schedule domainで自由に実行CPUコアを選べないタスクの存在が結果として、何らかのタスクのdeadlineを保証出来無くしてしまう恐れが出るし、また特定のCPUにおいて低優先度のscheduling classのCPU枯渇を招いてしまうでしょう。(-dlのadmission controlはdomain単位)↩