NUMA

In multi-socket servers, not all RAM is equally fast. Each CPU socket has RAM physically attached to it (local memory) and can also access RAM attached to other sockets (remote memory) — but remote access is 50-100% slower due to the interconnect. NUMA (Non-Uniform Memory Access) is the architecture; Linux's NUMA scheduler tries to keep processes on CPUs near their memory.

NUMA Topology

Dual-socket server (2 NUMA nodes): Socket 0 (NUMA node 0): Socket 1 (NUMA node 1): CPU cores 0-23 CPU cores 24-47 Local RAM: 128GB Local RAM: 128GB | | +------------ QPI/UPI ------------+ (inter-socket interconnect) Access latency: Node 0 CPU reading Node 0 RAM: ~70ns (local) Node 0 CPU reading Node 1 RAM: ~140ns (remote, 2x slower!) # Real 4-socket server: 4 NUMA nodes # Remote access through 2 hops: 3x slower than local

Discovering NUMA Topology

# numactl (install: apt install numactl) numactl --hardware # available: 2 nodes (0-1) # node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 24 25 26 27 28 29 30 31 32 33 34 35 # node 0 size: 128000 MB # node 0 free: 98765 MB # node 1 cpus: 12 13 14 15 16 17 18 19 20 21 22 23 36 37 38 39 40 41 42 43 44 45 46 47 # node 1 size: 128000 MB # node 1 free: 76543 MB # node distances: # node 0 1 # 0: 10 21 ← local=10 (relative), remote=21 (2.1x slower) # 1: 21 10 # lscpu shows NUMA info: lscpu | grep NUMA # NUMA node(s): 2 # NUMA node0 CPU(s): 0-23,48-71 # NUMA node1 CPU(s): 24-47,72-95 # Kernel NUMA stats: cat /proc/zoneinfo | grep -A5 "Node 0"

Binding Processes to NUMA Nodes

# Run a process on specific NUMA node (CPUs + memory local to that node): numactl --cpunodebind=0 --membind=0 myapp # All CPUs from node 0, all memory from node 0 # Preferred node (use local, fall back to remote if needed): numactl --preferred=0 myapp # Interleave memory across nodes (for large working sets): numactl --interleave=all myapp # Alternates allocation between nodes — averages out latency # Bind to specific CPUs: numactl --physcpubind=0,1,2,3 myapp taskset -c 0-3 myapp # alternative using taskset # Check which NUMA node a process is using: numastat -p myapp # Per-node process memory usage # Node 0 1 # Huge 0 0 # Heap 1024 0 ← all heap on node 0 (good if running on node 0)

Linux NUMA Memory Policies

# Default policy: local allocation # Memory allocated on the node where the CPU is currently running # Works well when process stays on one node # NUMA allocation policies (set per-thread via syscall or numactl): # MPOL_DEFAULT = local node allocation (default) # MPOL_BIND = must allocate on specified nodes # MPOL_PREFERRED = prefer specified node, fall back if full # MPOL_INTERLEAVE = round-robin across nodes # Check kernel NUMA behavior: cat /proc/sys/vm/zone_reclaim_mode # 0 = try other nodes before reclaiming (default, usually best) # 1 = reclaim local memory before going remote (NUMA strict) # NUMA balancing (automatic migration): cat /proc/sys/kernel/numa_balancing # 1 = enabled (kernel migrates pages to the node accessing them most) # numastat — see remote vs local allocations: numastat # node node0 node1 # numa_hit 9876543 8765432 (allocations that went to preferred node) # numa_miss 123456 234567 (allocations that went to wrong node) # local_node 9000000 8000000 (allocated on running CPU's node) # other_node 876543 765432 (allocated on remote node)

NUMA and Databases

Why do databases like PostgreSQL have NUMA configuration options? A database process with a large shared buffer pool (say, 64GB) that spans NUMA nodes will have roughly half its memory accesses going to remote nodes — halving the effective memory bandwidth. PostgreSQL's huge_pages setting and Linux's NUMA interleave policy help. Many DBAs pin PostgreSQL to a single NUMA node or use numactl --interleave=all for large SMP systems.
# Run PostgreSQL with NUMA interleaving: numactl --interleave=all pg_ctlcluster 14 main start # Or in systemd service override: # [Service] # ExecStart= # ExecStart=numactl --interleave=all /usr/lib/postgresql/14/bin/postgres ... # Check PostgreSQL NUMA memory distribution: numastat -p postgres