提供するCloudFlareは、最近1億1000万ドルの資金調達が発表され、ますます注目を集めている。そんなCloudFlareのブログから、ネットワークチューニングの知見を紹介したい。(2015-11-20)※元記事の筆者には直接翻訳の許可を頂いて、翻訳・公開しております。McCaffrey先週、何気ない会話の中で同僚がこう話すのをふと耳にした。「Linuxのネットワーク・スタックが遅いんだよ!1コアあたり5万pps以上は期待できない」この発言に、私は考えさせられた。実用的には、コア毎50万パケット毎秒(pps)が限界だとは思うのだが、Linuxのネットワーク・スタックでどこまで可能なのか?もっと楽しめるように、言い換えてみよう。ppsを受信するプログラムを書くのは、どのくらい難しいのか?できれば、この質問に答えることが最新のネットワーク・スタック設計をする上で良い教訓になると良いと思う。以下のことを想定しよう。パケット/秒(pps)の測定は、バイト/秒(Bps)の測定よりずっと面白い。Bpsを高くするには、より良いパイプライン方式でより長いパケットを送信することで達成できる。ppsを改善するのはもっと難しい。ppsに興味があるので、私たちは短いUDPメッセージを使って実験を行う。正確には、32バイトのUDPペイロード、つまり、イーサネットレイヤでは74バイトだ。この実験のために、私たちは「受信用」と「送信用」の、2つの物理サーバを使う。この2台のサーバは両方とも、6コア2GHzのXeonプロセッサを搭載している。ハイパースレッディング(HT)を使うことで、それぞれ24プロセッサまで増やすことができる。この2台のサーバは、Solarflare製のマルチキュー10Gのネットワークカードを有しており、11の受信キューが設定されている。そのことについては後ほど。テストプログラムのソースコードはここで入手できる。[udpreceiver]必要条件UDPパケットには、ポート4321を使うとしよう。よって妨げられないようにしなければならない。NOTRACK明確に定義されたIPアドレス(2~3個)が、後に有効となる。始めるにあたって、最も簡単な実験をしてみよう。システムコールを使うこともできたかもしれないが、それは効率的ではないだろう。カーネルに切り替えるコンテキストはコストがかかるし、避けたほうがいい。幸いにも、最近Linuxに手頃なシステムコールが追加された。これによって、一回でたくさんのパケットを送信することができる。一度に1,024パケットでやってみよう。システムコールのより効率的なバージョンだ。50.509Mbこの単純なアプローチでは、19万7千pps~35万ppsの通信ができる。悪くはないが、残念ながら、かなりのばらつきがある。これは、カーネルがコア間でプログラムを切り替えているため起こる。CPUにプロセス・ピニングをすると良いだろう。92.033Mbさて、カーネルのスケジューラは定義されたCPU上でプロセスを保存する。これはプロセッサのキャッシュ局所参照性を改善し、希望する通りに数の整合性を取ってくれる。ppsというのは、単純なプログラムには悪くないが、100万ppsという目標にはまだほど遠い。多く受信するためにはまず、より多くのパケットを送信しなければならない。2つのスレッドからそれぞれ送信するのはどうだろうか。90.690Mb受信側の数字は増えていない。パケットが実際にどこに行ったかがわかる。0.0/sこのような統計値から、RXキュー番号#4に約35万ppsを正常に送信したとNICが報告してくれる。NICが45万ppsをカーネルに送信することができないというSolarflare特有のカウンターである。時に、なぜパケットが送信されなかったのかが明確でない時がある。私たちの場合、とても明らかで、RXキュー#4がCPU#4にパケットを送信したのだ。CPU#4が手いっぱいになり、35万ppsを読み取るのにビジー状態となってしまう。歴史的に見て、ネットワークカードにはかつてハードウェアとカーネル間でパケットを渡していた単一RXキューがあった。この設計は明らかに限界があった。単一CPUが処理を超えたパケットを送信することは不可能だったのだ。マルチコアシステムを活用するために、NICは複数RXキューのサポートを開始した。この設計はシンプルだ。各RXキューが個別のCPUに固定されているので、全てのRXキューにパケットを送信することで、NICは全てのUPUを利用できる。パケットを渡されて、NICはプッシュするRXキューをどのように決めているのだろうか?ラウンドロビン(均等負荷分散)バランシングは好ましくない。一つのコネクションでパケットの再組み立てを行う可能性があるためだ。一つの選択肢は、RXキュー番号を決めるのにパケットのハッシュを使うことだ。ハッシュは通常タプル(送信元IP、送信先IP、送信元ポート、送信先ポート)から数えられる。この方法だと、単一フロー用パケットが常に全く同じRXキューに行き着くようにし、単一フロー内のパケット再組み立てが起こらないように保証してくれる。私たちのケースでは、ハッシュをこのように使うことができただろう。私たちの設定ではこうだ。UDPパケットのために、NICがアドレス(送信元IP、送信先IP)をハッシュするということだ。number_of_queuesこれは、ポート番号を無視するため、かなり制限される。NICの多くでは、ハッシュをカスタマイズすることができる。使って、ハッシュ値を計算するタプル(送信元IP、送信先IP、送信元ポート、送信先ポート)を選ぶことができる。(送信元IP、送信先IP)ハッシュの生成のみ行う。NUMA(不均等メモリーアクセス)パフォーマンスにおける考察これまでのところ、私たちの全てのパケットは一つのRXキューにのみ流れ、一つのCPUにのみたどり着いている。これを機に、様々なCPUのパフォーマンスをベンチマークしてみよう。私たちの設定では、受信ホストにそれぞれ別々のNUMAノードを持った個別プロセッサバンクが2つ入っている。私たちの設定では、興味深い4つのCPUのうち1つに、シングルスレッドのレシーバを固定できる。その4つのオプションとは以下の通りだ。レシーバを、もう一つのCPUで、ただしRXキューと同じNUMAノードで稼働させる。上述した通り、パフォーマンスはおよそ36万ppsだ。RXキューと全く同じCPUのレシーバでは、~43万ppsまで達することができる。これは変動性が高くなる。NICがパケットで一杯になると、パフォーマンスはゼロまで落ちる。RXキューを処理するCPUと対応しているハイパースレッドでレシーバが動く時、パフォーマンスは通常の半分の約20万ppsになる。RXキューとは異なるNUMAノードにおけるCPUのレシーバでは、~33万ppsまで達する。数字にあまり一貫性がない。別々のNUMAノード上で動くことでの10%の損失はそこまで悪く聞こえないかもしれないが、拡張する度に問題は悪化する。幾つかのテストでは、コア毎に25ppsまでしかひねり出せなかった。NUMA間で行った全てのテストでばらつきが見られた。処理量が多くなると、NUMAノード間でのパフォーマンス損失はいっそう一目瞭然となった。あるテストでは、悪いNUMAノードでレシーバを動かした時には4倍の損失があった。数の受信IPアドレス私たちのNICでは、ハッシングアルゴリズムはかなり制限されるので、RXキュー間でパケットを分散する唯一の方法はIPアドレスをたくさん使うことだ。異なる距離のIPアドレスへパケットを送信する方法は以下の通りだ。パケットを明確なRXキューにパケットを確認する。166.120Mb万歳!RXキューの処理でビジー状態の2つのコアと、アプリケーションを実行中の3番目のコアで、65万ppsを達成できる!3または4のRXキューにデータを送れば、この数字をさらに増やすことが出来るが、アプリケーションはすぐに別の限界に達する。増えないが、netstatが受け取った「レシーバエラー」は以下の通りだ。0つまり、NICはカーネルにパケットを送ることが出来るが、カーネルはアプリケーションにパケットを送ることができないということだ。私たちのケースでは、44万ppsしか送信できず、アプリケーションが十分速く受信できないために残りの39万pps+12万3千ppsは落ちてしまう。多くのスレッドから受信するこれには、受信側アプリケーションをスケールアウトする必要がある。多くのスレッドから受信するためには、簡単な方法ではうまくいかない。124.322Mb受信中のパフォーマンスは、シングルスレッドのプログラムと比べて低い。これは、UDP受信バッファ側のロック競合によって起こる。両方のスレッドが同じソケットディスクリプタを使用しているので、UDP受信バッファ周りのロックに時間をかけすぎてしまうのだ。この資料に、この問題についてのより詳しい記述がある。一つのディスクリプタから受信するために、たくさんのスレッドを使うことは最適ではない。SO_REUSEPORTラッキーなことに、最近Linuxに追加された次善策がある。SO_REUSEPORTフラグだ。このフラグがソケットディスクリプタにセットされている時、Linuxは多くのプロセスが同じポートに結び付くことを許可する。実際、任意の数のプロセスがバインドされることを許可され、負荷が分散するだろう。SO_REUSEPORTによって、各プロセスが独立したソケットディスクリプタを持つ。したがって、それぞれが専用のUDP受信バッファを所有することになる。これは、以前遭遇した競合の問題を回避してくれる。288.354Mbよしよし、スループットがまともになった!より進んだ調査をすれば、さらに改善の余地が見つかるだろう。私たちは受信スレッドを4つ開始したにもかかわらず、負荷はそれぞれに均等にかかるわけではなかった。4つのうち、2つのスレッドは全てのワークを受信したが、他の2つはパケットを全く受信しなかった。これはハッシングの衝突によって起こるものだが、今回はSO_REUSEPORTレイヤで起こったものだ。140万ppsを達成することができた。異なるNUMAノードでレシーバを動かすと数字は落ち、せいぜい100万ppsまでしかいかなかった。要するに、完璧なパフォーマンスを望むなら、以下のようにする必要がある。たくさんのRXキューとSO_REUSEPORTプロセスに、均等にトラフィックを分散させるようにすること。実際には、接続(フロー)数が多ければ、負荷はうまく分散する。カーネルから実際にパケットを受け取れるよう、CPUキャパシティに十分に空きを持たせておく必要がある。大変なことに、RXキューと受信プロセスは同一のNUMAノードにある必要がある私たちはLinuxマシンで100万ppsを達成することが技術的に可能だということを証明してきたが、アプリケーションは実際にパケット受信処理をしていたわけではないし、トラフィックの中身すら見ていない。踏み込んだ調査をせずに、実際の運用で同じようなパフォーマンスは期待しないでいただきたい。❏フリーランスエンジ今夜が頑張り時のエンジニアのために。前提として、徹夜はすべきではありませんPHPエンジニアのキャリアパスと年収の遷移モデルPHPはWeb開発言語の中でも、1995年から人気が衰えない言語です。私がReact/Fluxアプリケーションを書いてきて、もう1年になる。❏フリーエンジニアの生活はどんなCriteoにおける大規模機械学習の仕組みCriteoの事業の核を担うのは、機械学習です。