NUMAについて

NUMAについてしょっちゅう調べては忘れているのでまとめておくことにした。
全般的にFrank Denneman氏のブログを大変参考にさせてもらった。

NUMAとは

NUMA (Non-Uniform Memory Access) は、共有メモリ型マルチプロセッサコンピュータシステムのアーキテクチャのひとつ。
プロセッサとメモリの対 (これをノードと呼ぶ) が複数存在し、それらをインターコネクトで接続したものがNUMAと定義される。
共有メモリ型であるので各プロセッサが全ノードのメモリを利用可能である必要があるが、あるプロセッサから見て同一ノードのメモリをローカルメモリ、他ノードのメモリをリモートメモリと呼ぶ。
ローカルメモリへのアクセスは低レイテンシ、高パフォーマンスであり、リモートメモリへのアクセスは高レイテンシ、低パフォーマンスとなる。

f:id:hs_31:20170718150941p:plain

ほとんどのアプリケーションでメモリアクセスはある特定の領域に集中するので、オペレーティングシステムが適切にメモリを割り当てることによって、プロセッサが頻繁に参照する必要のあるデータをアクセスコストの低いメモリに配置し、アクセスコストの高いメモリには頻繁に参照しないデータを配置することができる。
この点に着目したのがNUMAアーキテクチャである。

UMAとは

UMA (Uniform Memory Access) は、NUMA以前から存在する共有メモリ型マルチプロセッサコンピュータシステムのアーキテクチャである。
各プロセッサがバスを共有しており、すべてのCPUが任意のメモリに対して同じ時間でアクセスできるのが特徴である。
UMAは SMP (Symmetric Multi-Processor) とも呼ばれる。

f:id:hs_31:20170718151112p:plain

UMAの場合、CPUはシステムバス (フロントサイドバス) を介してノースブリッジに接続されている。
ノースブリッジにはメモリコントローラがあり、メモリとの間のすべての通信はノースブリッジを通過する必要がある。
すべてのデバイスへのI/Oを管理するI/Oコントローラもノースブリッジに接続されており、すべてのI/Oもノースブリッジを通過する必要がある。
ノースブリッジの内部帯域幅に依存するアーキテクチャのため、UMAのスケーラビリティは限られていた。

NUMAの登場

スケーラビリティとパフォーマンスの向上のためNUMAが登場した。
2003年にAMDは、NUMAをサポートするOpteronをリリースした。
2007年にIntelは、同じくNUMAをサポートするNehalemをリリースした。
CPU間を接続するインターコネクトは、IntelではQuickPath、AMDではHyperTransportと呼ばれる。

NUMAの有効化/無効化とノードインタリーブ

NUMAをサポートするシステムでNUMAを有効化するには、BIOSでノードインタリーブを無効にする必要がある。
逆にノードインタリーブを有効にするとNUMAは無効になる。

f:id:hs_31:20110511214323j:plain

ノードインタリーブとは、RAID0のようにストライピングでメモリに書き込みアクセスを高速化するものである。
これにより、システムはメモリをUMAのように扱うことができる。

f:id:hs_31:20170718151257p:plain

NUMAノード

NUMAを有効化したシステムではNUMAノードという単位が用いられる。
NUMAノードは、物理プロセッサとローカルメモリをひとまとまりにしたグループを表し、基本的にソケット数分のNUMAノードが作られる。
例えば、ソケットあたり4コアのプロセッサを2つ、96GBのメモリを持つシステムの場合、4コア・48GBメモリの2つのNUMAノードで構成される。
NUMAノードは基本的にハイパースレッドを考慮しないので、単純に物理コア数計算となる。
ただし、Intel Cluster-on-DieテクノロジとAMD Opteron (Magny-Cours 以降)は例外となる。
Cluster-on-Die (CoD) は、Intel Haswellアーキテクチャから提供された。
CoDが有効にされると、各プロセッサが論理的に2つのNUMAノードに分割され、NUMAノード数はソケット数の倍になる。

f:id:hs_31:20170718151539p:plain

AMD Magny-Coursは、12コアのCPUを1つ開発する代わりに、実際には6コアのCPU2つを1つのパッケージにまとめたもの。
実体として内部に2つのプロセッサを持っているため、NUMAノード数はソケット数の倍になる。

f:id:hs_31:20170718151655p:plain

キャッシュスヌーピング

マルチプロセッサシステムでは、各コアがキャッシュを保持しており、共有メモリから読み出したデータをキャッシュで保持、更新してメモリに書き戻すことで、遅いメモリへのアクセスの回数を少なくして高速化を実現している。
このため、同じ場所のデータを複数のコアがキャッシュに持っており、一方が更新して書き戻すと矛盾が生じることになる。
これをキャッシュコヒーレンシ問題という。
この問題を解決するために各コアで持っているキャッシュの内容を同期させる必要があり、その方法はキャッシュスヌーピングと呼ばれる。
キャッシュコヒーレンシに影響を与える可能性があるキャッシュ操作が発生すると、キャッシュはこれを他のすべてのキャッシュに対して同期させるようブロードキャストする。
各キャッシュはこのメッセージを確認してそれに対して操作を行う。
キャッシュスヌーピングの方法の1つが前述のCoDである。

キャッシングアーキテクチャ

キャッシュスヌーピングを理解するためにキャッシングアーキテクチャを理解する必要がある。
Sandy Bridge以降のプロセッサ内部はリングバスを使った構造になっている。

f:id:hs_31:20170719110011p:plain

L1、L2キャッシュはコア内部に存在しコアごとに専有されて利用される。
LLC (Last Level Cache)はコアごとにスライスに分割され、各コア用のLLCスライスが存在し、リングバス経由で各コアで共有される。
キャッシュスヌーピングはリングバス経由で行われるが、送信元がプロセッサごとに1つしか存在しないホームエージェントか、コアごとに1つずつ存在するキャッシュエージェントかによって特性が異なる。
Broadwell世代では以下の4つのキャッシュスヌーピングモードがサポートされている。

キャッシュスヌーピングモード

Early Snoop

Early SnoopはSandy Bridgeにて導入された。
以前はこのモードがデフォルトになっていたが、現在は後述のDir+OSBがデフォルトになっている。
各コアごとのLLCに対応したキャッシュエージェント(CBOX)がメッセージを他のキャッシュエージェントに送信したり、全てのエージェントにブロードキャストする。
このモデルは、ブロードキャストトラフィックがNUMAノード間の帯域を消費する可能性があるが、レイテンシは小さくなる。
NUMAを利用する場合、このモードは推奨されない。

Home Snoop

このモードはIvy Bridgeにて導入された。
プロセッサごとに存在するメモリコントローラに結合されたホームエージェントがメッセージを生成する。
メッセージが都度ホームエージェントに行く必要があり、レイテンシが大きくなるが、NUMAノード間の帯域の消費は少なくなる。

Home Snoop with Directory and Opportunistic Snoop Broadcast (Dir+OSB)

このモードはIvy Bridgeにて導入され、Haswellで削除されたが、Broadwellで復活した。
現在はこのモードがIntelの推奨のデフォルトスヌープモードとなっているが、サーバベンダによっては別のモードがデフォルトになっているので注意する必要がある。
このモードはホームエージェントを使用するが、各ホームエージェント内に存在するディレクトリキャッシュを使用し、キャッシュ間の転送のレイテンシを小さくし、またブロードキャストを少なくすることで帯域の消費を少なくする。

Cluster on Die

Dir+OSBは全体的に最高のパフォーマンスを示すが、高度にNUMAに最適化されたワークロードの場合はCoDを検討することが推奨される。
CoDはプロセッサとメモリの組み合わせを更に2つのNUMAノードに分割する。
CoDの構造は下記のようになっている。

f:id:hs_31:20170719111938p:plain

CoDは対応したプロセッサでのみ有効にすることができる。
図のとおりプロセッサ内に2つのグループがあり、それぞれにリングバスおよびメモリコントローラ、ホームエージェントが存在し、それぞれのグループ間はインターコネクトで接続される。
スヌーピングはそれぞれのホームエージェントからHome Snoop with Dirで送信される。
各グループごとに分割されるためレイテンシが小さくなり、また各グループごとにホームエージェントが存在しそれぞれにディレクトリキャッシュを持つためヒット率が高まりブロードキャストが少なくなるので帯域の消費も少なくなる。

NUMAノードへの仮想マシンの割り当て

NUMAシステム上でESXiが仮想マシンにリソースを割り当てる際、ESXiは仮想マシンのvCPU数がNUMAノードに収まるかどうかを確認する。
収まらない場合、仮想マシンは複数のNUMAノードに跨って均等に割り当てられる。
例えば、4コア・48GBメモリの2つのNUMAノードが存在するシステムで、6コア・64GBの仮想マシンを作成する場合、2つのNUMAノードに跨って、1NUMAノードあたり3コア・32GBのリソースが割り当てられる。

ESXiのNUMAの扱い

仮想マシンがパワーオンされると、ESXi内のNUMAスケジューラは、仮想マシンのCPU、メモリの配置を決める。
これはPPD (Physical Proximity Domain)と呼ばれる。
PPDは物理NUMA構成に従って決定される。
仮想マシンから見えるCPU、メモリの配置はVPD (Virtual Proximity Domain)と呼ばれる。
これは、仮想マシンのvCPUの数、cpuid.CorePerSocketの数、preferHT設定によって決定される。
デフォルトでは、VPDはPPDに揃えられるが、cpuid.CorePerSocketが1より大きい場合、複数のPPDに跨るVPDが作成される。
例えば下記は、10コア/4ソケットのシステムに、20コア/2ソケットの仮想マシンを搭載した場合のPPD/VPDの構成である。

f:id:hs_31:20170718151946p:plain

この場合、仮想マシンは20コアのNUMAノードx2で構成されていると考えるため、キャッシュを利用しようとするが、物理的には20コアのうち10コアずつが別のCPUになっているため、
キャッシュミスが頻繁に発生して性能が劣化する (Cache Pollution)。

vCPUのコアとソケット

上記のCache Pollutionの問題があるため、VMware仮想マシンのvCPU構成はコア数は1とし、ソケット数を変更することを推奨している。
コア数を増やす機能が存在する理由は基本的にはソケット数計算で計上されるソフトウェアライセンスに対応するためである。

vNUMA とは

vNUMAとは、NUMAアーキテクチャ仮想マシンのOSに直接意識させる技術。
VMwareは、vSphere5でvNUMAをサポートした。
NUMAシステムではリモートメモリ操作とローカルメモリ操作でパフォーマンスコストに差があるため、複数のNUMAノードに跨ってリソースを割り当てられた仮想マシンの場合、仮想マシンのOS自身がNUMA構成を理解して適切にリソースを利用することがパフォーマンス向上に繋がる。
vNUMAは、ESXiの物理NUMA全体ではなく、仮想マシンに割り当てられているNUMA構造だけを、仮想マシンOSに公開する。
デフォルトでは、vCPUが9以上の場合にvNUMAは自動的に有効になる。
物理コア数が9未満で、vCPUが9未満でも複数のNUMAノードに跨る構成になる場合、この閾値を変更することで、適切にvNUMAが構成されるようにすることができる。
numa.vcpu.min

NUMAとハイパースレッド

デフォルトではNUMAノードの計算は物理コア数で計算され、ハイパースレッドは考慮されない。
ただしアプリケーションの中にはスレッド間で大量のメモリを共有するものがあり、そういう場合、複数のNUMAノードに分散せず単一のNUMAノード内に仮想マシンのリソースを配置するほうが性能が大きく向上する。
その場合、仮想マシンのpreferHTの設定を有効にすることにより、ハイパースレッドを考慮して、物理コア数を超えたNUMAノードを作成することができる。
numa.vcpupreferHT=TRUE
ハイパースレッドがデフォルトで考慮されない理由は、より多くの物理コアを使用することでパフォーマンスを最大化するためである。
ハイパースレッドは物理コアごとに論理的に2つのコアが存在するかのように見せる技術だが、実際は物理コア内のL2キャッシュを共有するため、単純に性能が2倍になるわけではない。
一方独立した物理コアを2つ使えば性能は2倍になるため、デフォルトでは物理コアを使用する仕様となっている。

vNUMAとCPUホットアド

仮想マシンのCPUホットアドが有効になっている場合、仮想マシンのvNUMAは無効になる。
物理的に複数のNUMAノードにまたがっていたとしてもノードインターリーブでのアクセスになる。
CPUホットアドは、使う予定が無ければ有効にすべきではない。

NUMAと透過的ページ共有(TPS)

TPSはセキュリティの考慮から近年のESXiではデフォルトで無効になっている。
有効にした場合、TPSはNUMAノード内でのみ有効となり、NUMAノード間では共有されない。
TPSはソルト値の等しい仮想マシンのメモリを共有するが、デフォルトのソルト値は仮想マシンのUUIDになっているので、仮想マシン内でのみ共有される。
VDIのような環境で複数の仮想マシンでメモリを共有したい場合はソルト値を変更すればよい。
ただし、Windows Vista以降のOSではラージページが採用されており、そもそもラージページはTPS対象外となっている。
ラージページがTPS対象外となるのは、通常のページサイズ(4KB)に比べて大きなサイズ(4MB)でメモリがアサインされるため、メモリ内容が一致するケースが少なく、TPSが効きづらいためである。
そのため、TPSを有効にしてもラージページを使用する仮想マシンにTPSは普段は効かないが、ホストのメモリが枯渇した時には例外的にTPSが効いてくる。
これは、ESXiがラージページを4KBに区切ってTPSを実行してメモリを確保しようとするためである。
また、仮想マシンOSがセキュリティのためにアドレス空間のランダム化(ASLR)や、windows superfetch を使っている場合は、さらにTPSが効きづらくなるので注意する。

NUMA関連の設定まとめ

cpuid.coresPerSocket

仮想CPUソケットあたりのコア数。
デフォルトは1。
変更すると複数のPPDに跨るVPDが生成され、Cache Pollutionを引き起こす可能性があるため、1が推奨。
ライセンスによる制約がある場合は変更は検討する。

numa.autosize

仮想マシンがパワーオンされた時に毎回NUMAノードの配置を自動調整するかどうかの設定。
デフォルトはFALSE。
クラスタに物理コア数の異なるサーバが存在する場合、配置が適切にされない場合があるのでTRUEにする。
その際、numa.autosize.onceをFALSEにする。

numa.autosize.once

仮想マシンが最初にパワーオンされた時にだけNUMAノードの配置を自動調整するかどうかの設定。
デフォルトはTRUE。
クラスタに物理コア数の異なるサーバが存在する場合、配置が適切にされない場合があるのでFALSEにする。
その際、numa.autosizeをTRUEにする。

numa.vcpupreferHT

NUMAノードの計算においてハイパースレッドを考慮するかどうかの設定。
デフォルトはFALSE。
メモリのレイテンシに性能が大きく依存するような仮想マシンの場合、有効にしたほうが性能が向上する場合がある。
一般的なワークロードではFALSEが推奨。

numa.vcpu.min

vNUMAが有効になるために必要な仮想マシンのvCPUの最小数。
デフォルトは9。
物理コア数が9未満で、vCPU数が少ないのに複数の物理NUMAノードに跨ってしまう場合、vNUMA構成を物理NUMA構成に合わせるために適切な値に変更するべき。

ベストプラクティスまとめ

  • 出来るだけ物理NUMAノード1つに収まるように仮想マシンをサイジングすべき
  • キャッシュスヌーピングモードは、大抵の場合Home Snoop with Dir+OSBが最適であるが、NUMAに最適化された環境ならCoDを検討してもよい
  • vCPUホットアドは使うべきではない
  • 仮想マシンのソケット数あたりのコア数は1にすべき
  • 大抵の場合ハイパースレッドは考慮しない設定でよいが、メモリレイテンシに敏感な環境ではハイパースレッドを考慮する設定を検討してもよい