Linux Kernel NUMA and SMP

Linux Kernel NUMA & SMP 徹底解説


目次

  1. はじめに
  2. SMP (Symmetric Multi-Processing) アーキテクチャ
  3. UMA と NUMA メモリモデル
  4. NUMA アーキテクチャの詳細
  5. NUMA トポロジ: ノード、ゾーン、距離
  6. CPU トポロジ: ソケット、コア、スレッド
  7. NUMA 対応メモリ割り当て
  8. NUMA メモリポリシー
  9. set_mempolicy, mbind, numactl
  10. CPU ホットプラグ
  11. スケジューラドメインとグループ
  12. NUMA バランシング (自動 NUMA マイグレーション)
  13. Per-CPU データと Per-NUMA ノード割り当て
  14. キャッシュコヒーレンシプロトコル (MESI, MOESI)
  15. メモリインターコネクト (QPI, UPI, Infinity Fabric)
  16. False Sharing とキャッシュラインバウンシング
  17. irqbalance と NUMA
  18. NUMA パフォーマンスの影響と最適化
  19. 仮想化における NUMA (KVM, QEMU)
  20. データベースの実用的 NUMA チューニング (MySQL, PostgreSQL)
  21. ツール: numactl, numastat, lstopo, hwloc, lscpu, turbostat
  22. トラブルシューティングガイド
  23. まとめとベストプラクティス
  24. 参考文献

1. はじめに

1.1 本書の目的

現代のサーバーシステムは、数十から数百のCPUコアと数百GBから数TBのメモリを搭載している。これらのリソースを効率的に利用するには、SMP (Symmetric Multi-Processing) と NUMA (Non-Uniform Memory Access) アーキテクチャに関する深い理解が不可欠である。

本書では、Linux カーネルがマルチプロセッサシステムをどのように管理しているかを、アーキテクチャレベルから実践的なチューニングまで包括的に解説する。

1.2 対象読者

  • Linux システム管理者・SRE エンジニア
  • パフォーマンスエンジニア
  • カーネル開発者・デバイスドライバ開発者
  • データベース管理者
  • 仮想化基盤エンジニア

1.3 前提知識

  • Linux の基本的な操作
  • C言語の基礎知識
  • コンピュータアーキテクチャの基本概念

1.4 動作環境

# 検証環境の例
$ uname -r
6.8.0-45-generic

$ lscpu | head -20
Architecture:            x86_64
CPU op-mode(s):          32-bit, 64-bit
Address sizes:           46 bits physical, 57 bits virtual
Byte Order:              Little Endian
CPU(s):                  128
On-line CPU(s) list:     0-127
Vendor ID:               GenuineIntel
Model name:              Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz
CPU family:              6
Stepping:                6
CPU MHz:                 2300.000
CPU max MHz:             3400.0000
BogoMIPS:                4600.00
L1d cache:               3 MiB (64 instances)
L1i cache:               2 MiB (64 instances)
L2 cache:                80 MiB (64 instances)
L3 cache:                120 MiB (2 instances)
NUMA node(s):            2
NUMA node0 CPU(s):       0-31,64-95
NUMA node1 CPU(s):       32-63,96-127

2. SMP (Symmetric Multi-Processing) アーキテクチャ

2.1 SMP の概要

SMP (Symmetric Multi-Processing / 対称型マルチプロセッシング) は、複数のプロセッサが単一の共有メモリに対して対等にアクセスできるアーキテクチャである。「対称」とは、すべてのプロセッサが同等の権限でメモリや I/O デバイスにアクセスでき、どのプロセッサでも任意のタスクを実行できることを意味する。

SMP アーキテクチャの概略図:

    CPU 0     CPU 1     CPU 2     CPU 3
      |         |         |         |
    [L1]      [L1]      [L1]      [L1]
      |         |         |         |
    [L2]      [L2]      [L2]      [L2]
      |         |         |         |
      +----+----+----+----+----+----+
           |              |
         [共有 L3 キャッシュ]
           |
    [メモリコントローラ]
           |
    [   共有メモリ   ]

2.2 SMP の歴史と Linux での実装

Linux カーネルにおける SMP サポートの歴史は以下の通りである:

カーネルバージョンSMP 関連の変更
Linux 2.0 (1996)最初の SMP サポート。Big Kernel Lock (BKL) による粗粒度ロック
Linux 2.2 (1999)改善された SMP サポート、一部のサブシステムで細粒度ロック
Linux 2.4 (2001)BKL の適用範囲縮小、I/O サブシステムの並列化
Linux 2.6 (2003)O(1) スケジューラ、プリエンプティブカーネル、RCU 導入
Linux 2.6.23 (2007)CFS (Completely Fair Scheduler) 導入
Linux 3.8 (2013)Full tickless (NO_HZ_FULL) サポート
Linux 6.6 (2023)EEVDF スケジューラ導入

2.3 SMP カーネルの起動プロセス

SMP システムでは、BSP (Bootstrap Processor) が最初に起動し、AP (Application Processor) を順次起動する:

/*
 * Linux カーネルにおける SMP 起動の概要
 * arch/x86/kernel/smpboot.c
 */

/* BSP が AP を起動する流れ */
void __init smp_init(void)
{
    /* 各 AP に対して起動シーケンスを実行 */
    for_each_present_cpu(cpu) {
        if (cpu == 0)  /* BSP 自身はスキップ */
            continue;
        cpu_up(cpu);   /* AP を起動 */
    }
}

/*
 * AP 起動の詳細:
 * 1. INIT IPI (Inter-Processor Interrupt) を送信
 * 2. SIPI (Startup IPI) を送信
 * 3. AP がリアルモードで起動
 * 4. AP がプロテクトモード → ロングモードへ遷移
 * 5. AP が idle ループに入る
 */

2.4 SMP のメリットとデメリット

メリット:

  • プログラミングモデルがシンプル(全CPUが同じメモリを共有)
  • 負荷分散が容易
  • プロセスマイグレーションのコストが比較的低い

デメリット:

  • メモリバス帯域幅がボトルネックになりやすい
  • CPU数が増えるとメモリアクセスの競合が増大
  • キャッシュコヒーレンシのオーバーヘッドがスケーラビリティを制限
  • 一般に 8 ソケット程度が実用的な上限

2.5 /proc と /sys による SMP 情報の確認

# CPU 数の確認
$ nproc
128

# CPU のオンライン状態
$ cat /sys/devices/system/cpu/online
0-127

# CPU の詳細情報
$ cat /proc/cpuinfo | grep "processor" | wc -l
128

# CPU ごとのキャッシュ情報
$ ls /sys/devices/system/cpu/cpu0/cache/
index0  index1  index2  index3

$ cat /sys/devices/system/cpu/cpu0/cache/index0/type
Data
$ cat /sys/devices/system/cpu/cpu0/cache/index0/size
48K
$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64

# SMP 関連のカーネルパラメータ
$ sysctl -a 2>/dev/null | grep -E "sched_(min_granularity|migration_cost|nr_migrate)"
kernel.sched_min_granularity_ns = 3000000
kernel.sched_migration_cost_ns = 500000
kernel.sched_nr_migrate = 32

2.6 SMP カーネルコンフィグレーション

# カーネルの SMP 設定確認
$ zcat /proc/config.gz | grep -i smp
CONFIG_SMP=y
CONFIG_NR_CPUS=8192
CONFIG_SCHED_SMT=y
CONFIG_SCHED_MC=y

# 最大 CPU 数の確認
$ cat /sys/devices/system/cpu/possible
0-8191

2.7 SMP における同期プリミティブ

SMP 環境では、共有データへの同時アクセスを正しく制御する必要がある:

/* スピンロック - SMP で最も基本的なロック機構 */
#include <linux/spinlock.h>

static DEFINE_SPINLOCK(my_lock);

void smp_safe_function(void)
{
    unsigned long flags;
    
    spin_lock_irqsave(&my_lock, flags);
    /* クリティカルセクション */
    /* 共有データへのアクセス */
    spin_unlock_irqrestore(&my_lock, flags);
}

/* Per-CPU 変数 - ロック不要で SMP セーフ */
#include <linux/percpu.h>

static DEFINE_PER_CPU(unsigned long, my_counter);

void increment_counter(void)
{
    /* プリエンプション無効化で保護 */
    preempt_disable();
    this_cpu_inc(my_counter);
    preempt_enable();
}

/* RCU (Read-Copy-Update) - 読み取り側のオーバーヘッドが極めて低い */
#include <linux/rcupdate.h>

struct my_data {
    int value;
    struct rcu_head rcu;
};

static struct my_data __rcu *global_data;

int read_data(void)
{
    struct my_data *p;
    int val;
    
    rcu_read_lock();
    p = rcu_dereference(global_data);
    val = p->value;
    rcu_read_unlock();
    
    return val;
}

3. UMA と NUMA メモリモデル

3.1 UMA (Uniform Memory Access) モデル

UMA は、すべてのプロセッサからメモリへのアクセス遅延が均一なアーキテクチャである。従来の SMP システムは UMA モデルを採用していた。

UMA アーキテクチャ:

  CPU 0   CPU 1   CPU 2   CPU 3
    |       |       |       |
    +---+---+---+---+---+---+
        |       |       |
    [  共有バス / クロスバー  ]
        |
    [メモリコントローラ]
        |
    +---+---+---+---+
    |   |   |   |   |
   DIMM DIMM DIMM DIMM
   
   全CPUからの遅延: 均一 (~80ns)

UMA の特徴:

  • すべてのメモリアクセスが同じ遅延
  • プログラミングが容易
  • スケーラビリティに限界(通常 2-4 ソケット)
  • メモリバス帯域幅がボトルネック

3.2 NUMA (Non-Uniform Memory Access) モデル

NUMA は、各プロセッサ(またはプロセッサグループ)が「ローカルメモリ」を持ち、他のプロセッサのメモリにも「リモートアクセス」可能なアーキテクチャである。

NUMA アーキテクチャ (2 ノード):

    NUMA Node 0                    NUMA Node 1
  +------------------+          +------------------+
  | CPU 0  CPU 1     |          | CPU 4  CPU 5     |
  | CPU 2  CPU 3     |          | CPU 6  CPU 7     |
  |       |          |          |       |          |
  | [メモリコントローラ] |   QPI/UPI   | [メモリコントローラ] |
  |       |          |<-------->|       |          |
  | +---+---+---+    |          | +---+---+---+    |
  | |DDR|DDR|DDR|    |          | |DDR|DDR|DDR|    |
  | +---+---+---+    |          | +---+---+---+    |
  | ローカルメモリ 64GB |          | ローカルメモリ 64GB |
  +------------------+          +------------------+

  ローカルアクセス遅延: ~80ns     リモートアクセス遅延: ~130ns
  NUMA ratio = 130/80 = 1.625

3.3 UMA vs NUMA の詳細比較

特性UMANUMA
メモリアクセス遅延均一不均一(ローカル < リモート)
スケーラビリティ低 (2-4 ソケット)高 (2-8+ ソケット)
メモリ帯域幅共有(制限あり)分散(スケーラブル)
プログラミング複雑性中〜高
典型的な遅延比率1.01.2〜2.0+
最適化の必要性
現在の主流デスクトップ/小規模サーバー/HPC

3.4 NUMA ratio の理解

NUMA ratio は、リモートメモリアクセスとローカルメモリアクセスの遅延比率を表す:

NUMA ratio = リモートアクセス遅延 / ローカルアクセス遅延
# NUMA 距離マトリックスから NUMA ratio を推定
$ numactl --hardware
available: 4 nodes (0-3)
node 0 cpus: 0 1 2 3 4 5 6 7 64 65 66 67 68 69 70 71
node 0 size: 128832 MB
node 0 free: 115234 MB
node 1 cpus: 8 9 10 11 12 13 14 15 72 73 74 75 76 77 78 79
node 1 size: 129024 MB
node 1 free: 120456 MB
node 2 cpus: 16 17 18 19 20 21 22 23 80 81 82 83 84 85 86 87
node 2 size: 129024 MB
node 2 free: 118901 MB
node 3 cpus: 24 25 26 27 28 29 30 31 88 89 90 91 92 93 94 95
node 3 size: 129022 MB
node 3 free: 121345 MB
node distances:
node   0   1   2   3
  0:  10  12  20  22
  1:  12  10  22  20
  2:  20  22  10  12
  3:  22  20  12  10

上記の例では:

  • 同一ノード内: 距離 10 (基準)
  • 隣接ノード間: 距離 12 (NUMA ratio = 1.2)
  • 対角ノード間: 距離 20-22 (NUMA ratio = 2.0-2.2)

3.5 NUMA が必要となった背景

メモリバス帯域幅の壁:

CPU数     メモリ要求帯域    UMA バス帯域    NUMA ローカル帯域合計
  1        25 GB/s          100 GB/s        25 GB/s
  4       100 GB/s          100 GB/s       100 GB/s
  8       200 GB/s          100 GB/s ←限界  200 GB/s
 16       400 GB/s          100 GB/s       400 GB/s
 32       800 GB/s          100 GB/s       800 GB/s

→ CPU 数が増えると UMA ではバス帯域が飽和する
→ NUMA では各ノードが独立したメモリバスを持つため、
   ローカルアクセスが支配的であればスケールする

3.6 NUMA のメモリアクセスパターン計測

/*
 * NUMA ローカル vs リモートメモリアクセス遅延の計測プログラム
 * コンパイル: gcc -O2 -o numa_latency numa_latency.c -lnuma
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <numa.h>
#include <numaif.h>
#include <sched.h>
#include <time.h>

#define ARRAY_SIZE (64 * 1024 * 1024)  /* 64 MB */
#define ITERATIONS 1000000
#define STRIDE 64  /* キャッシュライン単位 */

static inline unsigned long long rdtsc(void)
{
    unsigned int lo, hi;
    __asm__ __volatile__("rdtsc" : "=a"(lo), "=d"(hi));
    return ((unsigned long long)hi << 32) | lo;
}

void measure_latency(int cpu_node, int mem_node)
{
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    
    /* 指定ノードの最初の CPU にバインド */
    struct bitmask *cpumask = numa_allocate_cpumask();
    numa_node_to_cpus(cpu_node, cpumask);
    int first_cpu = -1;
    for (int i = 0; i < numa_num_configured_cpus(); i++) {
        if (numa_bitmask_isbitset(cpumask, i)) {
            first_cpu = i;
            break;
        }
    }
    CPU_SET(first_cpu, &cpuset);
    sched_setaffinity(0, sizeof(cpuset), &cpuset);
    
    /* 指定ノードにメモリを割り当て */
    char *buffer = numa_alloc_onnode(ARRAY_SIZE, mem_node);
    if (!buffer) {
        fprintf(stderr, "Failed to allocate on node %d\n", mem_node);
        return;
    }
    
    /* メモリを初期化(実際にページを確保) */
    memset(buffer, 0, ARRAY_SIZE);
    
    /* ランダムアクセスパターンでポインタチェーンを構築 */
    volatile char *p = buffer;
    unsigned long long start, end;
    unsigned long sum = 0;
    
    start = rdtsc();
    for (int i = 0; i < ITERATIONS; i++) {
        sum += *(volatile char *)(buffer + 
            ((unsigned long)(i * 997) % (ARRAY_SIZE / STRIDE)) * STRIDE);
    }
    end = rdtsc();
    
    double cycles = (double)(end - start) / ITERATIONS;
    printf("CPU Node %d -> Mem Node %d: %.1f cycles/access "
           "(sum=%lu to prevent optimization)\n",
           cpu_node, mem_node, cycles, sum);
    
    numa_free(buffer, ARRAY_SIZE);
    numa_free_cpumask(cpumask);
}

int main(void)
{
    if (numa_available() < 0) {
        fprintf(stderr, "NUMA is not available\n");
        return 1;
    }
    
    int num_nodes = numa_max_node() + 1;
    printf("NUMA nodes: %d\n\n", num_nodes);
    
    for (int cpu = 0; cpu < num_nodes; cpu++) {
        for (int mem = 0; mem < num_nodes; mem++) {
            measure_latency(cpu, mem);
        }
        printf("\n");
    }
    
    return 0;
}

4. NUMA アーキテクチャの詳細

4.1 NUMA の基本構成要素

NUMA アーキテクチャは以下の要素で構成される:

NUMA システムの構成要素:

1. NUMA ノード (Node)
   - 1つ以上のプロセッサソケット
   - ローカルメモリ
   - ローカル I/O デバイス

2. インターコネクト (Interconnect)
   - Intel QPI/UPI
   - AMD HyperTransport/Infinity Fabric
   - ノード間通信を提供

3. メモリコントローラ
   - 各ノードに統合
   - ローカルメモリへのアクセスを管理

4. キャッシュコヒーレンシプロトコル
   - ノード間のデータ一貫性を保証
   - スヌーピングまたはディレクトリベース

4.2 Intel マルチソケットの NUMA 構成

Intel 2-Socket Xeon (Ice Lake) の例:

  Socket 0 (NUMA Node 0)              Socket 1 (NUMA Node 1)
  +----------------------------+       +----------------------------+
  | Core 0  Core 1  ... Core 39|       | Core 40 Core 41 ... Core 79|
  |    |       |          |    |       |    |       |          |    |
  |  [  L1/L2 キャッシュ  ]   |       |  [  L1/L2 キャッシュ  ]   |
  |          |                 |       |          |                 |
  |  [ 共有 L3 キャッシュ 60MB ]|       |  [ 共有 L3 キャッシュ 60MB ]|
  |          |                 |       |          |                 |
  |  [ IMC (統合メモリコントローラ) ]|   |  [ IMC (統合メモリコントローラ) ]|
  |    |    |    |    |    |   |       |    |    |    |    |    |   |
  |  Ch0  Ch1  Ch2  Ch3  Ch4  Ch5|    |  Ch0  Ch1  Ch2  Ch3  Ch4  Ch5|
  |   |    |    |    |    |    | |     |   |    |    |    |    |    | |
  | DDR4 DDR4 DDR4 DDR4 DDR4 DDR4|    | DDR4 DDR4 DDR4 DDR4 DDR4 DDR4|
  +------|-----|-----------+----+      +------|-----|-----------+----+
         |     |           |                  |     |           |
         +-----|-----------|------------------+     |           |
               |    UPI (Ultra Path Interconnect)   |           |
               +------------------------------------+           |
                        3 UPI リンク                             |
                        各 20.8 GT/s                            |
  +-------------------+                           +-------------------+
  | PCIe Gen4 レーン   |                           | PCIe Gen4 レーン   |
  | NVMe, NIC, GPU    |                           | NVMe, NIC, GPU    |
  +-------------------+                           +-------------------+

4.3 AMD EPYC の NUMA 構成 (NPS設定)

AMD EPYC プロセッサは、BIOS の NPS (NUMA Nodes Per Socket) 設定により、1ソケット内で複数の NUMA ノードを構成できる:

AMD EPYC 7003 (Milan) - NPS 設定による違い:

NPS1 (1 NUMA Node per Socket):
  Socket 0 = 1 NUMA ノード (全 CCD のメモリを統合)
  
NPS2 (2 NUMA Nodes per Socket):
  Socket 0 = 2 NUMA ノード (CCD を2グループに分割)
  
NPS4 (4 NUMA Nodes per Socket):
  Socket 0 = 4 NUMA ノード (CCD を4グループに分割)

例: 2 Socket EPYC 7763 (64コア/ソケット)

NPS1: 2 NUMA ノード (各 64コア, 各 256GB)
NPS2: 4 NUMA ノード (各 32コア, 各 128GB)
NPS4: 8 NUMA ノード (各 16コア, 各 64GB)
# AMD EPYC NPS 設定の確認
$ dmesg | grep -i "SRAT\|NUMA"
[    0.000000] SRAT: Node 0 PXM 0 [mem 0x00000000-0x7fffffff]
[    0.000000] SRAT: Node 0 PXM 0 [mem 0x100000000-0x207fffffff]
[    0.000000] SRAT: Node 1 PXM 1 [mem 0x2080000000-0x407fffffff]
[    0.000000] NUMA: Node 0 [mem 0x00000000-0x207fffffff] + ...
[    0.000000] NUMA: Initmem setup node 0 [mem 0x00000000-0x207fffffff]

4.4 Linux カーネルの NUMA 初期化

Linux カーネルは起動時に ACPI SRAT (System Resource Affinity Table) と SLIT (System Locality Information Table) を解析して NUMA トポロジを構築する:

/*
 * NUMA 初期化の流れ (x86)
 * arch/x86/mm/numa.c
 */

void __init x86_numa_init(void)
{
    /* 1. ACPI SRAT テーブルの解析 */
    if (!acpi_numa_init())
        return;
    
    /* 2. AMD のトポロジ情報による初期化(フォールバック) */
    if (!amd_numa_init())
        return;
    
    /* 3. フラット(全メモリを1ノード)として初期化 */
    numa_init(dummy_numa_init);
}

/*
 * SRAT テーブルからメモリ親和性情報を取得:
 *   - 各メモリ範囲がどのNUMAノードに属するか
 *   - 各CPUがどのNUMAノードに属するか
 *
 * SLIT テーブルからノード間距離を取得:
 *   - ノード間のアクセスコスト(相対値)
 */
# ACPI SRAT/SLIT テーブルの確認
$ sudo acpidump -b
$ sudo iasl -d srat.dat
$ sudo iasl -d slit.dat

# dmesg で NUMA 初期化ログを確認
$ dmesg | grep -i "numa\|srat\|slit" | head -30
[    0.000000] SRAT: PXM 0 -> APIC 0x00 -> Node 0
[    0.000000] SRAT: PXM 0 -> APIC 0x01 -> Node 0
[    0.000000] SRAT: PXM 1 -> APIC 0x10 -> Node 1
[    0.000000] SRAT: PXM 1 -> APIC 0x11 -> Node 1
[    0.000000] ACPI: SLIT: nodes = 2
[    0.000000] NUMA: Node 0 [mem 0x00000000-0x7fffffff] + [mem 0x100000000-0x87fffffff]
[    0.000000] NUMA: Node 1 [mem 0x880000000-0x107fffffff]

5. NUMA トポロジ: ノード、ゾーン、距離

5.1 NUMA ノード

Linux カーネルでは、NUMA ノードは pglist_data (通称 pg_data_t) 構造体で表現される:

/*
 * include/linux/mmzone.h
 * NUMA ノードを表すデータ構造
 */
typedef struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];    /* ノード内のゾーン */
    struct zonelist node_zonelists[MAX_ZONELISTS]; /* ゾーン検索順序 */
    int nr_zones;                              /* ゾーン数 */
    unsigned long node_start_pfn;              /* ノード開始ページフレーム番号 */
    unsigned long node_present_pages;          /* 存在するページ数 */
    unsigned long node_spanned_pages;          /* アドレス範囲に含まれるページ数 */
    int node_id;                               /* ノードID */
    wait_queue_head_t kswapd_wait;            /* kswapd 待ちキュー */
    struct task_struct *kswapd;                /* kswapd デーモン */
    /* ... */
} pg_data_t;

/* 全 NUMA ノードの配列 */
extern struct pglist_data *node_data[];
#define NODE_DATA(nid) (node_data[nid])

5.2 メモリゾーン

各 NUMA ノード内のメモリは、用途に応じてゾーンに分割される:

NUMA Node 0 のメモリレイアウト (x86_64):

アドレス空間:
  0x00000000 - 0x00FFFFFF  →  ZONE_DMA      (16 MB)
  0x01000000 - 0xFFFFFFFF  →  ZONE_DMA32    (4 GB - 16 MB)
  0x100000000以降           →  ZONE_NORMAL   (残りすべて)

  ZONE_DMA:     レガシー ISA DMA 用 (最初の 16MB)
  ZONE_DMA32:   32ビット DMA 対応デバイス用 (最初の 4GB)
  ZONE_NORMAL:  通常のメモリ割り当て
  ZONE_MOVABLE: ホットプラグ可能なメモリ
  ZONE_DEVICE:  永続メモリ (pmem) 用
# ゾーン情報の確認
$ cat /proc/zoneinfo | head -60
Node 0, zone      DMA
  per-node stats
      nr_inactive_anon 245678
      nr_active_anon 1234567
      nr_inactive_file 567890
      nr_active_file 890123
      nr_unevictable 12345
      nr_slab_reclaimable 78901
      nr_slab_unreclaimable 34567
  pages free     3456
        boost    0
        min      32
        low      40
        high     48
        spanned  4095
        present  3998
        managed  3840
        cma      0
        protection: (0, 3904, 128512, 128512)

Node 0, zone    DMA32
  pages free     567890
        boost    0
        min      4789
        low      5986
        high     7183
        spanned  1044480
        present  782288
        managed  756432

Node 0, zone   Normal
  pages free     23456789
        boost    0
        min      67890
        low      84862
        high     101834
        spanned  31457280
        present  31457280
        managed  30987456

# ノードごとのメモリ統計
$ cat /proc/buddyinfo
Node 0, zone      DMA      1      1      0      1      2      1      1      0      1      1      3
Node 0, zone    DMA32    567    432    321    234    178    123     89     56     34     12      8
Node 0, zone   Normal  12345   9876   7654   5432   4321   3210   2345   1234    567    234     89
Node 1, zone   Normal  11234   8765   6543   4321   3456   2987   2123   1098    456    198     76

5.3 NUMA 距離とノード間トポロジ

NUMA 距離は、ノード間のメモリアクセスコストの相対値を表す。ACPI SLIT テーブルで定義され、10 がローカルアクセスの基準値である。

# NUMA 距離マトリックスの確認
$ cat /sys/devices/system/node/node*/distance
10 12 20 22
12 10 22 20
20 22 10 12
22 20 12 10

# より見やすい形式で表示
$ numactl --hardware | grep -A5 "node distances"
node distances:
node   0   1   2   3
  0:  10  12  20  22
  1:  12  10  22  20
  2:  20  22  10  12
  3:  22  20  12  10
4ノード NUMA トポロジの可視化:

  Node 0 ----[距離 12]---- Node 1
    |                        |
 [距離 20]              [距離 20]
    |                        |
  Node 2 ----[距離 12]---- Node 3

  距離 10: 同一ノード内 (ローカル)
  距離 12: 同一ソケット内の隣接ノード (AMD NPS2 など)
  距離 20: 異なるソケットの対称位置ノード
  距離 22: 異なるソケットの非対称位置ノード

5.4 ゾーンリストとフォールバック

メモリ割り当て時、カーネルはゾーンリストに従ってページを検索する:

/*
 * ゾーンリストのフォールバック順序
 * ローカルノードのゾーンを優先し、距離の近いノードから順に検索
 */

/*
 * NUMA Node 0 のゾーンリスト (ZONELIST_FALLBACK):
 * 
 * 1. Node 0 - ZONE_NORMAL  (ローカル、最優先)
 * 2. Node 0 - ZONE_DMA32
 * 3. Node 0 - ZONE_DMA
 * 4. Node 1 - ZONE_NORMAL  (距離 12、次に優先)
 * 5. Node 1 - ZONE_DMA32
 * 6. Node 2 - ZONE_NORMAL  (距離 20)
 * 7. Node 2 - ZONE_DMA32
 * 8. Node 3 - ZONE_NORMAL  (距離 22)
 * 9. Node 3 - ZONE_DMA32
 */
# ゾーンリストの確認
$ cat /proc/zoneinfo | grep -E "^Node|zone |protection"
Node 0, zone      DMA
        protection: (0, 3904, 128512, 128512)
Node 0, zone    DMA32
        protection: (0, 0, 124608, 124608)
Node 0, zone   Normal
        protection: (0, 0, 0, 0)
Node 1, zone   Normal
        protection: (0, 0, 0, 0)

5.5 カーネルのノードごとの kswapd

各 NUMA ノードには独自の kswapd デーモンが存在し、ローカルメモリの回収を担当する:

# ノードごとの kswapd
$ ps -ef | grep kswapd
root        78     2  0 Jan01 ?        00:00:45 [kswapd0]
root        79     2  0 Jan01 ?        00:00:38 [kswapd1]
root        80     2  0 Jan01 ?        00:00:42 [kswapd2]
root        81     2  0 Jan01 ?        00:00:36 [kswapd3]

# ノードごとのメモリ統計
$ cat /sys/devices/system/node/node0/meminfo
Node 0 MemTotal:       131924992 kB
Node 0 MemFree:        118001664 kB
Node 0 MemUsed:         13923328 kB
Node 0 Active:           8765432 kB
Node 0 Inactive:         3456789 kB
Node 0 Active(anon):     5678901 kB
Node 0 Inactive(anon):   1234567 kB
Node 0 Active(file):     3086531 kB
Node 0 Inactive(file):   2222222 kB
Node 0 Unevictable:       123456 kB
Node 0 Mlocked:            123456 kB
Node 0 Dirty:              45678 kB
Node 0 Writeback:              0 kB
Node 0 FilePages:         5555555 kB
Node 0 Mapped:            1111111 kB
Node 0 AnonPages:         6789012 kB
Node 0 Shmem:              234567 kB
Node 0 KernelStack:         12345 kB
Node 0 SReclaimable:       678901 kB
Node 0 SUnreclaim:         345678 kB
Node 0 AnonHugePages:     2097152 kB
Node 0 ShmemHugePages:          0 kB
Node 0 HugePages_Total:       0
Node 0 HugePages_Free:        0
Node 0 HugePages_Surp:        0

6. CPU トポロジ: ソケット、コア、スレッド

6.1 CPU トポロジの階層構造

CPU トポロジ階層:

マシン
  └── ソケット 0 (物理パッケージ)
  │     └── L3 キャッシュ (共有)
  │           └── コア 0
  │           │     └── L2 キャッシュ
  │           │     │     └── L1d / L1i キャッシュ
  │           │     │           └── スレッド 0 (logical CPU 0)
  │           │     │           └── スレッド 1 (logical CPU 64)  ← HT
  │           │     
  │           └── コア 1
  │           │     └── L2 キャッシュ
  │           │           └── L1d / L1i キャッシュ
  │           │                 └── スレッド 0 (logical CPU 1)
  │           │                 └── スレッド 1 (logical CPU 65)
  │           └── ...
  │           └── コア 31
  │
  └── ソケット 1 (物理パッケージ)
        └── L3 キャッシュ (共有)
              └── コア 32
              │     └── スレッド 0 (logical CPU 32)
              │     └── スレッド 1 (logical CPU 96)
              └── ...

6.2 CPU トポロジの確認方法

# lscpu による詳細表示
$ lscpu
Architecture:            x86_64
CPU(s):                  128
On-line CPU(s) list:     0-127
Thread(s) per core:      2
Core(s) per socket:      32
Socket(s):               2
NUMA node(s):            2
Vendor ID:               GenuineIntel
CPU family:              6
Model:                   106
Model name:              Intel(R) Xeon(R) Platinum 8380 CPU @ 2.30GHz
Stepping:                6
CPU MHz:                 2300.000
CPU max MHz:             3400.0000
CPU min MHz:             800.0000
BogoMIPS:                4600.00
Virtualization:          VT-x
L1d cache:               3 MiB (64 instances)
L1i cache:               2 MiB (64 instances)
L2 cache:                80 MiB (64 instances)
L3 cache:                120 MiB (2 instances)
NUMA node0 CPU(s):       0-31,64-95
NUMA node1 CPU(s):       32-63,96-127

# トポロジの詳細確認
$ lscpu -e
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE MAXMHZ    MINMHZ
  0    0      0    0  0:0:0:0         yes 3400.0000  800.0000
  1    0      0    1  1:1:1:0         yes 3400.0000  800.0000
  2    0      0    2  2:2:2:0         yes 3400.0000  800.0000
...
 32    1      1   32 32:32:32:1       yes 3400.0000  800.0000
 33    1      1   33 33:33:33:1       yes 3400.0000  800.0000
...
 64    0      0    0  0:0:0:0         yes 3400.0000  800.0000
 65    0      0    1  1:1:1:0         yes 3400.0000  800.0000
...
 96    1      1   32 32:32:32:1       yes 3400.0000  800.0000

# sysfs を通じたトポロジ情報
$ cat /sys/devices/system/cpu/cpu0/topology/physical_package_id
0
$ cat /sys/devices/system/cpu/cpu0/topology/core_id
0
$ cat /sys/devices/system/cpu/cpu0/topology/thread_siblings_list
0,64
$ cat /sys/devices/system/cpu/cpu0/topology/core_siblings_list
0-31,64-95

6.3 Hyper-Threading (SMT) の理解

Hyper-Threading (HT) / SMT の仕組み:

物理コア:
  +------------------------------------------+
  | 実行ユニット (ALU, FPU, Load/Store, etc.) |
  |        共有リソース                        |
  +------------------------------------------+
       |                    |
  [スレッド 0 状態]    [スレッド 1 状態]
  - レジスタセット       - レジスタセット
  - APIC                - APIC
  - TLB エントリ        - TLB エントリ
  (= logical CPU 0)    (= logical CPU 64)
  
  HT により 1物理コアが 2 論理CPU として見える
  実効性能は通常 +20〜30% 程度の向上
# HT が有効かどうかの確認
$ cat /sys/devices/system/cpu/smt/active
1

# HT の制御
$ cat /sys/devices/system/cpu/smt/control
on

# HT を無効にする(実行時)
$ echo off > /sys/devices/system/cpu/smt/control

# カーネルブートパラメータで無効にする
# GRUB: nosmt または nosmt=force

# HT ペアの確認
$ cat /sys/devices/system/cpu/cpu0/topology/thread_siblings_list
0,64
# CPU 0 と CPU 64 が同じ物理コアの HT ペア

6.4 hwloc/lstopo による可視化

# hwloc のインストール
$ sudo apt install hwloc   # Ubuntu/Debian
$ sudo yum install hwloc   # RHEL/CentOS

# テキスト形式でトポロジ表示
$ lstopo-no-graphics --of txt
Machine (512GB total)
  NUMANode L#0 (P#0 256GB)
    Package L#0
      L3 L#0 (60MB)
        L2 L#0 (1280KB) + L1d L#0 (48KB) + L1i L#0 (32KB) + Core L#0
          PU L#0 (P#0)
          PU L#1 (P#64)
        L2 L#1 (1280KB) + L1d L#1 (48KB) + L1i L#1 (32KB) + Core L#1
          PU L#2 (P#1)
          PU L#3 (P#65)
        ...
  NUMANode L#1 (P#1 256GB)
    Package L#1
      L3 L#1 (60MB)
        L2 L#32 (1280KB) + L1d L#32 (48KB) + L1i L#32 (32KB) + Core L#32
          PU L#64 (P#32)
          PU L#65 (P#96)
        ...

# 画像として出力
$ lstopo topology.png
$ lstopo topology.svg
$ lstopo topology.pdf

# XML 形式で出力(プログラムからの利用向け)
$ lstopo topology.xml

# hwloc-info でサマリ表示
$ hwloc-info
depth 0:           1 Machine (type #0)
 depth 1:          2 NUMANode (type #13)
  depth 2:         2 Package (type #1)
   depth 3:        2 L3Cache (type #6)
    depth 4:       64 L2Cache (type #5)
     depth 5:      64 L1dCache (type #4)
      depth 6:     64 L1iCache (type #9)
       depth 7:    64 Core (type #2)
        depth 8:   128 PU (type #3)

# hwloc-calc でCPUセット計算
$ hwloc-calc --physical NUMANode:0
0,1,2,...,31,64,65,...,95

$ hwloc-calc --physical NUMANode:0.Core:0
0,64

6.5 CPU 周波数とパフォーマンス状態

# turbostat で詳細なCPU状態を表示
$ sudo turbostat --show Core,CPU,Avg_MHz,Busy%,Bzy_MHz,TSC_MHz,IRQ,C1%,C6%,PkgTmp,PkgWatt
Core  CPU  Avg_MHz  Busy%  Bzy_MHz  TSC_MHz  IRQ  C1%  C6%  PkgTmp  PkgWatt
-      -     1234   45.6    2800    2300     5678  12.3  42.1   65     180.5
 0     0     2345   67.8    3100    2300      456   8.9  23.3   -       -
 0    64      890   34.5    2600    2300      234  15.6  49.9   -       -
 1     1     1567   56.7    2900    2300      345  10.2  33.1   -       -
...

# cpufreq ガバナーの確認と設定
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
performance

$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors
conservative ondemand userspace powersave performance schedutil

# 全CPUを performance モードに設定
$ for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
    echo performance > $cpu
  done

7. NUMA 対応メモリ割り当て

7.1 カーネルのメモリ割り当てと NUMA

Linux カーネルのメモリアロケータ (Buddy Allocator, SLAB/SLUB) は NUMA を認識し、可能な限りローカルノードからメモリを割り当てる:

/*
 * ページ割り当ての NUMA 対応フロー
 * mm/page_alloc.c
 */

/*
 * alloc_pages() の呼び出しチェーン:
 * 
 * alloc_pages(gfp_mask, order)
 *   → alloc_pages_node(numa_node_id(), gfp_mask, order)
 *     → __alloc_pages(gfp_mask, order, preferred_nid, nodemask)
 *       → get_page_from_freelist(gfp_mask, order, alloc_flags, ac)
 *         → ゾーンリストを順に検索
 *            1. ローカルノードのゾーンを最初に試行
 *            2. リモートノードへフォールバック
 */

/* NUMA ノードを指定したメモリ割り当て */
struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
    if (nid < 0)
        nid = numa_node_id();  /* 現在のCPUのノードを使用 */
    
    return __alloc_pages(gfp_mask, order, nid, NULL);
}

/* カーネル内で特定ノードに割り当てる例 */
void *ptr = kmalloc_node(size, GFP_KERNEL, node_id);
struct page *page = alloc_pages_node(node_id, GFP_KERNEL, 0);

7.2 GFP フラグと NUMA

/*
 * NUMA 関連の GFP フラグ
 */

/* 基本的な割り当て - 現在のCPUのローカルノードから割り当て */
ptr = kmalloc(size, GFP_KERNEL);

/* 特定のノードに割り当て */
ptr = kmalloc_node(size, GFP_KERNEL, target_node);

/* NUMA ポリシーに関連する GFP フラグ */
#define __GFP_THISNODE    ((__force gfp_t)___GFP_THISNODE)
/* 指定ノードのみで割り当て(フォールバックなし) */

/*
 * 使用例:
 * ローカルノードからのみ割り当て(失敗時はリモートへフォールバックしない)
 */
ptr = kmalloc_node(size, GFP_KERNEL | __GFP_THISNODE, node);

/* Huge Page の NUMA 対応割り当て */
struct page *huge_page = alloc_pages_node(node_id,
    GFP_TRANSHUGE | __GFP_THISNODE, HPAGE_PMD_ORDER);

7.3 ユーザー空間のメモリ割り当てと NUMA

/*
 * ユーザー空間プロセスのメモリ割り当てにおける NUMA の影響
 *
 * 1. malloc() / mmap()
 *    → カーネルは仮想アドレスを割り当てるが、物理ページはまだ確保しない
 *    → 最初のアクセス時にページフォルトが発生
 *    → ページフォルト時にCPUが実行されているノードからページを割り当て
 *    → これを「first-touch」ポリシーと呼ぶ
 *
 * 2. first-touch の重要性:
 *    メモリを初期化するスレッドのCPU親和性が、
 *    実際にメモリが割り当てられるノードを決定する
 */

/* first-touch の例 */
#include <stdlib.h>
#include <string.h>
#include <numa.h>
#include <sched.h>

#define SIZE (1UL << 30)  /* 1 GB */

void *buffer = malloc(SIZE);
/* この時点では物理ページは割り当てられていない */

/* スレッド 0 (Node 0 上で実行) で初期化 */
memset(buffer, 0, SIZE);
/* → 全ページが Node 0 に割り当てられる */

/* もし Node 1 の CPU でアクセスすると、全リモートアクセスになる! */

/* NUMA を意識した初期化パターン */
void numa_aware_init(void *buffer, size_t size, int num_threads)
{
    size_t chunk = size / num_threads;
    
    #pragma omp parallel num_threads(num_threads)
    {
        int tid = omp_get_thread_num();
        /* 各スレッドが自分の担当部分を初期化 */
        /* → 各スレッドの実行ノードにページが分散 */
        memset((char *)buffer + tid * chunk, 0, chunk);
    }
}

7.4 メモリ配置の確認

# プロセスのメモリが各ノードにどう配置されているか確認
$ numastat -p <PID>

Per-node process memory usage (in MBs) for PID 12345 (my_application)
                           Node 0          Node 1           Total
                  --------------- --------------- ---------------
Huge                         0.00            0.00            0.00
Heap                       512.34           23.45          535.79
Stack                        0.12            0.00            0.12
Private                   2345.67          123.45         2469.12
----------------  --------------- --------------- ---------------
Total                     2858.13          146.90         3005.03

# /proc/<PID>/numa_maps でページレベルの詳細を確認
$ cat /proc/12345/numa_maps | head -20
00400000 default file=/usr/bin/my_app mapped=234 active=230 N0=200 N1=34
00601000 default file=/usr/bin/my_app anon=12 dirty=12 active=12 N0=12
7f1234500000 default file=/usr/lib/libc.so.6 mapped=456 N0=400 N1=56
7f1234800000 default anon=131072 dirty=131072 active=120000 N0=120000 N1=11072
7fff12340000 default stack anon=32 dirty=32 active=32 N0=32

# フィールドの意味:
# default    = メモリポリシー
# file=...   = マッピングされたファイル
# anon=N     = 匿名ページ数
# dirty=N    = ダーティページ数
# active=N   = アクティブページ数
# N0=N       = Node 0 に配置されたページ数
# N1=N       = Node 1 に配置されたページ数
# mapped=N   = マップされたページ数

8. NUMA メモリポリシー

8.1 メモリポリシーの種類

Linux カーネルは 4 種類の NUMA メモリポリシーを提供する:

ポリシー説明使用場面
defaultローカルノード優先(first-touch)一般的な用途
bind指定ノードのみに割り当てメモリ帯域幅の隔離
interleave全(または指定)ノードに均等分散共有データ、カーネルブート時
preferred指定ノードを優先、フォールバック可優先ノード指定だが OOM 回避
preferred_many複数ノードを優先 (Linux 5.15+)複数ノードにまたがる優先配置
local常にローカルノード (Linux 5.14+)明示的なローカル配置

8.2 default ポリシー

/*
 * default ポリシー (MPOL_DEFAULT):
 * - プロセスのデフォルトポリシー
 * - ページフォルト時にCPUが実行されているノードからページを割り当て
 * - first-touch 動作
 */

/* カーネル内での実装 */
/* mm/mempolicy.c */
static int policy_node(gfp_t gfp, struct mempolicy *policy, int nd)
{
    if (policy->mode == MPOL_DEFAULT) {
        /* 現在のCPUのノードを返す */
        return numa_node_id();
    }
    /* ... */
}

8.3 bind ポリシー

/*
 * bind ポリシー (MPOL_BIND):
 * - 指定されたノードのみにメモリを割り当て
 * - 指定ノードのメモリが不足した場合、OOM になる可能性がある
 * - 厳密なメモリ配置制御が必要な場合に使用
 */

#include <numaif.h>
#include <numa.h>

void use_bind_policy(void)
{
    unsigned long nodemask = 0x1;  /* Node 0 のみ */
    unsigned long maxnode = sizeof(nodemask) * 8;
    
    /* Node 0 に bind */
    if (set_mempolicy(MPOL_BIND, &nodemask, maxnode) < 0) {
        perror("set_mempolicy BIND");
    }
    
    /* 以降の割り当ては Node 0 のみ */
    void *ptr = malloc(1024 * 1024);  /* Node 0 から割り当て */
    
    /* ポリシーをデフォルトに戻す */
    set_mempolicy(MPOL_DEFAULT, NULL, 0);
}

8.4 interleave ポリシー

/*
 * interleave ポリシー (MPOL_INTERLEAVE):
 * - メモリを指定ノード間で均等に分散 (ラウンドロビン)
 * - メモリ帯域幅の最大化に有効
 * - カーネル起動時のデフォルト(ブートメモリの割り当て)
 * - 大量の共有データを持つアプリケーションに適する
 */

void use_interleave_policy(void)
{
    struct bitmask *mask = numa_allocate_nodemask();
    numa_bitmask_setbit(mask, 0);  /* Node 0 */
    numa_bitmask_setbit(mask, 1);  /* Node 1 */
    
    /* Node 0 と Node 1 で interleave */
    numa_set_interleave_mask(mask);
    
    /* 大量のメモリを割り当て - 均等に分散される */
    void *large_buffer = malloc(8UL * 1024 * 1024 * 1024);  /* 8GB */
    memset(large_buffer, 0, 8UL * 1024 * 1024 * 1024);
    
    /* 結果: Node 0 に ~4GB, Node 1 に ~4GB */
    
    numa_free_nodemask(mask);
}
# interleave ポリシーでのメモリ分布確認
$ numactl --interleave=all ./my_application &
$ PID=$!
$ numastat -p $PID
Per-node process memory usage (in MBs) for PID 23456
                           Node 0          Node 1           Total
                  --------------- --------------- ---------------
Huge                         0.00            0.00            0.00
Heap                      4012.34         3998.76         8011.10
Stack                        0.06            0.06            0.12
Private                     56.78           55.90          112.68
----------------  --------------- --------------- ---------------
Total                     4069.18         4054.72         8123.90
# ほぼ均等に分散されている

8.5 preferred ポリシー

/*
 * preferred ポリシー (MPOL_PREFERRED):
 * - 指定ノードからの割り当てを優先
 * - 指定ノードのメモリが不足した場合、他のノードにフォールバック
 * - bind より柔軟(OOM を回避できる)
 */

void use_preferred_policy(void)
{
    unsigned long nodemask = 0x2;  /* Node 1 を優先 */
    unsigned long maxnode = sizeof(nodemask) * 8;
    
    /* Node 1 を preferred に設定 */
    if (set_mempolicy(MPOL_PREFERRED, &nodemask, maxnode) < 0) {
        perror("set_mempolicy PREFERRED");
    }
    
    /* 可能な限り Node 1 から割り当て、不足時は他ノードへ */
    void *ptr = malloc(1024 * 1024);
}

8.6 メモリポリシーの適用範囲

メモリポリシーの階層:

1. VMA (Virtual Memory Area) レベル: mbind()
   - 特定のメモリ領域に対してポリシーを設定
   - 最も細粒度

2. プロセスレベル: set_mempolicy()
   - プロセス全体のデフォルトポリシー
   - VMA レベルのポリシーが優先

3. システムレベル: カーネルブートパラメータ
   - numa_balancing=enable/disable
   - カーネル全体の NUMA 動作を制御

優先順位: VMA > プロセス > システム

9. set_mempolicy, mbind, numactl

9.1 set_mempolicy システムコール

/*
 * set_mempolicy(2) - プロセスのデフォルト NUMA メモリポリシーを設定
 *
 * #include <numaif.h>
 * long set_mempolicy(int mode, const unsigned long *nodemask,
 *                    unsigned long maxnode);
 *
 * mode: MPOL_DEFAULT, MPOL_BIND, MPOL_INTERLEAVE, 
 *       MPOL_PREFERRED, MPOL_LOCAL
 * nodemask: 対象ノードのビットマスク
 * maxnode: ビットマスクの最大ビット数
 *
 * 戻り値: 成功時 0, エラー時 -1
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <numaif.h>
#include <errno.h>

int main(void)
{
    unsigned long nodemask;
    unsigned long maxnode = sizeof(nodemask) * 8;
    int mode;
    
    /* 現在のポリシーを取得 */
    if (get_mempolicy(&mode, &nodemask, maxnode, NULL, 0) < 0) {
        perror("get_mempolicy");
        return 1;
    }
    printf("Current policy: %d, nodemask: 0x%lx\n", mode, nodemask);
    
    /* interleave ポリシーに変更 */
    nodemask = 0x3;  /* Node 0 と Node 1 */
    if (set_mempolicy(MPOL_INTERLEAVE, &nodemask, maxnode) < 0) {
        perror("set_mempolicy");
        return 1;
    }
    
    /* 大量メモリ割り当て */
    size_t size = 1UL << 30;  /* 1 GB */
    void *buffer = malloc(size);
    if (!buffer) {
        perror("malloc");
        return 1;
    }
    
    /* first-touch でページを確保 */
    memset(buffer, 0, size);
    
    printf("Allocated %zu bytes with interleave policy\n", size);
    
    /* ポリシーをデフォルトに戻す */
    set_mempolicy(MPOL_DEFAULT, NULL, 0);
    
    free(buffer);
    return 0;
}

9.2 mbind システムコール

/*
 * mbind(2) - 特定のメモリ領域に NUMA ポリシーを設定
 *
 * #include <numaif.h>
 * long mbind(void *addr, unsigned long len, int mode,
 *           const unsigned long *nodemask, unsigned long maxnode,
 *           unsigned flags);
 *
 * flags:
 *   MPOL_MF_STRICT  - 既存ページがポリシーに合わない場合エラー
 *   MPOL_MF_MOVE    - 既存ページをポリシーに合うノードに移動
 *   MPOL_MF_MOVE_ALL - 他プロセスと共有しているページも移動
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <numaif.h>

int main(void)
{
    size_t size = 256 * 1024 * 1024;  /* 256 MB */
    
    /* メモリマッピングを作成 */
    void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        return 1;
    }
    
    /* 最初にページを確保(デフォルトポリシーで) */
    memset(addr, 0, size);
    
    /* Node 1 に bind ポリシーを設定し、ページを移動 */
    unsigned long nodemask = 0x2;  /* Node 1 */
    unsigned long maxnode = sizeof(nodemask) * 8;
    
    if (mbind(addr, size, MPOL_BIND, &nodemask, maxnode,
              MPOL_MF_MOVE | MPOL_MF_STRICT) < 0) {
        perror("mbind");
        /* MPOL_MF_MOVE でページ移動を要求 */
    } else {
        printf("Successfully bound %zu bytes to Node 1\n", size);
    }
    
    /* 前半を Node 0、後半を Node 1 に分割配置 */
    unsigned long mask0 = 0x1;
    unsigned long mask1 = 0x2;
    
    mbind(addr, size / 2, MPOL_BIND, &mask0, maxnode, MPOL_MF_MOVE);
    mbind((char *)addr + size / 2, size / 2, MPOL_BIND, &mask1, maxnode, MPOL_MF_MOVE);
    
    munmap(addr, size);
    return 0;
}

9.3 numactl コマンド

# numactl の基本的な使い方

# システムの NUMA トポロジを表示
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 64 65 66 67 68 69 70 71
node 0 size: 131072 MB
node 0 free: 98304 MB
node 1 cpus: 8 9 10 11 12 13 14 15 72 73 74 75 76 77 78 79
node 1 size: 131072 MB
node 1 free: 102400 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

# 現在のプロセスの NUMA ポリシーを表示
$ numactl --show
policy: default
preferred node: current
physcpubind: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ...
cpubind: 0 1
nodebind: 0 1
membind: 0 1

# --- CPU バインド ---

# Node 0 の CPU のみで実行
$ numactl --cpunodebind=0 ./my_application

# 特定の CPU のみで実行
$ numactl --physcpubind=0-7 ./my_application

# --- メモリポリシー ---

# Node 0 のメモリのみに bind
$ numactl --membind=0 ./my_application

# interleave モードで全ノードに分散
$ numactl --interleave=all ./my_application

# Node 1 を preferred で実行
$ numactl --preferred=1 ./my_application

# --- CPU + メモリの組み合わせ ---

# Node 0 で実行し、Node 0 のメモリのみ使用
$ numactl --cpunodebind=0 --membind=0 ./my_application

# Node 1 で実行し、メモリは interleave
$ numactl --cpunodebind=1 --interleave=all ./my_application

# --- 実用的な例 ---

# MySQL を Node 0 に bind
$ numactl --cpunodebind=0 --membind=0 mysqld

# Redis を interleave モードで起動
$ numactl --interleave=all redis-server /etc/redis/redis.conf

# Java アプリケーションを特定ノードで実行
$ numactl --cpunodebind=0 --membind=0 \
    java -Xmx64g -XX:+UseNUMA -jar application.jar

# 大規模データ処理を全ノードに分散
$ numactl --interleave=all ./data_processing_app

9.4 libnuma ライブラリ

/*
 * libnuma を使った NUMA 制御プログラム
 * コンパイル: gcc -o numa_example numa_example.c -lnuma
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <numa.h>
#include <numaif.h>

void print_numa_info(void)
{
    printf("=== NUMA System Information ===\n");
    printf("NUMA available: %s\n", 
           numa_available() >= 0 ? "yes" : "no");
    printf("Max node: %d\n", numa_max_node());
    printf("Configured nodes: %d\n", numa_num_configured_nodes());
    printf("Configured CPUs: %d\n", numa_num_configured_cpus());
    
    /* 各ノードの情報 */
    for (int i = 0; i <= numa_max_node(); i++) {
        long long free_mem;
        long long total_mem = numa_node_size64(i, &free_mem);
        
        printf("\nNode %d:\n", i);
        printf("  Total memory: %lld MB\n", total_mem / (1024 * 1024));
        printf("  Free memory:  %lld MB\n", free_mem / (1024 * 1024));
        
        /* ノードに属する CPU を表示 */
        struct bitmask *cpumask = numa_allocate_cpumask();
        numa_node_to_cpus(i, cpumask);
        printf("  CPUs: ");
        for (int cpu = 0; cpu < numa_num_configured_cpus(); cpu++) {
            if (numa_bitmask_isbitset(cpumask, cpu))
                printf("%d ", cpu);
        }
        printf("\n");
        numa_free_cpumask(cpumask);
    }
    
    /* ノード間距離 */
    printf("\nNode distances:\n");
    printf("     ");
    for (int j = 0; j <= numa_max_node(); j++)
        printf("  %3d", j);
    printf("\n");
    for (int i = 0; i <= numa_max_node(); i++) {
        printf("  %2d:", i);
        for (int j = 0; j <= numa_max_node(); j++) {
            printf("  %3d", numa_distance(i, j));
        }
        printf("\n");
    }
}

void demonstrate_numa_allocation(void)
{
    printf("\n=== NUMA Allocation Demo ===\n");
    
    size_t size = 64 * 1024 * 1024;  /* 64 MB */
    
    /* Node 0 に割り当て */
    void *node0_mem = numa_alloc_onnode(size, 0);
    if (node0_mem) {
        memset(node0_mem, 'A', size);
        printf("Allocated %zu MB on Node 0\n", size / (1024 * 1024));
    }
    
    /* Node 1 に割り当て */
    void *node1_mem = numa_alloc_onnode(size, 1);
    if (node1_mem) {
        memset(node1_mem, 'B', size);
        printf("Allocated %zu MB on Node 1\n", size / (1024 * 1024));
    }
    
    /* interleave で割り当て */
    void *interleaved = numa_alloc_interleaved(size * 2);
    if (interleaved) {
        memset(interleaved, 'C', size * 2);
        printf("Allocated %zu MB interleaved\n", size * 2 / (1024 * 1024));
    }
    
    /* ローカルノードに割り当て */
    void *local_mem = numa_alloc_local(size);
    if (local_mem) {
        memset(local_mem, 'D', size);
        printf("Allocated %zu MB on local node\n", size / (1024 * 1024));
    }
    
    /* ページの移動 */
    int status;
    int target_node = 1;
    void *pages[] = { node0_mem };
    int nodes[] = { target_node };
    int statuses[1];
    
    if (numa_move_pages(0, 1, pages, nodes, statuses, MPOL_MF_MOVE) == 0) {
        printf("Moved pages from Node 0 to Node %d (status: %d)\n",
               target_node, statuses[0]);
    }
    
    /* クリーンアップ */
    numa_free(node0_mem, size);
    numa_free(node1_mem, size);
    numa_free(interleaved, size * 2);
    numa_free(local_mem, size);
}

int main(void)
{
    if (numa_available() < 0) {
        fprintf(stderr, "NUMA is not available on this system\n");
        return 1;
    }
    
    print_numa_info();
    demonstrate_numa_allocation();
    
    return 0;
}

10. CPU ホットプラグ

10.1 CPU ホットプラグの概要

CPU ホットプラグは、システム稼働中にCPUを論理的にオンライン/オフラインにする機能である:

# CPU のオンライン/オフライン制御
$ cat /sys/devices/system/cpu/cpu7/online
1

# CPU 7 をオフラインにする
$ echo 0 > /sys/devices/system/cpu/cpu7/online
$ dmesg | tail -5
[12345.678901] smpboot: CPU 7 is now offline
[12345.678902] IRQ 45: no longer affine to CPU7

# CPU 7 をオンラインに戻す
$ echo 1 > /sys/devices/system/cpu/cpu7/online
$ dmesg | tail -5
[12350.123456] smpboot: Booting Node 0 Processor 7 APIC 0x7
[12350.234567] CPU7 is up

# CPU 0 はオフラインにできない(ブートCPU)
$ echo 0 > /sys/devices/system/cpu/cpu0/online
-bash: echo: write error: Invalid argument

10.2 CPU ホットプラグの使用例

# HT ペアの片方をオフラインにしてパフォーマンスを向上
# (L1/L2 キャッシュの競合を排除)
$ for cpu in $(cat /sys/devices/system/cpu/cpu0/topology/thread_siblings_list | \
    tr ',' '\n' | tail -n +2); do
    echo 0 > /sys/devices/system/cpu/cpu${cpu}/online
done

# 特定の NUMA ノードの全 CPU をオフラインにする
$ for cpu in $(cat /sys/devices/system/node/node1/cpulist | \
    tr ',' '\n' | tr '-' ' ' | while read a b; do
    seq $a $b; done); do
    if [ "$cpu" != "0" ]; then
        echo 0 > /sys/devices/system/cpu/cpu${cpu}/online 2>/dev/null
    fi
done

# 省電力のために不要な CPU をオフラインにするスクリプト
#!/bin/bash
# cpu_power_save.sh - CPU 数を動的に調整

LOAD_THRESHOLD_HIGH=70
LOAD_THRESHOLD_LOW=30
CHECK_INTERVAL=30

get_cpu_load() {
    awk '{printf "%.0f\n", (1 - $5/100) * 100}' <(top -bn1 | grep "Cpu(s)")
}

while true; do
    load=$(get_cpu_load)
    online=$(nproc)
    total=$(ls -d /sys/devices/system/cpu/cpu[0-9]* | wc -l)
    
    if [ "$load" -gt "$LOAD_THRESHOLD_HIGH" ] && [ "$online" -lt "$total" ]; then
        # CPU を追加
        for cpu_dir in /sys/devices/system/cpu/cpu[0-9]*; do
            if [ "$(cat ${cpu_dir}/online 2>/dev/null)" = "0" ]; then
                echo 1 > ${cpu_dir}/online 2>/dev/null
                break
            fi
        done
    elif [ "$load" -lt "$LOAD_THRESHOLD_LOW" ] && [ "$online" -gt 2 ]; then
        # CPU を削減(最低 2 CPU は維持)
        for cpu_dir in $(ls -rd /sys/devices/system/cpu/cpu[0-9]*); do
            cpu_num=$(basename $cpu_dir | sed 's/cpu//')
            if [ "$cpu_num" -gt 0 ] && [ "$(cat ${cpu_dir}/online)" = "1" ]; then
                echo 0 > ${cpu_dir}/online 2>/dev/null
                break
            fi
        done
    fi
    
    sleep $CHECK_INTERVAL
done

10.3 カーネルの CPU ホットプラグ実装

/*
 * CPU ホットプラグのカーネル内部フロー
 * kernel/cpu.c
 */

/*
 * CPU オフライン時の処理:
 * 1. cpuhp_kick_ap_work() - AP にシャットダウンを指示
 * 2. sched_cpu_deactivate() - スケジューラからCPUを除外
 * 3. タスクのマイグレーション - 他のCPUにタスクを移動
 * 4. IRQ の再配置 - IRQ affinity を更新
 * 5. タイマーのマイグレーション
 * 6. RCU コールバックの処理
 * 7. CPU キャッシュのフラッシュ
 * 8. AP を停止
 */

/* CPU ホットプラグのコールバック登録 */
#include <linux/cpuhotplug.h>

static int my_cpu_online(unsigned int cpu)
{
    pr_info("CPU %u is coming online\n", cpu);
    /* CPU オンライン時の初期化処理 */
    return 0;
}

static int my_cpu_offline(unsigned int cpu)
{
    pr_info("CPU %u is going offline\n", cpu);
    /* CPU オフライン時のクリーンアップ処理 */
    return 0;
}

static int __init my_module_init(void)
{
    int ret;
    
    ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,
                            "my_module:online",
                            my_cpu_online,
                            my_cpu_offline);
    if (ret < 0)
        return ret;
    
    return 0;
}

11. スケジューラドメインとグループ

11.1 スケジューラドメイン階層

Linux のタスクスケジューラは、CPU トポロジを「スケジューラドメイン」として階層的に管理する:

スケジューラドメイン階層 (2 Socket, 各 32 コア, HT 有効):

  SD_NUMA (ドメインレベル 3)
  ├── 全128 CPU をカバー
  │   ├── Group 0: Node 0 の CPU (0-31, 64-95)
  │   └── Group 1: Node 1 の CPU (32-63, 96-127)
  │
  ├── SD_MC (ドメインレベル 2) - Multi-Core
  │   ├── Node 0 の全コア
  │   │   ├── Group 0: Core 0 (CPU 0, 64)
  │   │   ├── Group 1: Core 1 (CPU 1, 65)
  │   │   └── ...
  │   └── Node 1 の全コア
  │       ├── Group 0: Core 32 (CPU 32, 96)
  │       └── ...
  │
  └── SD_SMT (ドメインレベル 1) - Hyper-Threading
      ├── Core 0: CPU 0, CPU 64
      ├── Core 1: CPU 1, CPU 65
      └── ...

11.2 スケジューラドメインの確認

# スケジューラドメインの表示
$ cat /proc/sys/kernel/sched_domain/cpu0/domain0/name
SMT
$ cat /proc/sys/kernel/sched_domain/cpu0/domain1/name
MC
$ cat /proc/sys/kernel/sched_domain/cpu0/domain2/name
NUMA

# ドメインの詳細パラメータ
$ for d in /proc/sys/kernel/sched_domain/cpu0/domain*; do
    echo "=== $(basename $d): $(cat $d/name) ==="
    echo "  flags: $(cat $d/flags)"
    echo "  min_interval: $(cat $d/min_interval)"
    echo "  max_interval: $(cat $d/max_interval)"
    echo "  busy_factor: $(cat $d/busy_factor)"
    echo "  imbalance_pct: $(cat $d/imbalance_pct)"
    echo "  cache_nice_tries: $(cat $d/cache_nice_tries)"
  done

=== domain0: SMT ===
  flags: 255
  min_interval: 1
  max_interval: 2
  busy_factor: 64
  imbalance_pct: 110
  cache_nice_tries: 1

=== domain1: MC ===
  flags: 4143
  min_interval: 4
  max_interval: 32
  busy_factor: 64
  imbalance_pct: 117
  cache_nice_tries: 1

=== domain2: NUMA ===
  flags: 12463
  min_interval: 32
  max_interval: 128
  busy_factor: 64
  imbalance_pct: 125
  cache_nice_tries: 2

11.3 負荷分散のメカニズム

/*
 * スケジューラの負荷分散
 * kernel/sched/fair.c
 *
 * CFS (Completely Fair Scheduler) は定期的に
 * 各スケジューラドメイン内で負荷分散を行う
 */

/*
 * 負荷分散の流れ:
 * 
 * 1. load_balance() が定期的に呼ばれる
 *    - アイドル CPU: 即座に実行
 *    - ビジー CPU: ドメインの interval に基づく周期で実行
 *
 * 2. find_busiest_group() で最も負荷の高いグループを検索
 *    - ドメイン内の各グループの負荷を比較
 *    - imbalance_pct を超える差がある場合にバランシング
 *
 * 3. find_busiest_queue() で最も負荷の高い CPU を選択
 *
 * 4. detach_tasks() でタスクを選択
 *    - キャッシュ効率を考慮
 *    - NUMA locality を考慮
 *
 * 5. attach_tasks() でタスクを自分の CPU に移動
 */

/* スケジューラ統計の確認 */
# スケジューラ統計
$ cat /proc/schedstat | head -20
version 15
timestamp 4294967296
cpu0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
domain0 000000ff,00000003 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
domain1 000000ff,ffffffff 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
domain2 ffffffff,ffffffff 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

# スケジューラデバッグ情報
$ cat /proc/sched_debug | head -40
Sched Debug Version: v0.11, 6.8.0-45-generic
ktime                                   : 123456789.012345
sched_clk                               : 123456789.012345
cpu_clk                                 : 123456789.012345
jiffies                                 : 12345678901
sched_clock_stable()                    : 1

sysctl_sched
  .sysctl_sched_latency                 : 6.000000
  .sysctl_sched_min_granularity         : 0.750000
  .sysctl_sched_idle_min_granularity    : 0.750000
  .sysctl_sched_wakeup_granularity      : 1.000000
  .sysctl_sched_child_runs_first        : 0
  .sysctl_sched_features                : 2059067
  .sysctl_sched_tunable_scaling         : 1 (logarithmic)

# スケジューラ関連 sysctl パラメータ
$ sysctl kernel.sched_migration_cost_ns
kernel.sched_migration_cost_ns = 500000

$ sysctl kernel.sched_nr_migrate
kernel.sched_nr_migrate = 32

$ sysctl kernel.sched_min_granularity_ns
kernel.sched_min_granularity_ns = 3000000

11.4 NUMA 対応のスケジューリング

# NUMA バランシング関連のスケジューラパラメータ
$ sysctl -a 2>/dev/null | grep numa
kernel.numa_balancing = 1
kernel.numa_balancing_scan_delay_ms = 1000
kernel.numa_balancing_scan_period_min_ms = 1000
kernel.numa_balancing_scan_period_max_ms = 60000
kernel.numa_balancing_scan_size_mb = 256
kernel.numa_balancing_promote_rate_limit_MBps = 65536

# スケジューラのNUMA統計
$ cat /proc/vmstat | grep numa
numa_hit 567890123
numa_miss 12345678
numa_foreign 12345678
numa_interleave 9876543
numa_local 556789012
numa_other 23456789
numa_pte_updates 345678901
numa_huge_pte_updates 1234567
numa_hint_faults 23456789
numa_hint_faults_local 18765432
numa_pages_migrated 4567890

12. NUMA バランシング (自動 NUMA マイグレーション)

12.1 NUMA バランシングの仕組み

Linux カーネルの自動 NUMA バランシング (Automatic NUMA Balancing) は、プロセスのメモリアクセスパターンを監視し、頻繁にアクセスされるページをアクセス元の CPU に近いノードに自動移動する:

NUMA バランシングの動作フロー:

1. ページテーブルスキャン
   - 定期的にページテーブルエントリを「NUMA hint fault」用にマーク
   - PTE の Present ビットをクリア、NUMA ビットをセット
   
2. NUMA hint fault の発生
   - マークされたページにアクセスすると NUMA hint fault が発生
   - フォルト時にアクセスパターンを記録
   
3. マイグレーション判定
   - ページが別ノードのCPUから頻繁にアクセスされている場合
   - アクセス元ノードにページを移動

4. タスクマイグレーション
   - メモリの大部分が別ノードにある場合
   - タスク自体を別ノードに移動(メモリ移動より効率的な場合)

タイムライン:
  t=0    : PTE をスキャン、NUMA ビットをセット
  t=T1   : ページアクセス → NUMA hint fault
           → アクセスパターン記録
  t=T2   : 十分なサンプリング後
           → ページ移動 or タスク移動を判定
  t=T3   : マイグレーション実行

12.2 NUMA バランシングの設定

# NUMA バランシングの有効/無効
$ sysctl -w kernel.numa_balancing=1   # 有効
$ sysctl -w kernel.numa_balancing=0   # 無効

# スキャン間隔の設定
# スキャン開始までの遅延 (ms)
$ sysctl -w kernel.numa_balancing_scan_delay_ms=1000

# 最小スキャン周期 (ms) - アクセスが多い場合の最短間隔
$ sysctl -w kernel.numa_balancing_scan_period_min_ms=1000

# 最大スキャン周期 (ms) - アクセスが少ない場合の最長間隔
$ sysctl -w kernel.numa_balancing_scan_period_max_ms=60000

# 1回のスキャンでチェックするメモリサイズ (MB)
$ sysctl -w kernel.numa_balancing_scan_size_mb=256

# /etc/sysctl.d/99-numa.conf に永続化
cat > /etc/sysctl.d/99-numa.conf << 'EOF'
kernel.numa_balancing = 1
kernel.numa_balancing_scan_delay_ms = 1000
kernel.numa_balancing_scan_period_min_ms = 1000
kernel.numa_balancing_scan_period_max_ms = 60000
kernel.numa_balancing_scan_size_mb = 256
EOF

12.3 NUMA バランシングの監視

# NUMA バランシングの統計情報
$ cat /proc/vmstat | grep numa
numa_hit 567890123          # ローカルノードから割り当て成功
numa_miss 12345678          # リモートノードから割り当て(ローカル不足)
numa_foreign 12345678       # 他ノード用の割り当てが自ノードで発生
numa_interleave 9876543     # interleave ポリシーでの割り当て
numa_local 556789012        # ローカルノードでの割り当て
numa_other 23456789         # リモートノードでの割り当て

# NUMA hint fault 統計
numa_pte_updates 345678901  # スキャンで更新された PTE 数
numa_huge_pte_updates 1234567  # Huge Page の PTE 更新数
numa_hint_faults 23456789   # NUMA hint fault 発生回数
numa_hint_faults_local 18765432  # ローカルノードでの fault(移動不要)
numa_pages_migrated 4567890 # 移動されたページ数

# NUMA 効率の計算
# NUMA locality = numa_hint_faults_local / numa_hint_faults * 100
# 上記例: 18765432 / 23456789 * 100 = 80% (80% がローカルアクセス)

# perf でNUMAイベントを監視
$ sudo perf stat -e numa-stores,numa-store-misses,numa-loads,\
numa-load-misses -p <PID> sleep 10

# numastat で全体の NUMA 統計を確認
$ numastat
                           node0           node1
numa_hit              567890123       543210987
numa_miss              12345678        11223344
numa_foreign           11223344        12345678
interleave_hit          9876543         9654321
local_node            556789012       532109876
other_node             23456789        22334455

12.4 NUMA バランシングの無効化が推奨されるケース

# NUMA バランシングを無効にすべきケース:
# 1. numactl で既に適切にバインドされている場合
# 2. DPDK 等のポーリングベースアプリケーション
# 3. リアルタイムワークロード(レイテンシジッター回避)
# 4. データベースで明示的な NUMA 設定をしている場合

# 無効化
$ sysctl -w kernel.numa_balancing=0

# カーネルブートパラメータで無効化
# GRUB_CMDLINE_LINUX="numa_balancing=disable"

13. Per-CPU データと Per-NUMA ノード割り当て

13.1 Per-CPU 変数

/*
 * Per-CPU 変数は各 CPU ごとにデータのコピーを持つ
 * ロック不要でアクセスでき、キャッシュラインの競合を回避
 */

#include <linux/percpu.h>

/* 静的 Per-CPU 変数の定義 */
static DEFINE_PER_CPU(unsigned long, packet_count);
static DEFINE_PER_CPU(struct statistics, cpu_stats);

/* アクセス方法 */
void process_packet(struct packet *pkt)
{
    /* プリエンプションを無効にしてアクセス */
    preempt_disable();
    
    /* 現在のCPUの変数をインクリメント */
    this_cpu_inc(packet_count);
    
    /* 構造体へのアクセス */
    struct statistics *stats = this_cpu_ptr(&cpu_stats);
    stats->bytes += pkt->len;
    stats->packets++;
    
    preempt_enable();
}

/* 全CPUの合計を計算 */
unsigned long total_packets(void)
{
    unsigned long total = 0;
    int cpu;
    
    for_each_possible_cpu(cpu) {
        total += per_cpu(packet_count, cpu);
    }
    return total;
}

/* 動的 Per-CPU 変数 */
void *pcpu_data;

int init_module(void)
{
    pcpu_data = alloc_percpu(struct my_data);
    if (!pcpu_data)
        return -ENOMEM;
    return 0;
}

void cleanup_module(void)
{
    free_percpu(pcpu_data);
}

13.2 Per-CPU アロケータの NUMA 対応

/*
 * Per-CPU アロケータは NUMA を認識し、
 * 各 CPU の Per-CPU データをその CPU のローカルノードに配置する
 *
 * mm/percpu.c
 *
 * Per-CPU メモリレイアウト:
 *
 * Node 0:
 *   [CPU 0 のデータ] [CPU 1 のデータ] ... [CPU 31 のデータ]
 *
 * Node 1:
 *   [CPU 32 のデータ] [CPU 33 のデータ] ... [CPU 63 のデータ]
 *
 * → 各 CPU は自ノードのメモリにアクセスするため高速
 */

/* Per-CPU データの配置確認 */
# Per-CPU メモリの使用状況確認
$ cat /proc/meminfo | grep Percpu
Percpu:            12345 kB

# 各CPUの割り当て詳細
$ sudo cat /proc/cpu/cpu0/cpuinfo | grep "physical id"
# CPU がどの NUMA ノードに属するかは /sys から確認
$ cat /sys/devices/system/cpu/cpu0/node0  # シンボリックリンクの存在で確認

13.3 Per-NUMA ノード割り当て

/*
 * NUMA ノード単位でのメモリ割り当て
 * ノードローカルなデータ構造の配置に使用
 */

#include <linux/slab.h>
#include <linux/numa.h>

/* ノードごとのデータ構造 */
struct node_data {
    spinlock_t lock;
    struct list_head free_list;
    unsigned long pages_allocated;
    unsigned long pages_freed;
    /* キャッシュラインにアラインメント */
} ____cacheline_aligned_in_smp;

static struct node_data *node_info;

int init_numa_data(void)
{
    int nid;
    
    node_info = kmalloc_array(nr_node_ids, sizeof(*node_info), GFP_KERNEL);
    if (!node_info)
        return -ENOMEM;
    
    for_each_online_node(nid) {
        /* 各ノードのローカルメモリに割り当て */
        struct node_data *nd = kmalloc_node(sizeof(*nd), 
                                             GFP_KERNEL, nid);
        if (!nd)
            goto fail;
        
        spin_lock_init(&nd->lock);
        INIT_LIST_HEAD(&nd->free_list);
        nd->pages_allocated = 0;
        nd->pages_freed = 0;
        
        node_info[nid] = *nd;
    }
    
    return 0;
    
fail:
    /* エラー処理 */
    return -ENOMEM;
}

/* NUMA 対応の SLAB キャッシュ */
struct kmem_cache *my_cache;

int init_slab_cache(void)
{
    my_cache = kmem_cache_create("my_objects",
                                 sizeof(struct my_object),
                                 0,  /* アラインメント */
                                 SLAB_HWCACHE_ALIGN,  /* キャッシュラインアラインメント */
                                 NULL);  /* コンストラクタ */
    
    /* SLUB アロケータは自動的に NUMA を考慮し、
     * ローカルノードからオブジェクトを割り当てる */
    return my_cache ? 0 : -ENOMEM;
}

/* 特定ノードからの SLAB 割り当て */
struct my_object *obj = kmem_cache_alloc_node(my_cache, GFP_KERNEL, target_node);

14. キャッシュコヒーレンシプロトコル (MESI, MOESI)

14.1 キャッシュコヒーレンシの必要性

SMP/NUMA システムにおけるキャッシュコヒーレンシ問題:

CPU 0 (Cache)    CPU 1 (Cache)    メモリ
  [X = 10]         [X = 10]       [X = 10]

Step 1: CPU 0 が X を 20 に更新
  [X = 20]         [X = 10]       [X = 10]  ← 不整合!

CPU 1 が古い値を読む可能性がある
→ キャッシュコヒーレンシプロトコルで解決

14.2 MESI プロトコル

MESI は Intel プロセッサで使用される基本的なキャッシュコヒーレンシプロトコルである:

MESI プロトコルの 4 つの状態:

M (Modified):  変更済み - このキャッシュのみが有効、メモリは古い
E (Exclusive): 排他 - このキャッシュのみが有効、メモリと一致
S (Shared):    共有 - 複数キャッシュが保持、メモリと一致
I (Invalid):   無効 - キャッシュラインは無効

状態遷移図:

     ┌──────────────────────────────────────┐
     │                                      ▼
   [Modified] ──(他CPUの読み)──> [Shared]
     ▲    │                        ▲   │
     │    │(書き込み)              │   │(他CPUの書き込み)
     │    ▼                        │   ▼
   [Exclusive] ────────────> [Invalid]
     ▲                           ▲ │
     │(キャッシュミス、他に     │ │(キャッシュミス、
     │  コピーなし)              │ │  他にコピーあり)
     │                           │ │
     └───────[メモリから読み込み]──┘ │
                                     │
     ← ← ← ← ← ← ← ← ← ← ← ← ← ┘

具体例:

初期状態: 変数 X はメモリにのみ存在 (全キャッシュ: I)

1. CPU 0 が X を読む:
   CPU 0: I → E (排他的に保持)
   
2. CPU 1 が X を読む:
   CPU 0: E → S (共有に降格)
   CPU 1: I → S (共有で保持)
   
3. CPU 0 が X に書き込む:
   CPU 0: S → M (変更済みに昇格)
   CPU 1: S → I (無効化される) ← Invalidation が発生
   
4. CPU 1 が X を読む:
   CPU 0: M → S (変更をメモリに書き戻し、共有に降格)
   CPU 1: I → S (メモリから読み込み、共有で保持)

14.3 MOESI プロトコル

MOESI は AMD プロセッサで使用される拡張プロトコルで、O (Owner) 状態が追加される:

MOESI プロトコルの 5 つの状態:

M (Modified):  変更済み - このキャッシュのみが有効
O (Owner):     オーナー - ★新規★ 変更済みだが他にも有効コピーあり
E (Exclusive): 排他
S (Shared):    共有
I (Invalid):   無効

MESI との違い:
  MESI:  CPU 0 が Modified → CPU 1 が読む 
         → CPU 0 はメモリに書き戻してから Shared に遷移
         
  MOESI: CPU 0 が Modified → CPU 1 が読む
         → CPU 0 は Owner に遷移(メモリ書き戻し不要!)
         → CPU 1 は Shared で保持
         → CPU 0 が Owner としてメモリ書き戻し責任を持つ

利点:
  - メモリ書き戻し回数の削減
  - キャッシュ間転送 (Cache-to-Cache Transfer) の活用
  - メモリバス帯域幅の節約

O (Owner) 状態の動作:
  CPU 0 [Owner, X=20]  CPU 1 [Shared, X=20]  メモリ [X=10 (古い)]
  
  CPU 0 が Owner として、他の CPU への供給とメモリ書き戻し責任を持つ
  → メモリは即座に更新されない(遅延書き戻し)

14.4 キャッシュコヒーレンシのパフォーマンス影響

# perf でキャッシュコヒーレンシイベントを計測
$ sudo perf stat -e \
    cache-references,cache-misses,\
    L1-dcache-loads,L1-dcache-load-misses,\
    L1-dcache-stores,\
    LLC-loads,LLC-load-misses,\
    LLC-stores,LLC-store-misses \
    -p <PID> sleep 10

 Performance counter stats for process 'PID':

     1,234,567,890      cache-references
        23,456,789      cache-misses           #    1.90% of all cache refs
     9,876,543,210      L1-dcache-loads
       234,567,890      L1-dcache-load-misses  #    2.37% of all L1-dcache accesses
     4,567,890,123      L1-dcache-stores
       345,678,901      LLC-loads
        12,345,678      LLC-load-misses        #    3.57% of all LL-cache accesses
       123,456,789      LLC-stores
         5,678,901      LLC-store-misses

# コヒーレンシ関連のパフォーマンスカウンタ
$ sudo perf stat -e \
    'offcore_response.all_data_rd.l3_miss.snoop_miss',\
    'offcore_response.all_data_rd.l3_miss.snoop_hit_no_fwd',\
    'offcore_response.all_data_rd.l3_miss.snoop_hitm' \
    -p <PID> sleep 10

# snoop_hitm が高い = キャッシュラインバウンシングが頻発

14.5 スヌーピングとディレクトリベースコヒーレンシ

スヌーピング方式 (少数プロセッサ向け):
  - 各キャッシュがバスを監視(スヌープ)
  - バス上のすべてのトランザクションをチェック
  - O(N) のスヌープトラフィック(N = CPU数)
  - 小規模 SMP (2-8 ソケット) で使用

ディレクトリベース方式 (大規模 NUMA 向け):
  - 各キャッシュラインの状態を中央ディレクトリで管理
  - 必要な CPU にのみ通知
  - O(1) の通知オーバーヘッド
  - 大規模 NUMA (8+ ソケット) で使用

Intel のアプローチ:
  - Xeon Scalable: スヌープフィルタ + ディレクトリキャッシュ
  - UPI 上のスヌープモード:
    - Early Snoop: 低レイテンシ、高帯域使用
    - Home Snoop: 帯域効率重視
    - Directory + OpportunisticSnoop: バランス型

15. メモリインターコネクト (QPI, UPI, Infinity Fabric)

15.1 Intel QPI (QuickPath Interconnect)

Intel QPI の概要:
  - Xeon 5500 (Nehalem) 〜 Xeon E5/E7 v4 (Broadwell) で使用
  - ポイントツーポイント接続
  - 各方向に 20 レーンの差動信号
  - 帯域幅: 各方向 12.8-19.2 GB/s (6.4-9.6 GT/s)
  - レイテンシ: ~40ns 追加(リモートアクセス時)

QPI リンクの構成:
  Socket 0 ←──── QPI Link ────→ Socket 1
           20 lanes each direction
           Full duplex

15.2 Intel UPI (Ultra Path Interconnect)

Intel UPI の概要:
  - Xeon Scalable (Skylake-SP 以降) で使用
  - QPI の後継
  - 各方向に 20 レーン (Skylake-SP/Cascade Lake)
    または 24 レーン (Ice Lake-SP 以降)
  - 帯域幅: 各方向 10.4-20.8 GT/s → 20.8-41.6 GB/s
  - 3 UPI リンク/ソケット (2S構成)
  - ディレクトリベースのコヒーレンシをサポート

2 Socket 構成:
  Socket 0 ←── UPI 0 ──→ Socket 1
           ←── UPI 1 ──→
           ←── UPI 2 ──→
  
  合計双方向帯域幅: ~124.8 GB/s (3 x 20.8 GT/s x 2 bytes x 2 方向)

4 Socket 構成 (全接続):
  Socket 0 ──── Socket 1
    |    \    /    |
    |      ×       |
    |    /    \    |
  Socket 2 ──── Socket 3
  
  各ソケット間に 2-3 UPI リンク
# UPI リンクの状態確認 (Intel プラットフォーム)
$ sudo lspci -vvv | grep -i "QPI\|UPI"

# turbostat で UPI 帯域を間接的に確認
$ sudo turbostat --show Package,PkgWatt,RAMWatt sleep 10

# PCM (Processor Counter Monitor) による UPI 帯域監視
$ sudo pcm-memory
|---------------------------------------||---------------------------------------|
|-- Socket 0 --||-- Socket 1 --|
|---------------------------------------||---------------------------------------|
|-- Memory (MB/s) --||-- Memory (MB/s) --|
| Read  | Write | Total || Read  | Write | Total |
|  8765 |  3456 | 12221 ||  7654 |  3210 | 10864 |
|---------------------------------------||---------------------------------------|
|-- UPI Incoming Data Traffic (MB/s) --||-- UPI Incoming Data Traffic (MB/s) --|
|  UPI0  |  UPI1  |  UPI2  || UPI0  |  UPI1  |  UPI2  |
|  1234  |  1198  |  1156  ||  1345  |  1278  |  1210  |

15.3 AMD Infinity Fabric

AMD Infinity Fabric の概要:
  - EPYC プロセッサのインターコネクト
  - Scalable Data Fabric (SDF) + Scalable Control Fabric (SCF)
  - CCD (Core Complex Die) 間の接続
  - ソケット間の接続 (xGMI: Global Memory Interconnect)

EPYC 7003 (Milan) の構成:
  
  Socket 0:
  +----------------------------------------------------+
  |  CCD 0    CCD 1    CCD 2    CCD 3                  |
  |   8C       8C       8C       8C                    |
  |    \       |       /       /                       |
  |     +------+------+------+                         |
  |     |  IOD (I/O Die)     |                         |
  |     |  UMC 0-7           |  ← 8チャネル DDR4       |
  |     |  xGMI links        |                         |
  |     +------+------+------+                         |
  |   CCD 4    CCD 5    CCD 6    CCD 7                |
  |   8C       8C       8C       8C                    |
  +----------------------------------------------------+
         |           |           |
       xGMI 0     xGMI 1     xGMI 2    → Socket 1 へ
       
  Infinity Fabric 帯域幅:
  - CCD ↔ IOD: ~32 GB/s (各方向)
  - Socket 間 xGMI: ~36 GB/s x 3 = ~108 GB/s (各方向)
# AMD EPYC のインターコネクト情報
$ dmesg | grep -i "fabric\|xgmi\|gmi"

# AMD の CPU トポロジ確認
$ lscpu -e=CPU,NODE,SOCKET,CORE,L1d,L1i,L2,L3

15.4 インターコネクトの帯域幅計測

# Intel Memory Latency Checker (MLC) による測定
$ sudo mlc --bandwidth_matrix
                Numa node
Numa node        0       1
    0        95000   45000    (MB/s)
    1        45000   95000

$ sudo mlc --latency_matrix
                Numa node
Numa node        0       1
    0         81.2   130.5    (ns)
    1        130.5    81.2

# STREAM ベンチマークによるメモリ帯域測定
$ numactl --cpunodebind=0 --membind=0 ./stream_c
-------------------------------------------------------------
Function    Best Rate MB/s  Avg time     Min time     Max time
Copy:           95432.1     0.016789     0.016732     0.016845
Scale:          94567.8     0.016942     0.016883     0.017001
Add:           102345.6     0.023499     0.023451     0.023547
Triad:         101234.5     0.023756     0.023708     0.023804
-------------------------------------------------------------

# リモートアクセスの帯域
$ numactl --cpunodebind=0 --membind=1 ./stream_c
-------------------------------------------------------------
Function    Best Rate MB/s  Avg time     Min time     Max time
Copy:           42345.6     0.037845     0.037789     0.037901
Scale:          41234.5     0.038923     0.038867     0.038979
Add:            45678.9     0.052601     0.052545     0.052657
Triad:          44567.8     0.053901     0.053845     0.053957
-------------------------------------------------------------
# → リモートアクセスはローカルの約 45% の帯域

16. False Sharing とキャッシュラインバウンシング

16.1 False Sharing の問題

/*
 * False Sharing: 異なるデータが同じキャッシュラインに存在し、
 * 一方の更新が他方のキャッシュラインを無効化する問題
 *
 * キャッシュラインサイズ: 通常 64 バイト
 */

/* ★ 悪い例: False Sharing が発生 */
struct bad_counters {
    unsigned long counter_cpu0;  /* offset 0-7 */
    unsigned long counter_cpu1;  /* offset 8-15 */
    unsigned long counter_cpu2;  /* offset 16-23 */
    unsigned long counter_cpu3;  /* offset 24-31 */
    /* 全てが同じ 64 バイトのキャッシュラインに収まる! */
};

static struct bad_counters counters;

/* CPU 0 が counter_cpu0 を更新すると、
 * CPU 1-3 のキャッシュラインも無効化される */
void thread_func(int cpu_id)
{
    for (int i = 0; i < 1000000; i++) {
        /* 各 CPU が自分のカウンタを更新 */
        (&counters.counter_cpu0)[cpu_id]++;
        /* → 他の CPU のキャッシュラインが無効化される */
        /* → キャッシュラインバウンシングが発生 */
    }
}

/* ★ 良い例: False Sharing を回避 */
struct good_counters {
    unsigned long counter_cpu0;
    char padding0[64 - sizeof(unsigned long)];  /* パディング */
    unsigned long counter_cpu1;
    char padding1[64 - sizeof(unsigned long)];
    unsigned long counter_cpu2;
    char padding2[64 - sizeof(unsigned long)];
    unsigned long counter_cpu3;
    char padding3[64 - sizeof(unsigned long)];
    /* 各カウンタが別のキャッシュラインに配置 */
};

/* Linux カーネルでの対処 */
struct per_cpu_data {
    unsigned long counter;
    /* ... その他のフィールド ... */
} ____cacheline_aligned_in_smp;
/* ____cacheline_aligned_in_smp マクロで
 * SMP 環境でキャッシュラインにアラインメント */

16.2 False Sharing の検出

# perf c2c (Cache-to-Cache) で False Sharing を検出
$ sudo perf c2c record -p <PID> sleep 10
$ sudo perf c2c report --stdio

=================================================
            Shared Data Cache Line Table
=================================================
#
#        Total      Rmt  LLC Load  Rmt LLC Load     Total     Total
#        Hitm      Hitm     Lcl        Rmt         Loads    Stores
# .......  ........  .......  ...........  .........  ........  ........
#
     4523      2345     1234        1111    567890    345678
     3456      1789      987         802    456789    234567
     2345      1234      678         556    345678    123456

     Cacheline  0x7f1234560000 (struct bad_counters)
       Offset   0:  CPU 0 writes (counter_cpu0)
       Offset   8:  CPU 1 writes (counter_cpu1)  ← False Sharing!
       Offset  16:  CPU 2 writes (counter_cpu2)  ← False Sharing!
       
# Hitm (Hit Modified) が高い = キャッシュラインバウンシング

# perf stat でキャッシュコヒーレンシイベントの概要
$ sudo perf stat -e \
    'mem_load_l3_miss_retired.remote_hitm',\
    'mem_load_l3_miss_retired.local_dram',\
    'mem_load_l3_miss_retired.remote_dram' \
    -p <PID> sleep 10

16.3 C/C++ での False Sharing 対策

/* C11 の alignas を使用 */
#include <stdalign.h>

struct counter {
    alignas(64) unsigned long value;  /* 64バイトアラインメント */
};

/* GCC 属性を使用 */
struct counter {
    unsigned long value;
} __attribute__((aligned(64)));

/* C++ の alignas */
struct alignas(std::hardware_destructive_interference_size) counter {
    unsigned long value;
};

/* Java での対策 (@Contended アノテーション) */
// @sun.misc.Contended
// public volatile long counter;
// JVM オプション: -XX:-RestrictContended

/* Linux カーネルでの標準的なパターン */
#include <linux/cache.h>

struct node_data {
    spinlock_t lock;
    unsigned long counter;
    struct list_head list;
} ____cacheline_aligned_in_smp;

/* DEFINE_PER_CPU は自動的に False Sharing を回避 */
static DEFINE_PER_CPU(unsigned long, my_counter);

17. irqbalance と NUMA

17.1 IRQ と NUMA の関係

# IRQ の CPU 親和性の確認
$ cat /proc/interrupts | head -20
           CPU0       CPU1       CPU2       CPU3  ...
  0:         45          0          0          0   IO-APIC   2-edge      timer
  1:          0          0          9          0   IO-APIC   1-edge      i8042
  8:          0          0          0          1   IO-APIC   8-edge      rtc0
 16:    1234567          0          0          0   IO-APIC  16-fastedge  eth0
 17:          0    2345678          0          0   IO-APIC  17-fastedge  eth1

# 特定の IRQ の CPU 親和性を確認
$ cat /proc/irq/16/smp_affinity
00000001    # CPU 0 のみ

$ cat /proc/irq/16/smp_affinity_list
0           # CPU 0

# IRQ の NUMA ノードを確認
$ cat /proc/irq/16/node
0

17.2 irqbalance デーモン

# irqbalance の設定
$ cat /etc/sysconfig/irqbalance  # RHEL/CentOS
# または
$ cat /etc/default/irqbalance     # Ubuntu/Debian

# irqbalance の動作確認
$ sudo irqbalance --debug --oneshot 2>&1 | head -30
Package 0:  numa_node 0
   Cache domain 0:  [0-31, 64-95]
      CPU 0
      CPU 1
      ...
Package 1:  numa_node 1
   Cache domain 1:  [32-63, 96-127]
      CPU 32
      CPU 33
      ...

# irqbalance のポリシーヒント設定
# /etc/irqbalance.d/ にポリシーファイルを配置

# 特定の IRQ を特定の CPU にピン留め
$ echo 2 > /proc/irq/16/smp_affinity_list   # IRQ 16 を CPU 2 に固定

# NUMA ノード内に IRQ を閉じ込める
# NIC の IRQ を NIC が接続されたノードの CPU に限定
$ NIC_NODE=$(cat /sys/class/net/eth0/device/numa_node)
$ NIC_IRQS=$(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':')
$ NODE_CPUS=$(cat /sys/devices/system/node/node${NIC_NODE}/cpulist)

for irq in $NIC_IRQS; do
    echo "$NODE_CPUS" > /proc/irq/$irq/smp_affinity_list
done

# irqbalance の NUMA ポリシー設定
$ cat /etc/irqbalance.d/numa_policy
# NIC の IRQ をローカルノードに制限
IRQBALANCE_BANNED_CPULIST=""
IRQBALANCE_ONESHOT=no

# 特定の CPU を irqbalance の対象外にする
# (リアルタイムワークロード用の CPU を隔離)
$ echo "IRQBALANCE_BANNED_CPUS=ffff0000" >> /etc/default/irqbalance

17.3 高性能ネットワーキングにおける IRQ と NUMA

# マルチキュー NIC の IRQ 分散(NUMA 最適化)

# NIC のキュー数を確認
$ ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
	RX:		0
	TX:		0
	Other:		1
	Combined:	64
Current hardware settings:
	RX:		0
	TX:		0
	Other:		1
	Combined:	32

# NIC が接続されている NUMA ノードを確認
$ cat /sys/class/net/eth0/device/numa_node
0

# 各キューの IRQ をローカルノードの CPU に分散
#!/bin/bash
NIC="eth0"
NODE=$(cat /sys/class/net/$NIC/device/numa_node)
CPUS=($(cat /sys/devices/system/node/node${NODE}/cpulist | \
    tr ',' '\n' | tr '-' ' ' | while read a b; do
    if [ -n "$b" ]; then seq $a $b; else echo $a; fi
done))

i=0
for irq in $(grep "$NIC-" /proc/interrupts | awk '{print $1}' | tr -d ':'); do
    cpu=${CPUS[$((i % ${#CPUS[@]}))]}
    echo $cpu > /proc/irq/$irq/smp_affinity_list
    echo "IRQ $irq -> CPU $cpu (Node $NODE)"
    ((i++))
done

# RPS (Receive Packet Steering) の NUMA 最適化
$ echo "00ff" > /sys/class/net/eth0/queues/rx-0/rps_cpus
# Node 0 の CPU 0-7 で RPS を処理

# XPS (Transmit Packet Steering) の設定
$ echo "00ff" > /sys/class/net/eth0/queues/tx-0/xps_cpus

18. NUMA パフォーマンスの影響と最適化

18.1 NUMA パフォーマンス問題の典型例

問題パターン 1: リモートメモリアクセスの支配

  アプリケーションスレッドが Node 0 で実行
  メモリの大部分が Node 1 に配置
  → 全アクセスがリモート → 30-100% の性能低下

問題パターン 2: メモリ帯域幅の偏り

  全スレッドが Node 0 のメモリにアクセス
  Node 0 のメモリ帯域が飽和
  Node 1 のメモリ帯域は遊休
  → 利用可能帯域の半分しか使えない

問題パターン 3: NUMA swapping

  Node 0 のメモリが枯渇
  Node 1 にはまだ空きがある
  しかし zone_reclaim_mode が有効だとスワップが発生
  → リモートメモリを使えば回避できたのにスワップ

問題パターン 4: プロセス起動時の first-touch

  大規模アプリケーションが単一スレッドでメモリを初期化
  → 全メモリが1つのノードに集中
  → その後マルチスレッドで実行してもリモートアクセスだらけ

18.2 zone_reclaim_mode の理解と設定

# zone_reclaim_mode: ローカルメモリが不足した場合の動作
$ cat /proc/sys/vm/zone_reclaim_mode
0

# 値の意味:
# 0 (デフォルト): ローカルメモリ不足時にリモートノードからも割り当て
#   → ほとんどのワークロードで推奨
# 1: ローカルノードでのページ回収を試みる
# 2: ローカルノードでダーティページの書き戻しを試みる
# 4: ローカルノードでの swap を試みる

# 一般的な推奨: 0(無効)
$ sysctl -w vm.zone_reclaim_mode=0

# 大量のファイルキャッシュを使うワークロードでは 1 が有効な場合もある
# (HPC、大規模ファイルI/O)
$ sysctl -w vm.zone_reclaim_mode=1

18.3 NUMA 最適化のベストプラクティス

# 1. ワークロードの NUMA 特性を把握
$ numastat -p <PID>

# 2. メモリ配置の確認
$ cat /proc/<PID>/numa_maps | awk '
{
    for(i=1; i<=NF; i++) {
        if ($i ~ /^N[0-9]+=/) {
            split($i, a, "=");
            node = substr(a[1], 2);
            pages[node] += a[2];
            total += a[2];
        }
    }
}
END {
    for (n in pages) {
        pct = pages[n] / total * 100;
        printf "Node %s: %d pages (%.1f%%)\n", n, pages[n], pct;
    }
    printf "Total: %d pages\n", total;
}'

# 3. NUMA ミスの監視
$ watch -n 1 'numastat | tail -3'

# 4. プロセスの CPU バインドの確認
$ taskset -p <PID>
pid 12345's current affinity mask: ffffffffffffffff

# 5. NUMA 対応の起動スクリプト例
#!/bin/bash
# numa_optimized_start.sh

APP=$1
NODE=${2:-0}  # デフォルト Node 0

echo "Starting $APP on NUMA Node $NODE"

# CPU とメモリを同じノードに bind
exec numactl --cpunodebind=$NODE --membind=$NODE $APP

# または interleave で全ノードの帯域を活用
# exec numactl --interleave=all $APP

18.4 NUMA パフォーマンス計測スクリプト

#!/bin/bash
# numa_benchmark.sh - NUMA パフォーマンスの簡易計測

echo "=== NUMA Performance Benchmark ==="
echo "Date: $(date)"
echo "Kernel: $(uname -r)"
echo ""

# NUMA トポロジ
echo "--- NUMA Topology ---"
numactl --hardware
echo ""

# ノード間の遅延計測 (MLC が必要)
if command -v mlc &>/dev/null; then
    echo "--- Memory Latency Matrix ---"
    sudo mlc --latency_matrix
    echo ""
    
    echo "--- Memory Bandwidth Matrix ---"
    sudo mlc --bandwidth_matrix
    echo ""
fi

# STREAM ベンチマーク(各ノード)
if [ -x ./stream_c ]; then
    NODES=$(numactl --hardware | grep "available:" | awk '{print $2}')
    
    for ((node=0; node<NODES; node++)); do
        echo "--- STREAM: CPU Node $node, Mem Node $node (Local) ---"
        numactl --cpunodebind=$node --membind=$node ./stream_c 2>/dev/null | \
            grep -E "^(Copy|Scale|Add|Triad):"
        echo ""
        
        for ((mem=0; mem<NODES; mem++)); do
            if [ $mem -ne $node ]; then
                echo "--- STREAM: CPU Node $node, Mem Node $mem (Remote) ---"
                numactl --cpunodebind=$node --membind=$mem ./stream_c 2>/dev/null | \
                    grep -E "^(Copy|Scale|Add|Triad):"
                echo ""
            fi
        done
    done
fi

# 現在の NUMA 統計
echo "--- Current NUMA Statistics ---"
numastat
echo ""

# vmstat の NUMA 統計
echo "--- NUMA vmstat ---"
grep numa /proc/vmstat

19. 仮想化における NUMA (KVM, QEMU)

19.1 KVM/QEMU の NUMA パススルー

# QEMU でゲストに NUMA トポロジを公開

# 2ノード NUMA 構成のゲスト VM を起動
$ qemu-system-x86_64 \
    -machine type=q35,accel=kvm \
    -cpu host \
    -smp cpus=16,sockets=2,cores=4,threads=2 \
    -m 32G \
    -object memory-backend-ram,size=16G,id=ram-node0,\
        host-nodes=0,policy=bind \
    -object memory-backend-ram,size=16G,id=ram-node1,\
        host-nodes=1,policy=bind \
    -numa node,nodeid=0,cpus=0-7,memdev=ram-node0 \
    -numa node,nodeid=1,cpus=8-15,memdev=ram-node1 \
    -numa dist,src=0,dst=1,val=21 \
    -numa dist,src=1,dst=0,val=21 \
    ...

# Huge Pages を使用する場合
$ qemu-system-x86_64 \
    -machine type=q35,accel=kvm \
    -cpu host \
    -smp cpus=16,sockets=2,cores=4,threads=2 \
    -m 32G \
    -object memory-backend-memfd,size=16G,id=ram-node0,\
        host-nodes=0,policy=bind,hugetlb=on,hugetlbsize=2M \
    -object memory-backend-memfd,size=16G,id=ram-node1,\
        host-nodes=1,policy=bind,hugetlb=on,hugetlbsize=2M \
    -numa node,nodeid=0,cpus=0-7,memdev=ram-node0 \
    -numa node,nodeid=1,cpus=8-15,memdev=ram-node1 \
    ...

19.2 libvirt での NUMA 設定

<!-- /etc/libvirt/qemu/vm-numa.xml -->
<domain type='kvm'>
  <name>numa-aware-vm</name>
  <memory unit='GiB'>32</memory>
  <vcpu placement='static'>16</vcpu>
  
  <!-- CPU トポロジ -->
  <cpu mode='host-passthrough'>
    <topology sockets='2' cores='4' threads='2'/>
    <numa>
      <cell id='0' cpus='0-7' memory='16' unit='GiB'/>
      <cell id='1' cpus='8-15' memory='16' unit='GiB'/>
    </numa>
  </cpu>
  
  <!-- NUMA チューニング -->
  <numatune>
    <!-- ゲスト Node 0 をホスト Node 0 に bind -->
    <memnode cellid='0' mode='strict' nodeset='0'/>
    <!-- ゲスト Node 1 をホスト Node 1 に bind -->
    <memnode cellid='1' mode='strict' nodeset='1'/>
  </numatune>
  
  <!-- vCPU のピン留め -->
  <cputune>
    <!-- ゲスト vCPU をホストの対応する NUMA ノードの CPU にピン -->
    <vcpupin vcpu='0' cpuset='0'/>
    <vcpupin vcpu='1' cpuset='1'/>
    <vcpupin vcpu='2' cpuset='2'/>
    <vcpupin vcpu='3' cpuset='3'/>
    <vcpupin vcpu='4' cpuset='4'/>
    <vcpupin vcpu='5' cpuset='5'/>
    <vcpupin vcpu='6' cpuset='6'/>
    <vcpupin vcpu='7' cpuset='7'/>
    <vcpupin vcpu='8' cpuset='32'/>
    <vcpupin vcpu='9' cpuset='33'/>
    <vcpupin vcpu='10' cpuset='34'/>
    <vcpupin vcpu='11' cpuset='35'/>
    <vcpupin vcpu='12' cpuset='36'/>
    <vcpupin vcpu='13' cpuset='37'/>
    <vcpupin vcpu='14' cpuset='38'/>
    <vcpupin vcpu='15' cpuset='39'/>
    
    <!-- エミュレータスレッドのピン留め -->
    <emulatorpin cpuset='62-63'/>
  </cputune>
  
  <!-- Huge Pages -->
  <memoryBacking>
    <hugepages>
      <page size='2048' unit='KiB' nodeset='0-1'/>
    </hugepages>
    <locked/>
    <nosharepages/>
  </memoryBacking>
  
  <!-- ... -->
</domain>

19.3 NUMA と仮想化の最適化ガイドライン

# 1. ホストの NUMA トポロジを確認
$ virsh capabilities | grep -A20 "topology"

# 2. ゲストの NUMA 配置を確認
$ virsh numatune <vm-name>

# 3. vCPU のピン留めを確認
$ virsh vcpupin <vm-name>

# 4. emulatorpin の確認
$ virsh emulatorpin <vm-name>

# 5. ゲスト内の NUMA トポロジ確認
# (ゲスト内で実行)
$ numactl --hardware
$ lscpu

# 6. ホスト側で VM のNUMA 使用状況を確認
$ for pid in $(pgrep -f qemu-system); do
    echo "=== VM PID: $pid ==="
    numastat -p $pid
    echo ""
done

# --- NUMA 仮想化のベストプラクティス ---

# ホストの Huge Pages 設定 (ノードごと)
$ echo 8192 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
$ echo 8192 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages

# NUMA バランシングを VM ホストでは無効化(vCPU ピン留めと競合する)
$ sysctl -w kernel.numa_balancing=0

# KSM (Kernel Same-page Merging) を無効化(NUMA 配置を壊す)
$ echo 0 > /sys/kernel/mm/ksm/run

# THP (Transparent Huge Pages) の検討
$ echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

20. データベースの実用的 NUMA チューニング (MySQL, PostgreSQL)

20.1 MySQL/InnoDB の NUMA 最適化

# MySQL の NUMA 問題
# InnoDB Buffer Pool が1つのNUMAノードに集中 → 性能低下

# 方法 1: numactl で interleave 起動
$ numactl --interleave=all mysqld_safe &

# 方法 2: MySQL 設定ファイル (my.cnf)
[mysqld]
# MySQL 5.6.27+ / 5.7.9+
innodb_numa_interleave = ON

# 大規模インスタンスの推奨設定
innodb_buffer_pool_size = 100G
innodb_buffer_pool_instances = 16  # NUMA ノード数 x 8 程度
innodb_buffer_pool_chunk_size = 1G

# I/O スレッドの設定
innodb_read_io_threads = 16
innodb_write_io_threads = 16

# 方法 3: systemd で設定
$ cat /etc/systemd/system/mysqld.service.d/numa.conf
[Service]
ExecStart=
ExecStart=/usr/bin/numactl --interleave=all /usr/sbin/mysqld
# MySQL の NUMA パフォーマンス確認

# InnoDB Buffer Pool の使用状況
mysql> SHOW ENGINE INNODB STATUS\G
...
Buffer pool size        6553600
Buffer pool size, bytes 107374182400
Free buffers            1234567
Database pages          5319033
...

# MySQL プロセスの NUMA 配置
$ numastat -p $(pidof mysqld)
Per-node process memory usage (in MBs) for PID 12345 (mysqld)
                           Node 0          Node 1           Total
                  --------------- --------------- ---------------
Huge                         0.00            0.00            0.00
Heap                     51234.56        50123.45       101358.01
Stack                        0.50            0.50            1.00
Private                    234.56          223.45          458.01
----------------  --------------- --------------- ---------------
Total                    51469.62        50347.40       101817.02
# interleave により均等に分散

20.2 PostgreSQL の NUMA 最適化

# PostgreSQL の NUMA 設定

# 方法 1: numactl で interleave 起動
$ numactl --interleave=all pg_ctl start -D /var/lib/postgresql/data

# 方法 2: systemd で設定
$ cat /etc/systemd/system/postgresql.service.d/numa.conf
[Service]
ExecStart=
ExecStart=/usr/bin/numactl --interleave=all /usr/bin/postgres -D /var/lib/postgresql/data

# 方法 3: 大規模システムではノード分割戦略
# 接続プーラー (pgbouncer) を各ノードで稼働
# Node 0: PostgreSQL 本体 (shared_buffers)
# Node 1: WAL 処理と OS キャッシュ

# postgresql.conf の NUMA 最適化設定
shared_buffers = 64GB          # 全メモリの 25-40%
effective_cache_size = 192GB   # OS キャッシュ含む
work_mem = 256MB               # ソートやハッシュの作業メモリ
maintenance_work_mem = 2GB     # VACUUM 等の作業メモリ
huge_pages = try               # Huge Pages を使用

# WAL 設定
wal_buffers = 64MB
checkpoint_completion_target = 0.9
max_wal_size = 8GB

# 並列クエリ(NUMA ノード数を考慮)
max_parallel_workers_per_gather = 8
max_parallel_workers = 32
max_worker_processes = 64
# PostgreSQL の Huge Pages 設定

# 1. 必要な Huge Pages 数を計算
$ head -1 /proc/$(cat /var/lib/postgresql/data/postmaster.pid)/maps | \
    awk '{print "0x" $1}' | tr '-' ' '

$ grep ^VmPeak /proc/$(cat /var/lib/postgresql/data/postmaster.pid)/status
VmPeak:  68157440 kB

# 必要な 2MB Huge Pages 数: 68157440 / 2048 = 33280 + マージン
$ sysctl -w vm.nr_hugepages=34000

# NUMA ノードごとに Huge Pages を均等割り当て
$ echo 17000 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
$ echo 17000 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages

# 2. PostgreSQL ユーザーの権限設定
$ grep huge /proc/meminfo
HugePages_Total:   34000
HugePages_Free:    34000
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

# 3. postgresql.conf で有効化
# huge_pages = on

20.3 データベース NUMA チューニングのまとめ

データベース NUMA チューニングチェックリスト:

□ numactl --interleave=all でデータベースプロセスを起動
  理由: shared buffer が均等に分散され、メモリ帯域を最大化

□ Huge Pages を有効化
  理由: TLB ミスの削減、メモリ管理オーバーヘッドの削減

□ NUMA バランシングを無効化(明示的バインドの場合)
  理由: 不要なページマイグレーションのオーバーヘッド回避

□ zone_reclaim_mode = 0 を設定
  理由: ローカルメモリ不足時にスワップではなくリモート割り当て

□ IRQ をデータベース CPU と同じノードに配置
  理由: ネットワーク/ストレージ I/O のローカリティ向上

□ I/O スケジューラを none/noop に設定 (NVMe)
  理由: 不要なI/Oスケジューリングオーバーヘッドの排除

□ vm.swappiness = 1 に設定
  理由: できる限りスワップを回避(0 は OOM Killer が過敏に反応)

□ Transparent Huge Pages (THP) を madvise に設定
  理由: 予期しないレイテンシスパイクの回避

21. ツール: numactl, numastat, lstopo, hwloc, lscpu, turbostat

21.1 numactl

# numactl のインストール
$ sudo apt install numactl       # Debian/Ubuntu
$ sudo yum install numactl       # RHEL/CentOS
$ sudo dnf install numactl       # Fedora

# --- システム情報 ---
$ numactl --hardware
$ numactl --show

# --- 実行制御 ---

# CPU バインド
$ numactl --cpunodebind=0 ./app      # Node 0 の CPU で実行
$ numactl --physcpubind=0-7 ./app    # CPU 0-7 で実行

# メモリポリシー
$ numactl --membind=0 ./app          # Node 0 のメモリのみ
$ numactl --interleave=all ./app     # 全ノードに分散
$ numactl --preferred=1 ./app        # Node 1 を優先
$ numactl --localalloc ./app         # ローカルノード

# 組み合わせ
$ numactl --cpunodebind=0 --membind=0 ./app
$ numactl --cpunodebind=0,1 --interleave=0,1 ./app

# 既存プロセスへの適用(migratepages)
$ numactl --hardware   # ノード確認
$ migratepages <PID> 0 1  # PID のページを Node 0 → Node 1 へ移動

21.2 numastat

# numastat のインストール (numactl パッケージに含まれる)

# --- システム全体の統計 ---
$ numastat
                           node0           node1
numa_hit              1234567890      1123456789
numa_miss                1234567        1123456
numa_foreign             1123456        1234567
interleave_hit          12345678       11234567
local_node            1223456789      1112345678
other_node              12345678       12234567

# --- プロセス別統計 ---
$ numastat -p <PID>
$ numastat -p $(pidof mysqld)

# --- メモリ統計 (meminfo 形式) ---
$ numastat -m
                          Node 0          Node 1           Total
                 --------------- --------------- ---------------
MemTotal                131072.00       131072.00       262144.00
MemFree                  98765.43        99876.54       198641.97
MemUsed                  32306.57        31195.46        63502.03
Active                   23456.78        22345.67        45802.45
Inactive                  6789.01         6543.21        13332.22
Active(anon)             18765.43        17654.32        36419.75
Inactive(anon)            1234.56         1123.45         2358.01
Active(file)              4691.35         4691.35         9382.70
Inactive(file)            5554.45         5419.76        10974.21
Unevictable                123.45          112.34          235.79
Mlocked                    123.45          112.34          235.79
Dirty                       45.67           34.56           80.23
Writeback                    0.00            0.00            0.00
FilePages                 10567.89        10345.67        20913.56
Mapped                     2345.67         2234.56         4580.23
AnonPages                 19876.54        18654.32        38530.86
Shmem                       567.89          456.78         1024.67
KernelStack                 123.45          112.34          235.79
PageTables                  234.56          223.45          458.01
Bounce                       0.00            0.00            0.00
AnonHugePages              4096.00         4096.00         8192.00
HugePages_Total              0.00            0.00            0.00
HugePages_Free               0.00            0.00            0.00

# --- 特定プロセスのメモリ使用を継続監視 ---
$ watch -n 1 "numastat -p $(pidof postgres) 2>/dev/null"

# --- 全プロセスの NUMA 使用サマリ ---
$ numastat -c

21.3 lstopo / hwloc

# hwloc のインストール
$ sudo apt install hwloc          # Debian/Ubuntu
$ sudo yum install hwloc hwloc-gui  # RHEL/CentOS

# --- トポロジ表示 ---
$ lstopo-no-graphics              # テキスト形式
$ lstopo topology.png             # PNG 画像
$ lstopo topology.svg             # SVG 画像
$ lstopo topology.xml             # XML 形式

# --- hwloc-info ---
$ hwloc-info
$ hwloc-info --no-io              # I/O デバイスを除外
$ hwloc-info --only core          # コアのみ
$ hwloc-info --only pu            # 論理CPU のみ
$ hwloc-info --only numanode      # NUMA ノードのみ

# --- hwloc-calc: CPU セットの計算 ---
$ hwloc-calc NUMANode:0           # Node 0 の全 CPU
$ hwloc-calc NUMANode:0.Core:0    # Node 0 の Core 0
$ hwloc-calc PU:0 PU:1 PU:2      # 特定の論理 CPU

# --- hwloc-bind: プロセスのバインド ---
$ hwloc-bind node:0 -- ./my_app   # Node 0 にバインドして実行
$ hwloc-bind core:0-3 -- ./app    # Core 0-3 にバインド
$ hwloc-bind node:0.pu:0-3 -- ./app

# --- hwloc-distances: ノード間距離 ---
$ hwloc-distances
Relative latency matrix (name NUMALatency kind 5) between 2 NUMANodes (depth 2):
  index     0     1
      0 1.000 2.100
      1 2.100 1.000

# --- hwloc-ps: プロセスのバインド状態表示 ---
$ hwloc-ps -a
  PID   NAME                  CPUSET
    1   systemd               0x0000ffff,0xffffffff
 1234   mysqld                0x0000ffff
 2345   postgres              0xffff0000

# --- hwloc-gather-topology: トポロジ情報の収集 ---
$ sudo hwloc-gather-topology /tmp/my-topology
# → /tmp/my-topology.tar.bz2 が生成される(他のマシンでの分析用)

21.4 lscpu

# --- 基本情報 ---
$ lscpu
Architecture:            x86_64
CPU(s):                  128
On-line CPU(s) list:     0-127
Thread(s) per core:      2
Core(s) per socket:      32
Socket(s):               2
NUMA node(s):            2
...

# --- 拡張表示 ---
$ lscpu -e
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE MAXMHZ  MINMHZ
  0    0      0    0  0:0:0:0         yes 3400.00  800.00
  1    0      0    1  1:1:1:0         yes 3400.00  800.00
...

# --- JSON 出力 ---
$ lscpu -J

# --- パースしやすい形式 ---
$ lscpu -p=CPU,Node,Socket,Core,ONLINE
# CPU,Node,Socket,Core,ONLINE
0,0,0,0,Y
1,0,0,1,Y
...

# --- キャッシュ情報 ---
$ lscpu -C
NAME ONE-SIZE ALL-SIZE WAYS TYPE        LEVEL SETS PHY-LINE COHERENCY-SIZE
L1d       48K      3M   12 Data            1   64        1             64
L1i       32K      2M    8 Instruction     1   64        1             64
L2      1280K     80M   20 Unified         2 1024        1             64
L3        60M    120M   12 Unified         3 65536        1             64

21.5 turbostat

# turbostat のインストール (linux-tools パッケージ)
$ sudo apt install linux-tools-$(uname -r)

# --- 基本表示 ---
$ sudo turbostat
Core CPU  Avg_MHz  Busy%  Bzy_MHz  TSC_MHz  IRQ  C1    C1E   C6    POLL  C1    C1E   C6
-    -      1234   45.6   2800     2300     5678 12.3  23.4  42.1   0.1   0.2   1.3  45.0
 0    0     2345   67.8   3100     2300      456  8.9  15.6  23.3
 0   64      890   34.5   2600     2300      234 15.6  28.9  49.9
...

# --- 特定のフィールドのみ表示 ---
$ sudo turbostat --show Core,CPU,Avg_MHz,Busy%,Bzy_MHz,PkgTmp,PkgWatt,RAMWatt

# --- 継続監視 ---
$ sudo turbostat --interval 5  # 5秒間隔

# --- コマンドの実行時間中の統計 ---
$ sudo turbostat ./my_benchmark

# --- パッケージ (ソケット) ごとの電力情報 ---
$ sudo turbostat --show Package,PkgWatt,CorWatt,RAMWatt --quiet
Package PkgWatt CorWatt RAMWatt
0       180.50  145.30   25.40
1       175.80  140.20   24.60

21.6 その他の有用なツール

# --- Intel PCM (Processor Counter Monitor) ---
# https://github.com/intel/pcm
$ sudo pcm             # CPU カウンタモニタ
$ sudo pcm-memory      # メモリ帯域モニタ(NUMA 対応)
$ sudo pcm-numa        # NUMA トラフィックモニタ
$ sudo pcm-pcie        # PCIe 帯域モニタ

# --- Intel MLC (Memory Latency Checker) ---
$ sudo mlc --latency_matrix     # メモリ遅延マトリックス
$ sudo mlc --bandwidth_matrix   # メモリ帯域マトリックス
$ sudo mlc --idle_latency       # アイドル時レイテンシ
$ sudo mlc --loaded_latency     # 負荷時レイテンシ

# --- perf による NUMA 分析 ---
$ sudo perf stat -e \
    node-loads,node-load-misses,\
    node-stores,node-store-misses \
    -p <PID> sleep 10

$ sudo perf c2c record -p <PID> sleep 10
$ sudo perf c2c report

# --- bpftrace による NUMA モニタリング ---
$ sudo bpftrace -e '
tracepoint:migrate:mm_migrate_pages {
    @migrations[args->mode] = count();
}
interval:s:5 {
    print(@migrations);
    clear(@migrations);
}'

# --- /proc/vmstat の NUMA 統計 ---
$ grep numa /proc/vmstat

22. トラブルシューティングガイド

22.1 一般的な NUMA 問題の診断フロー

#!/bin/bash
# numa_diagnose.sh - NUMA 問題の診断スクリプト

echo "=== NUMA Diagnostics ==="
echo "Date: $(date)"
echo ""

# 1. NUMA トポロジの確認
echo "--- 1. NUMA Topology ---"
numactl --hardware
echo ""

# 2. メモリ配置の偏り確認
echo "--- 2. Memory Balance ---"
numastat -m | grep -E "MemTotal|MemFree|MemUsed"
echo ""

# 3. NUMA ヒット/ミス率
echo "--- 3. NUMA Hit/Miss Ratio ---"
numastat | awk '
NR==1 { for(i=1;i<=NF;i++) nodes[i]=$i; next }
/numa_hit/  { for(i=2;i<=NF;i++) hit[i]=$i }
/numa_miss/ { for(i=2;i<=NF;i++) miss[i]=$i }
END {
    for(i=2;i<=NF;i++) {
        total = hit[i] + miss[i];
        if (total > 0) {
            pct = hit[i] / total * 100;
            printf "%s: hit=%s miss=%s hit_ratio=%.1f%%\n",
                nodes[i], hit[i], miss[i], pct;
        }
    }
}'
echo ""

# 4. 主要プロセスの NUMA 配置
echo "--- 4. Top Processes NUMA Distribution ---"
for pid in $(ps -eo pid,rss --sort=-rss | head -6 | tail -5 | awk '{print $1}'); do
    name=$(ps -p $pid -o comm= 2>/dev/null)
    if [ -n "$name" ]; then
        echo "PID: $pid ($name)"
        numastat -p $pid 2>/dev/null | tail -3
        echo ""
    fi
done

# 5. zone_reclaim_mode の確認
echo "--- 5. Zone Reclaim Mode ---"
echo "zone_reclaim_mode = $(cat /proc/sys/vm/zone_reclaim_mode)"
echo "(0=disabled recommended for most workloads)"
echo ""

# 6. NUMA バランシングの状態
echo "--- 6. NUMA Balancing ---"
echo "numa_balancing = $(cat /proc/sys/kernel/numa_balancing)"
echo ""

# 7. NUMA 関連の vmstat
echo "--- 7. NUMA vmstat ---"
grep numa /proc/vmstat
echo ""

# 8. NUMA ページマイグレーション統計
echo "--- 8. Page Migration Stats ---"
grep -E "pgmigrate|numa" /proc/vmstat
echo ""

echo "=== Diagnosis Complete ==="

22.2 よくある問題と対処法

問題 1: numastat で numa_miss が多い
─────────────────────────────
原因: リモートメモリアクセスが頻発
対処:
  - numactl --membind でメモリを適切なノードに配置
  - numactl --interleave でメモリを均等分散
  - アプリケーションの初期化コードで NUMA を意識

問題 2: 1つのノードのメモリが枯渇、他は余裕あり
─────────────────────────────
原因: first-touch によるメモリ偏り
対処:
  - zone_reclaim_mode=0 に設定(リモート割り当てを許可)
  - numactl --interleave で起動
  - NUMA バランシングを有効化

問題 3: データベースの性能が不安定
─────────────────────────────
原因: NUMA による不均一なメモリアクセス
対処:
  - innodb_numa_interleave=ON (MySQL)
  - numactl --interleave=all で起動
  - Huge Pages の有効化
  - THP を madvise に設定
  
問題 4: VM のパフォーマンスが低い
─────────────────────────────
原因: vCPU とメモリが異なる NUMA ノードに配置
対処:
  - vCPU を適切なホスト CPU にピン留め
  - ゲストメモリをホストの同じノードに bind
  - Huge Pages の使用
  - ホストの NUMA バランシングを無効化

23. まとめとベストプラクティス

23.1 NUMA 最適化のまとめ

NUMA 最適化の黄金律:

1. データとそれを処理する CPU を同じ NUMA ノードに配置する
2. first-touch を意識してメモリ初期化を行う
3. 共有データは interleave で均等分散する
4. ワークロードに応じて適切なメモリポリシーを選択する
5. 計測→分析→最適化のサイクルを回す

23.2 ワークロード別推奨設定

ワークロード           CPU バインド     メモリポリシー    NUMA バランシング
──────────────────────────────────────────────────────────────────────
データベース (OLTP)    特定ノード       interleave        無効
データベース (OLAP)    全ノード         interleave        有効
Web サーバー           全ノード         default           有効
HPC / 科学計算         特定ノード       bind              無効
VM ホスト (KVM)        ピン留め         bind              無効
コンテナ (Kubernetes)  cpuset           default           有効
キャッシュサーバー     全ノード         interleave        無効
メッセージキュー       特定ノード       bind              無効
ストリーム処理         全ノード         interleave        有効

23.3 カーネルパラメータのまとめ

# /etc/sysctl.d/99-numa-tuning.conf

# NUMA バランシング
kernel.numa_balancing = 1
kernel.numa_balancing_scan_delay_ms = 1000
kernel.numa_balancing_scan_period_min_ms = 1000
kernel.numa_balancing_scan_period_max_ms = 60000
kernel.numa_balancing_scan_size_mb = 256

# メモリ管理
vm.zone_reclaim_mode = 0
vm.swappiness = 10
vm.dirty_ratio = 40
vm.dirty_background_ratio = 10
vm.min_free_kbytes = 1048576

# Huge Pages (必要に応じて)
# vm.nr_hugepages = 16384

# スケジューラ
kernel.sched_migration_cost_ns = 5000000
kernel.sched_min_granularity_ns = 3000000
kernel.sched_wakeup_granularity_ns = 4000000

23.4 チェックリスト

NUMA 対応システムの構築チェックリスト:

基本設定:
□ NUMA トポロジの把握 (numactl --hardware)
□ CPU トポロジの把握 (lscpu, lstopo)
□ BIOS/UEFI の NUMA 設定確認 (NPS, SNC)
□ カーネルの NUMA サポート確認

メモリ:
□ メモリの均等搭載 (全チャネル、全ノード)
□ zone_reclaim_mode の設定
□ Huge Pages の検討
□ THP の設定 (madvise 推奨)
□ swappiness の調整

アプリケーション:
□ ワークロード特性の分析
□ 適切な NUMA ポリシーの選択
□ numactl による起動設定
□ アプリケーション固有の NUMA 設定

監視:
□ numastat の定期監視
□ /proc/vmstat の NUMA 統計
□ perf による詳細分析
□ 帯域幅・遅延の計測

仮想化:
□ vCPU ピン留め
□ メモリの NUMA ノード配置
□ Huge Pages の使用
□ ホスト側 NUMA バランシング無効化
□ KSM の検討

24. 参考文献

書籍

  • Mel Gorman, "Understanding the Linux Virtual Memory Manager", Prentice Hall, 2004
  • Daniel P. Bovet, Marco Cesati, "Understanding the Linux Kernel", 3rd Edition, O'Reilly, 2005
  • Robert Love, "Linux Kernel Development", 3rd Edition, Addison-Wesley, 2010
  • Brendan Gregg, "Systems Performance: Enterprise and the Cloud", 2nd Edition, Addison-Wesley, 2020

カーネルドキュメント

  • Documentation/admin-guide/mm/numa_memory_policy.rst
  • Documentation/admin-guide/mm/numaperf.rst
  • Documentation/scheduler/sched-domains.rst
  • Documentation/vm/page_migration.rst
  • Documentation/core-api/cpu_hotplug.rst

オンラインリソース

RFC / 仕様

  • ACPI Specification (SRAT, SLIT tables)
  • Intel 64 and IA-32 Architectures Software Developer's Manual
  • AMD64 Architecture Programmer's Manual

本ドキュメントは Linux カーネル 6.x 系列を基に作成されています。カーネルバージョンによって一部の機能や設定値が異なる場合があります。