なぜ「速い」はずの仮想マシンが時折ラグるのか? NUMAトポロジと格闘するSREの現場
クラウドの管理画面でスペックを盛ればパフォーマンスが向上する――。そう信じていたジュニア時代の自分に会えるなら、「おい、メモリの物理配置を気にしろ」と耳打ちしてやりたい。
Web APIのレスポンスタイムが特定の時間帯だけスパイクする、あるいは高負荷時にCPU使用率が飽和していないのに処理が詰まる。こうした「不可解な遅延」の正体は、多くの場合、ハイパーバイザーレベルでのメモリ・CPUのミスマッチ、すなわち NUMA(Non-Uniform Memory Access)アフィニティの不整合にあります。
今回は、マルチソケットサーバーの性能を極限まで引き出すための、NUMA設計の深淵を覗いていきましょう。
—
NUMAアーキテクチャ:なぜ「距離」が命取りになるのか
現代のサーバーCPU(Intel XeonやAMD EPYC)は、複数の物理コアをひとつのパッケージに詰め込んだ「マルチソケット」構成が主流です。ここで重要になるのがNUMAという概念です。
NUMA環境では、CPUコアから見て「自分の直近にあるメモリ(ローカル)」と「隣のソケットを経由しなければならないメモリ(リモート)」が存在します。リモートメモリへのアクセスは、バスを経由するオーバーヘッドが発生し、レイテンシが跳ね上がります。
仮想化環境において、vCPUとメモリが物理的に異なるNUMAノードに配置されてしまうと、その仮想マシンは常に「遠くのメモリ」を読み書きするハンデを背負うことになります。これが、アプリケーションのパフォーマンスを微細に、しかし確実に蝕むのです。
パフォーマンスを最大化する「NUMAアフィニティ」の基本原則
1. vCPUは物理コアのNUMA境界をまたがない: 1つの仮想マシンが複数のNUMAノードにまたがる構成は、可能な限り避ける。
2. メモリはvCPUと同じNUMAノード内に確保する: これが「NUMAアフィニティ」の核心です。
—
実践:Linux環境でのNUMAトポロジ確認と配置
まずは、自分のサーバーが今どのようなトポロジになっているかを確認しましょう。lscpuコマンドは、我々SREにとっての地図です。
# 現在のNUMAノード構成とCPUの関係を確認
lscpu | grep -E 'NUMA|CPU(s)'
# より詳細な物理配置を確認するコマンド
numactl --hardware
numactl --hardware を叩くと、各ノードに紐づくCPUとメモリの容量が表示されます。ここで「Node 0に紐づくメモリが足りないから、Node 1のメモリを借りている」といった状況があれば、それがレイテンシの発生源です。
—
設定例:KVM/QEMUにおけるCPU pinningとNUMA配置
仮想マシン(VM)を定義する際、XML(virsh等の設定)で明示的にアフィニティを指定します。以下は、VMが「NUMAノード0」に完璧に居座るための設定例です。
<!-- 仮想マシンの設定ファイル例 -->
<cputune>
<!-- vCPU 0,1を物理コア 0,1 に固定(CPU pinning) -->
<vcpupin vcpu='0' cpuset='0'/>
<vcpupin vcpu='1' cpuset='1'/>
<!-- メモリの配置をNUMAノード0に強制(Strictモード) -->
<numatune>
<memory mode='strict' nodeset='0'/>
</numatune>
</cputune>
ここで重要なのが mode='strict' です。デフォルトの interleave や preferred では、物理メモリの空きが足りないときに別ノードへ平気で溢れさせます。「遅くなるくらいなら、メモリ割り当てに失敗して起動しない(またはエラーを出す)ほうがマシ」というSREの矜持を、この設定に込めるのです。
—
アプリケーション側からレイテンシを観測する
インフラの設定が効いているか、アプリケーション側からマイクロベンチマークで確認してみましょう。Pythonでメモリへのアクセスレイテンシを簡易計測するコードです。
import time
import numpy as np
# 大容量の配列を確保し、アクセス時間を計測
def benchmark_memory_access():
size = 10**8
data = np.zeros(size) # メモリ確保
start_time = time.time()
# 逐次アクセスしてレイテンシを確認
for i in range(size):
_ = data[i]
end_time = time.time()
print(f"アクセス完了時間: {end_time - start_time:.4f}秒")
# 実行
if __name__ == "__main__":
benchmark_memory_access()
このコードを、NUMA設定をしたVMと、そうでないVMで実行してみてください。数ミリ秒の差かもしれませんが、数千万リクエストを捌くWeb APIにおいては、この「チリツモ」がCPUのWait状態を減らし、スループットを劇的に改善します。
—
トラブルシューティングの鉄則
最後に、現場で「遅い」と報告を受けた時のチェックリストを共有します。
1. numastat -p <PID> で確認せよ: 特定のプロセスが numa_miss(割り当て失敗)を起こしていないか確認してください。ここが0でなければ、設定は無効です。
2. CPU Pinningの重複を疑え: 他のVMと物理コアが競合していないか。top コマンドで 1 キーを押して、全CPUの負荷を確認し、特定のコアだけが突き抜けていないか見てください。
3. OSレベルの割り込み(IRQ): 高負荷なNICの割り込み処理が、特定のNUMAノードに偏っていないか。cat /proc/interrupts で確認し、必要であれば smp_affinity を調整しましょう。
クラウドという「見えない箱」の中身を理解することは、エンジニアとしての基礎体力です。APIの設計も大切ですが、そのAPIを支える「土台の物理層」に思いを馳せることこそ、真のSREへの第一歩だと私は信じています。
さあ、皆さんの環境のNUMAノードは、正しく整列していますか?
コメント