【実務・中級編】 CPUピンニング(CPU Pinning)によるパフォーマンス最適化とNUMAトポロジー – クラウドインフラと仮想化ネットワーク実践ガイド

「なぜか遅い」を撲滅する。CPUピンニングとNUMAトポロジーで極限の低レイテンシを叩き出す

「APIのレスポンスタイムが、負荷時にだけ不規則に揺らぐ」。
そんな悪夢のような相談を現場で受けたとき、多くのエンジニアはまずRedisのコネクションプールやDBのインデックスを疑います。しかし、インフラの深層、OSカーネルのさらに下――仮想化層まで潜り込むと、そこには「CPUが物理的にどこで仕事をしているか」という、あまりに泥臭く、しかし決定的な真実が隠れていることがあります。

今回は、仮想化環境における「CPUピンニング」と「NUMAトポロジー」という、パフォーマンスチューニングの最終兵器について解説します。

—

1. 仮想化の「見えない壁」:NUMAの正体

現代のサーバーCPUは、1つのチップの中に複数のダイが詰め込まれている「マルチソケット」あるいは「マルチダイ」構成が当たり前です。ここで重要になるのが NUMA (Non-Uniform Memory Access) という概念です。

簡単に言えば、「CPUに近いメモリ(ローカル)と、遠いメモリ(リモート)がある」という物理的な構造です。もし、CPUが隣のソケットにあるメモリにアクセスしようとすれば、バスを跨ぐためのオーバーヘッド(レイテンシ)が発生します。

仮想マシン(VM)やコンテナが、このNUMAノードを跨いで動き回るとどうなるか? CPUのL3キャッシュは無効化され、メモリバスの競合が発生し、結果としてAPIの処理時間がマイクロ秒単位でジッター(揺らぎ)を起こします。

2. CPUピンニング:仮想と物理の「絆」を固定する

CPUピンニング(CPU Pinning) とは、特定のvCPUを特定の物理コア(pCPU)に固定する技術です。

これを怠ると、スケジューラは「空いているCPU」に処理を割り振ろうとしますが、そのたびにコンテキストスイッチが発生し、キャッシュデータが吹き飛びます。ピンニングを行うことで、vCPUは常に特定の物理コア上で動作し、L1/L2キャッシュを最大限に活用できるようになります。

実践:KVM/Libvirtでの設定例

KVM上で特定のVMを最適化する場合、XML定義ファイル(virsh edit <domain>)で以下のように設定します。

<cputune>
  <!-- vCPU 0を物理コア 0と4にピン留め -->
  <vcpupin vcpu='0' cpuset='0,4'/>
  <!-- vCPU 1を物理コア 1と5にピン留め -->
  <vcpupin vcpu='1' cpuset='1,5'/>
  
  <!-- メモリのNUMAノードをノード0に固定する -->
  <numatune>
    <memory mode='strict' nodeset='0'/>
  </numatune>
</cputune>

ここで重要なのは、cpusetの選び方です。物理CPUのトポロジーを lscpu -e で確認し、ハイパースレッディングのペアを意識して割り当てるのが鉄則です。

—

3. なぜWeb APIのエンジニアがこれを知るべきか?

「インフラの設定なんてSRE任せでいい」と思っていませんか? しかし、高頻度でリクエストを捌くAPIサーバーを設計する際、ノード間でメモリを共有するアーキテクチャでは、こうしたハードウェアの制約が性能の天井(頭打ち)を決定づけます。

もし、あなたが開発しているサービスで「特定のリクエストだけ異常に遅い」といった現象が起きたら、それはアプリケーションのコードの問題ではなく、「別のVMが同じ物理ホスト上で激しくCPUを使い、あなたのvCPUが別のNUMAノードへ追い出された」という事象かもしれません。

パフォーマンス確認用のPythonスクリプト例

実際に、プロセスがどのCPUコアで動いているかを確認する簡単なスクリプトです。

import os
import psutil

def check_cpu_affinity():
    # 現在のプロセスのPIDを取得
    pid = os.getpid()
    process = psutil.Process(pid)
    
    # プロセスが許可されているCPUコア一覧を取得
    affinity = process.cpu_affinity()
    print(f"プロセス {pid} は現在以下のCPUコアで動作可能です: {affinity}")

if __name__ == "__main__":
    check_cpu_affinity()
    # 実際にはここでリクエスト処理を行う

—

4. 現場のトラブルシューティング:チェックリスト

もしあなたが運用現場でパフォーマンス問題に直面したら、まず以下の手順で「物理層」を疑ってみてください。

1. lscpu でNUMAを確認: 物理サーバーがそもそも何個のNUMAノードを持っているか把握する。
2. numastat -p <PID> で確認: プロセスが「ローカルメモリ」で動いているか、「リモートメモリ(misses)」を多用しているかを確認する。
3. CPUの親和性(Affinity)を見る: taskset -cp <PID> コマンドで、プロセスが不自然に広いコア範囲を掴んでいないかを確認する。

最後に:トレードオフを理解する

CPUピンニングは諸刃の剣です。特定のコアに固定するということは、「そのコアが故障や高負荷に陥った際に、他のコアへ逃げることができない」ことを意味します。

クラウド環境(AWSの Dedicated Host や GCPの Sole-tenant node など)でこれを行う際は、冗長化設計とセットであることが大前提です。物理の制約を理解した上で、いかに論理的なリソースを効率的に配置するか。これこそが、大規模システムを支えるSREの腕の見せ所なのです。

次に皆さんがコードを書くとき、背景で動いている「シリコンの鼓動」を少しだけ想像してみてください。それが、あなたのAPIをもう一段上のレベルへ引き上げるヒントになるはずです。

コメント

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