なぜデフォルトのLinuxスケジューラが常に最適とは限らないのか
Linuxスケジューラは工学の傑作です。ワークロードの99%において、特定のCPUがボトルネックにならないようタスクを全コアに分散させる素晴らしい働きをします。しかし、高頻度取引(HFT)プラットフォームや低レイテンシのゲームサーバー、あるいは大規模なPostgreSQLデータベースを運用している場合、この自動バランシングが逆に仇となることがあります。
問題はコンテキストスイッチです。カーネルがプロセスをコア0からコア1に移動させると、そのプロセスは「ウォーム」なL1およびL2キャッシュデータを残したまま移動してしまいます。そのデータをL3キャッシュやシステムRAMから再度取得するには、約50〜200ナノ秒のレイテンシが発生します。わずかな時間に思えるかもしれませんが、これらのマイクロストール(微細な停滞)はすぐに蓄積されます。CPU Pinning(CPUの固定)を実装し、NUMA(Non-Uniform Memory Access)境界を考慮することで、重要なアプリケーションを特定のハードウェアスレッドにロックできます。
クイックスタート:5分でプロセスを固定する
CPUアフィニティ(親和性)を管理するための最も簡単なツールは taskset です。これは標準の util-linux パッケージに含まれており、ほぼすべての主要なディストリビューションで動作します。
現在のアフィニティを確認する
実行中のプロセスが現在使用を許可されているコアを確認するには、そのPIDを取得して次を実行します。
taskset -p 1234
出力は通常、16進数のマスク形式です。例えば、4コアのシステムで f と表示されれば、そのプロセスはどのコアでも実行可能であることを意味します。
特定コアで新しいプロセスを起動する
大量のデータ取り込みを行うPythonスクリプトがあるとします。これをコア0と1に制限するには、 -c (cpu-list) フラグを使用します。
taskset -c 0,1 python3 ingest_data.py
実行中のプロセスのアフィニティを変更する
すでにデータベースに負荷がかかっており、他のタスクのためにスペースを空ける目的でデータベースをコア2と3に移動したい場合は、次のようにします。
# PIDが5678であると仮定
taskset -cp 2,3 5678
NUMA要因:なぜキャッシュの局所性が重要なのか
デュアルAMD EPYCやIntel Xeonを搭載した最新のマルチソケットサーバーは、NUMAアーキテクチャを採用しています。これらのシステムでは、メモリは単一の均一なプールではありません。代わりに、各CPUソケットが独自のローカルRAMを持っています。CPU 0がCPU 1に接続されたメモリにアクセスすることは技術的に可能ですが、そのためにはインターコネクト(AMDのInfinity Fabricなど)を経由する必要があり、大幅なパフォーマンスの低下を招きます。
ハードウェアトポロジの可視化
固定を行う前に、どのコアがどのRAMバンクに属しているかを特定する必要があります。 lscpu を実行し、NUMAセクションを確認してください。
lscpu | grep -i numa
以下のような出力が表示されるはずです。
NUMA node0 CPU(s): 0-7,16-23
NUMA node1 CPU(s): 8-15,24-31
アプリケーションをコア0に固定しても、そのデータがノード1のメモリにある場合、スループットは大幅に低下します。これが numactl が不可欠である理由です。
メモリとCPUをまとめてバインドする
numactl ツールを使用すると、プロセスが計算とメモリの両方で同じノードに留まるように設定できます。
# CPUとRAMの両方でNUMAノード0を使用してアプリを実行する
numactl --cpunodebind=0 --membind=0 ./my_high_perf_app
高度な分離:専用のCpusetを作成する
taskset はクイックな修正には適していますが、 cpuset (cgroups経由)を使用すると、プロのようにサーバーをパーティショニングできます。特定のコアを「囲い込み」、OSが一般的なシステムタスクに使用しないようにすることで、そのコアを完全にアプリケーション専用に割り当てることが可能です。
ステップ1:cgroupのセットアップ
cgroup v2を使用しているシステムでは、優先度の高いアプリ専用のグループを作成できます。まず、コントローラーを有効にします。
mkdir /sys/fs/cgroup/production_app
echo "+cpuset" > /sys/fs/cgroup/cgroup.subtree_control
ステップ2:ハードウェアの予約
次に、このグループがアクセスを許可されるCPUとメモリノードを定義します。
echo "2-3" > /sys/fs/cgroup/production_app/cpuset.cpus
echo "0" > /sys/fs/cgroup/production_app/cpuset.mems
ステップ3:プロセスの移動
プロセスIDを cgroup.procs ファイルに書き込むだけで、そのプロセスを分離された環境に移動できます。
echo 1234 > /sys/fs/cgroup/production_app/cgroup.procs
systemdで固定を永続化する
手動コマンドはテストには適していますが、本番環境のサービスではアフィニティを構成に組み込むべきです。これは、systemdのユニットファイルの [Service] セクションで直接設定できます。
サービスファイル(例: /etc/systemd/system/redis.service )を開き、以下の行を追加します。
[Service]
ExecStart=/usr/bin/redis-server
CPUAffinity=0 1
NUMAPolicy=bind
NUMAMask=0
systemctl daemon-reload で変更を適用し、サービスを再起動します。これにより、再起動後もサービスが指定されたコアに戻ることが保証されます。
現場からの教訓:ベストプラクティス
長年、高トラフィックのKubernetesクラスターやベアメタルのデータベースノードを管理してきた経験から、Pinningは諸刃の剣であると感じています。熱中しすぎて、ネットワーク割り込みやディスクI/Oに必要なリソースをOSから誤って奪ってしまうことがよくあります。
1. NUMA Missを監視する
パフォーマンスの監視には numastat -p <PID> を使用してください。 numa_miss カウンタが増加している場合、アプリケーションがリモートのRAMバンクからデータを取得するためにマザーボードを跨いでアクセスしていることを意味します。これは、Pinning戦略がハードウェア構成と一致していない明確な兆候です。
2. 究極の分離:isolcpus
1ミリ秒の干渉も許容できないプロセスがある場合は、 isolcpus カーネルパラメータを使用します。 /etc/default/grub を編集して isolcpus=2,3 を追加することで、Linuxカーネルに対し、デフォルトでそれらのコアに何もスケジュールしないよう指示できます。これらのコアは、 taskset を使用して手動でプロセスを割り当てるまでアイドル状態を維持します。
3. ハイパースレッディングに注意
コア0とコア1が、同じ物理コアを共有する2つの論理スレッドである可能性があることを忘れないでください。CPUバウンドなタスクにおいて、2つの重いスレッドを同じ物理コアに固定すると、実行ユニットを奪い合うことになります。物理ハードウェアを共有している論理IDを特定するには、常に /sys/devices/system/cpu/cpu0/topology/thread_siblings_list を確認してください。

