【実務・中級編】 NUMA(Non-Uniform Memory Access)アーキテクチャと仮想マシン(vCPU/メモリ)配置のパフォーマンス最適化 – クラウドインフラと仮想化ネットワーク実践ガイド

なぜ「速い」はずの仮想マシンが時折ラグるのか? 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ノードは、正しく整列していますか?

コメント

タイトルとURLをコピーしました