NUMAアーキテクチャのスループット性能について


NUMAのアーキテクチャやコマンドの解説は、たくさんあるのですが、実際スループットに、どの程度影響するのかと言ったレポートは、ほとんど探せませんでした。 デュアルCPUのベアメタルの設定を確認する機会があったので、負荷試験をして効果を確認してみました。


## NUMAハードウェア

https://control.softlayer.com/ から、以下のメニューにあるデュアル・プロセッサ　マルチ・コアのベアメタルサーバーをオーダーすると、およそ２〜４時間くらいでサーバーが準備されて提供されるのですが、これらのIAサーバーはNUMAアーキテクチャーになっています。

![スクリーンショット 2017-12-28 12.57.39.png](https://qiita-image-store.s3.amazonaws.com/0/42829/fcbd5e82-beea-71e0-035c-d4edae6dd7a8.png "スクリーンショット 2017-12-28 12.57.39.png")

このNUMAの説明は、インターネットで検索すると、沢山の資料がヒットしますので、そちらを参照願います。 以下は [Wikipedia NUMA](https://ja.wikipedia.org/wiki/NUMA)からの引用です。

>  NUMA（Non-Uniform Memory Access、ヌマ）とは、共有メモリ型マルチプロセッサコンピュータシステムのアーキテクチャのひとつで、複数プロセッサが共有するメインメモリへのアクセスコストが、メモリ領域とプロセッサに依存して均一でないアーキテクチャである。

このNUMAアーキテクチャの難しい所は、メモリとCPUを持つ複数のノードが、バスで繋がって、一つのサーバーの様に見せているため、性能を追求するとノードについて配慮が必要になることです。 反対の言い方で混乱するかもしれませんが、あまり気にしなくても、そこそこ、ちゃんと性能が出る様になっている側面もあります。

今回利用するサーバーは、2つのNUMAノードを持ち、コア数がそれぞれ8個、計16コアのサーバーです。 1コアに２スレッドありますので、OSから見えるコア数は32個です。

NUMAノードとコアの対応関係は、`numactl -H`で参照することができます。 Node#0 のCPUコアは 0〜7, 17〜24 そして、Node#1 のCPUコアは 8〜15, 24〜31となっています。 

~~~
# numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 32064 MB
node 0 free: 29688 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 32253 MB
node 1 free: 29899 MB
node distances:
node   0   1 
  0:  10  21 
  1:  21  10 
~~~

このサーバーのシステム・ボードは、SuperMicro X10DRU-i+ で E5-2600v3/v4 で利用されるものです。 IAサーバーのNUMAボードは一般的に、I/Oデバイスを CPU#0 に接続することで、シングルCPUでも動作する様にしてあります。　このために、CPU#0に負荷が偏る傾向があります。例えば、WebサーバーやREST APIのサーバーでは、プロセスはパケットの到着を待って、受信したパケットを契機にプロセスが実行状態に入ります。 プロセスが実行状態に入る際に、対応するCPUを割り当てますが、パケット到着によって割り込みが発生したノードが優先されます。

![スクリーンショット 2017-12-28 12.52.53.png](https://qiita-image-store.s3.amazonaws.com/0/42829/bdfc463f-44ff-38d5-3c6f-b7d767d0da37.png "スクリーンショット 2017-12-28 12.52.53.png")



## テスト環境

このテストでは、２台のベアメタルサーバーを利用して、一方が検証対象のNUMAサーバー、そして、クライアントとしてリクエストを発生させるサーバーです。クライアントとなるサーバーで、CPUやネットワークがボトルネックにならない十分な性能を持ったものを選定しています。OSはUbuntu 14.04 64ビットです。

~~~
Linux: Linux version 3.13.0-137-generic (buildd@lgw01-amd64-058) 
Build: (gcc version 4.8.4 (Ubuntu 4.8.4-2ubuntu1~14.04.3) )
Release  : 3.13.0-137-generic
Version  : #186-Ubuntu SMP Mon Dec 4 19:09:19 UTC 2017
cpuinfo: model name : Intel(R) Xeon(R) CPU E5-2620 v4 @ 2.10GHz
cpuinfo: vendor_id : GenuineIntel
cpuinfo: microcode : 0xb000021
cpuinfo: cpuid level : 20
# of CPUs: 32
Machine  : x86_64
Nodename : test61
~~~


テストのアプリケーションは、Node.js Express フレームワークで開発した RESTリクエストに応答するサーバーです。 実際のアプリケーションに動作を近づけるために、リクエストを受けると疑似的な処理を繰り返すて、CPU時間を消費する様になっています。 テスト用アプリケーションのサーバー・プロセスは、CPUコアの数だけ一意のポート番号を与えながら同一プログラムのプロセスが起動します。 

テスト用クライアントは、すべてのプロセスのポート番号を順番にアクセスする様に動作します。 ネットーワーク越しに、一つのクライアントプロセスでは十分な負荷を与える事が出来ないので、テストでは７つのスレッドを並行処理し連続してリクエストを実行することで、負荷を与えます。


以下の６ケースについて検証を実施します。 

* ケース1 ノーマル
* ケース2 ノード＃0へのバインド
* ケース3 ノード＃1へのバインド
* ケース4 プロセスを担当するCPUのノード側のメモリを利用
* ケース5 ノードから均等にメモリを割当
* ケース6 RFS (Receive Flow Steering とノードバインドを併用



## ケース1 ノーマル

まずはノーマルと表現するのは、プロビジョニングされたベアメタルサーバーにチューンング設定などを入れず、アプリをインストールして、動作させた状態です。 サーバーを開始するコマンド実行します。 実行にあたりnumactlの設定は行いません。

~~~
$ ./start.sh 
~~~

負荷を与えるベアメタル・サーバーで計測されたスループットです。 開始から30秒後の値を比較対象として見て行きたいと思います。

~~~
経過時間(秒)    Hit スループット(Hit/sec)
5.00629782677 283 56.5288090527
10.0109198093 501 100.107461012
15.0148999691 416 83.1338228208
20.019949913  528 105.493452796
25.0249919891 498 99.4996630252
30.0300300121 523 104.494710649
35.0350530148 546 109.090407719
40.0401079655 494 98.7002150555
~~~

![スクリーンショット 2017-12-28 11.06.33.png](https://qiita-image-store.s3.amazonaws.com/0/42829/c60e4504-5f29-85eb-f75f-cc7487b18d41.png "スクリーンショット 2017-12-28 11.06.33.png")

nmonのスクリーン・ショットから、CPUが1〜32あり、Node#0 の CPU 1〜8, 17〜24 そして、Node#1 のCPU 9〜16, 25〜32へ処理が割り当てられていることが解ります。 Node#0のCPU(1〜8)そして Node#1のCPU(9〜16)に処理が割当たっています。 Ubuntu 14.04 のLinux カーネル 3.13.0では、Node#0とNode#1に程よく処理が分散されている様に思われます。 CentOS6のケースでは、Node#0に明確に偏った傾向があった様に思います...


## ケース2 ノード#0へのバインド

ノード#0に属するCPUコアが、nmonの表示と一致しているかを確認する目的も併せて、このケースを実施します。 `numactl --cpunodebind` を使って、Node#0に片寄します。 

~~~
$ numactl --cpunodebind=0 ./start.sh 
~~~
結果は、割当たるコア数が減ってしまったので、前者と比較して、スループットが落ちてしまいました。

~~~
経過時間(秒)    Hit スループット(Hit/sec)
5.00512981415 281 56.1424106818
10.0101838112 433 86.5125531625
15.0152308941 370 73.9253784972
20.0192139149 339 67.7460332283
25.0242547989 410 81.9174127646
30.0293109417 433 86.5125160728
35.0343358517 439 87.7118511689
40.0393569469 450 89.909710955
~~~

nmonのスクリーン・ショットを見れば、Node#0にサーバーのプロセスが片寄されていることが直感的に理解できますね。

![スクリーンショット 2017-12-28 11.23.28.png](https://qiita-image-store.s3.amazonaws.com/0/42829/9955622f-afef-134c-c7f2-86ba0e5b425e.png "スクリーンショット 2017-12-28 11.23.28.png")



## ケース3 ノード#1へのバインド

ノード#0へのバインドを実施したので、もう一方のノードも確認しておきます。

~~~
$ numactl --cpunodebind=1 ./start.sh 
~~~
結果は、ケース２とほぼ同じ値になっています。 Node#1のCPUのプロセスは、NICからのデータを取得するにあたり、間接的にQPI(QuickPath Interconnect)を介るす事になります。 このため、Node#1では　レイテンシーが増える事になり、Node#0と比較すると、結果も悪くなることが予測されたのですが、あまり変わらない結果となりました。 1GbEのリンク速度では、QPIリンクの速度が十分早いため、影響がわからないということだと思います。

~~~
経過時間(秒)    Hit スループット(Hit/sec)
5.00622606277 293 58.527132443
10.0112688541 424 84.7145604292
15.0163209438 439 87.7113748535
20.0213680267 446 89.1100508372
25.0264189243 458 91.507560936
30.0314729214 449 89.7093218706
35.0365250111 460 91.9071353818
40.0415630341 448 89.5098095043
~~~

Node#1のCPUコアに負荷がかかっていることが、ハッキリわかりますね。

![スクリーンショット 2017-12-28 11.31.03.png](https://qiita-image-store.s3.amazonaws.com/0/42829/4165f0bd-bdd7-f357-bb36-f2a330f138ff.png "スクリーンショット 2017-12-28 11.31.03.png")


## ケース4 プロセスを担当するCPUのノード側のメモリを利用

`numactl --localalloc` でアプリケーションを起動することで、プロセスが割当たったCPUコアのノードのメモリを利用するオプションを加えて、アプリケーションを起動します。 プロセスは自分のCPUコアと同じノードのメモリをアクセスしますから、もっとも高いスループットを期待することができます。

~~~
$ numactl --localalloc ./start.sh 
~~~

期待通り、これまでの中で、もっとも良好なスループットを記録しています。

~~~
経過時間(秒)    Hit スループット(Hit/sec)
5.0068500042  308  61.5157380537
10.0119161606 549 109.688859816
15.0170841217 536 107.089313318
20.01922822   558 111.552164239
25.024269104  569 113.685385032
30.0296270847 571 114.077754718
35.0312261581 565 112.963872495
40.0362770557 565 112.88596491
~~~

CPUコア番号の若い方から順に、負荷がかかっており、Node#0とNode#1に偏りなく負荷がかかっています。 この `numactl --localalloc` を利用しているため、QPIでメモリをアクセスする遅延が少ない状況が、スループット値に現れていると考えれます。

![スクリーンショット 2017-12-28 11.36.13.png](https://qiita-image-store.s3.amazonaws.com/0/42829/64ee23fd-1913-753a-78d5-b68bdd2a53fb.png "スクリーンショット 2017-12-28 11.36.13.png")


## ケース5 ノードから均等にメモリを割当

`numactl --interleave=all` は、すべてのNUMAノードから均等にメモリを取得します。 NUMAノード間のQPIを多用する事になり、メモリアクセスの遅延が増大するために、良い結果は期待できません。

~~~
$ numactl --interleave=all ./start.sh 
~~~

それほど、前者と比べ大きな差が出ていませんが、やはり、10パーセント程度のスループットの劣化が認めることができます。

~~~
経過時間(秒)    Hit スループット(Hit/sec)
5.00554013252 494 98.690666949
10.0105819702 516 103.096041299
15.0141611099 509 101.727180842
20.0192120075 497 99.2996894874
25.0242650509 513 102.496416233
30.0293190479 517 103.29558888
35.0343711376 502 100.298656438
40.0394220352 512 102.296662007
~~~

理由はハッキリしませんが、Node#0のCPUに偏っている様に見受けられます。

![スクリーンショット 2017-12-28 11.41.22.png](https://qiita-image-store.s3.amazonaws.com/0/42829/3820b8da-06f3-213f-e8f4-2a09de0f28a4.png "スクリーンショット 2017-12-28 11.41.22.png")


## ケース6 RFS (Receive Flow Steering とノードバインドを併用

これまで最も良いスループットが出たケース４と、RFSを組み合わせたケースです。 RFSを有効にするために、GitHub https://github.com/takara9/baremetal_rfs_tune の以下のシェルを実行して、ハードウェアのアクセラレーション機能を有効にするため、`ethtool -K eth0 ntuple on`を設定します。

~~~
root@test61:~/rfs# ./set_rfs.sh 
root@test61:~/rfs# ethtool -K eth0 ntuple on
~~~

次に、`--localalloc`を追加して、アプリケーションをスタートさせます。

~~~
$ numactl --localalloc ./start.sh 
~~~

結果は、最も良い値ですが`--localalloc`の時と、ほとんど差がありません。 RFSによる改善効果が無いといえます。  

~~~
経過時間(秒)    Hit スループット(Hit/sec)
5.00513195992 285 56.9415664934
10.0087099075 547 109.321770486
15.0137608051 557 111.287579566
20.0188019276 561 112.086991151
25.0238568783 575 114.883853556
30.0289578438 571 114.083612686
35.0327107906 577 115.313446952
40.0377647877 572 114.284481314
~~~

RFSを有効にすることで、各CPUコアへリクエスト負荷が分散される様になるのですが、それ以前に、Ubuntu14.04 カーネル 3.13.0は適切にノードに負荷分散が実行されるため、スループット向上への寄与が少ないと考えられます。 

![スクリーンショット 2017-12-28 11.54.17.png](https://qiita-image-store.s3.amazonaws.com/0/42829/f082c69f-1d50-2c0b-1567-d6b8310cb40e.png "スクリーンショット 2017-12-28 11.54.17.png")



# まとめ

テスト結果をまとめた表が以下になります。 スループットは経過時間 30秒 の値をサンプルしています。


| ケース  | スループット|
|:-------|--------:|
| ケース１ ノーマル | 104.494|
| ケース２ ノード＃0へのバインド|  86.512|
| ケース３ ノード＃1へのバインド |  89.709|
| ケース４ プロセスを担当するCPUのノード側のメモリを利用 | 114.077|
| ケース５ ノードから均等にメモリを割当 | 103.295|
| ケース６ RFS (Receive Flow Steering とノードバインドを併用 | 114.083|

このテストで解ったことを箇条書きにします。

* 何も設定しなければ、一部のCPUコアに負荷が偏って見えるのですが、Node#0 と Node#1のレベルで適切に負荷が分散化されており、numacltやRFSを設定しなくても、良好なスループットが得られることがわかりました。
* NUMA対応チューニングとして、プロセスに割当たるCPUコアのノードからメモリを確保する `numactl --localalloc` がスループットに対して寄与することが判りました。
* RFSを設定することで、CPUコアレベルで、リクエストが分散されるため、全コアに負荷を分散させることができますが、スループットに対しては、実は寄与しないことが判りました。 


カーネル2.6系について調べた続編があります。 [NUMAアーキテクチャのスループット性能について（その２）](https://qiita.com/MahoTakara/items/72f7749f4faac8d24579)