「なぜか遅い」を撲滅する。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をもう一段上のレベルへ引き上げるヒントになるはずです。
コメント