ハードウェア中級 #4 NUMA — メモリは均一ではない
第3回 までのメモリには、暗黙の前提が 1 つありました。どのコアからアクセスしてもメモリは同じ速さだ、という前提です。CPU ソケットが 1 個のサーバーではおおむね正しいのですが、ソケットが 2 個以上になった瞬間に崩れます。今回はその非均一性、つまり NUMA を扱います。サーバーのスペック表にある「2 ソケット」という 1 行が、性能にとって何を意味するのかという話です。
NUMA とは — メモリに距離が生まれる #
NUMA (Non-Uniform Memory Access、非均一メモリアクセス) はマルチソケットサーバーのメモリ構造です。各 CPU ソケットは自分に直接つながったメモリを持ち、ソケットとそのメモリのまとまりを NUMA ノードと呼びます。
┌─ ノード 0 ────────────┐ ┌─ ノード 1 ────────────┐
│ CPU ソケット 0 │ インターコネクト │ CPU ソケット 1 │
│ (コア 0〜15) │◀────▶│ (コア 16〜31) │
│ メモリ 256GB (ローカル) │ │ メモリ 256GB (ローカル) │
└──────────────────────┘ └──────────────────────┘ノード 0 のコアがノード 0 のメモリを読めばローカルアクセス、ノード 1 のメモリを読めば、ソケット間のインターコネクトを渡るリモートアクセスです。リモートはローカルよりレイテンシが長く (おおよそ 1.5〜2 倍程度)、帯域幅も狭いです。基礎 #3 のメモリ階層の絵に、「同じ RAM の中にも近い RAM と遠い RAM がある」という層がもう 1 つ増えるわけです。
オペレーティングシステムはこれを知って動きます。Linux は基本的に、プロセスが走っているノードのメモリを先に割り当て (first-touch)、スケジューラもタスクを同じノードにとどめようと努力します。問題は、その努力が崩れる瞬間です。
いつ問題になるのか #
NUMA が性能インシデントとして表に出る典型的な場面は 3 つです。
- 1 ノードより大きいメモリを使うプロセス — 256GB ノード 2 個のサーバーで 400GB を使うデータベースは、必然的に 2 つのノードにまたがります。どのコアで走っても、半分ほどはリモートアクセスです。
- 片方のノードのメモリ枯渇 — プロセスがノード 0 に偏ると、ノード 1 にメモリが残っていてもノード 0 が先に干上がります。カーネルはリモート割り当てやノード 0 のページ回収 (ひどいとスワップ) で対応しますが、「全体のメモリは余っているのにスワップが回る」という不可解な症状はこうして作られます。
- スレッドの移動 — スケジューラが負荷を均すためにスレッドを別のノードへ移すと、そのスレッドのメモリは元のノードに残ります。以後のアクセスがすべてリモートになります。第2回 のピンニングが、キャッシュだけでなく NUMA のためにも登場する理由です。
症状に共通するのは、指標上はリソースが余っているのにスループットが出ない という姿です。CPU 使用率もメモリ量も余裕なのに、同じワークロードが 1 ソケットのマシンより遅いなら、NUMA を疑う番です。
見方 — numastat と numactl #
構造の確認は numactl --hardware、動作の確認は numastat で行います。
$ numastat
node0 node1
numa_hit 98214532 97103211
numa_miss 1203334 5421887
numa_foreign 5421887 1203334
other_node 1456220 5673001読む行は 2 つです。numa_hit は意図したノードで割り当てができた回数、numa_miss は意図したノードが干上がっていて 別のノードで代わりに割り当てられた 回数です。miss が hit に対して無視できない比率で育っているなら、先ほどの 2 番目の場面 (ノードの偏り) が進行中という意味です。プロセス単位では numastat -p <PID> で、どのノードにメモリがどれだけあるかを見られます。
配置を直接指定するときは numactl を使います。
# プロセスをノード 0 のコアとメモリに束ねて実行
numactl --cpunodebind=0 --membind=0 ./my-server
# メモリを全ノードに均等分散 (インターリーブ) して実行
numactl --interleave=all ./my-databaseこの 2 つのオプションが正反対の戦略だという点が、NUMA 対処の要点です。ノード 1 個に収まるワークロードは束ねてすべてローカルにし、1 ノードより大きいワークロードはインターリーブで均等に広げて、「半分だけ妙に遅い」状況を避けます。実際、一部のデータベースの運用ガイドがインターリーブ実行を推奨しているのはこの論理です。
仮想化とクラウドでは #
仮想マシンにも同じ構造が透けて見えます。ハイパーバイザーは vCPU とゲストメモリを同じ物理ノードに置こうと努力しますが、大きい仮想マシンは物理サーバーと同じように複数のノードにまたがります。このときゲストに仮想 NUMA トポロジを見せて、ゲストカーネルが知ったうえで動けるようにするのが一般的な設計です。
クラウド利用者の観点での含意は単純です。小さいインスタンスで NUMA に出会うことはほぼなく、物理サーバー 1 台に近い大きいインスタンス (数十 vCPU 以上) では、ゲストの中で numactl --hardware を打つとノードが複数見えることがあります。そのサイズのインスタンスで性能を絞り出す必要があるなら、この記事の道具はクラウドの中でもそのまま通用します。
よく出会う落とし穴 #
- メモリ総量だけを見て配置を判断する — 合計 512GB の余裕は「どこでも 512GB」ではありません。ノード別の残量を見て初めて偏りが見えます。
- とにかく束ねるのが良いと信じる — ピンニングと membind は、ノード 1 個に収まる場合の戦略です。ノードより大きいワークロードを束ねると、そのノードだけが干上がってスワップを呼びます。サイズによって束ねるかインターリーブかを使い分けます。
- NUMA をサーバー専用の知識にしておく — 大きいインスタンスやベアメタル、そして第8回の GPU サーバー (GPU がどのノードにつながっているか) まで、クラウド時代にも NUMA はついてきます。
まとめ #
今回つかんだ絵です。
- マルチソケットサーバーのメモリはノード単位に分かれ、ローカルとリモートのアクセス速度が違います。
- 典型的なインシデントは、ノードより大きいプロセス、ノードの偏り (全体は余っているのにスワップ)、スレッドの移動です。
- numastat の miss 比率で診断し、numactl で束ねるか (小さいワークロード)、インターリーブします (大きいワークロード)。
- 大きい仮想マシンやクラウドインスタンスにも同じ構造が現れます。
次回 — ストレージ性能の実測 #
次回の「ハードウェア中級 #5 ストレージ性能の実測 — fio・キュー深度・SSD の内部事情」では、3 番目のリソースへ進みます。基礎 #4 で IOPS とレイテンシの概念をつかんだなら、今回は fio で実際に測り、キュー深度が数字をどう変えるのか、SSD 内部の事情 (書き込み増幅と TRIM) がなぜ昨日と今日の性能を変えるのかを扱います。