Learning Platform
Глоссарий Troubleshooting
Урок 07.05 · 18 мин
Начальный
NUMAMemoryLinuxnumactlMulti-socket

NUMA — зачем знать про non-uniform memory access

Если у вас сервер с двумя или более CPU sockets (то есть несколькими физическими CPU чипами), вы сталкиваетесь с NUMA — Non-Uniform Memory Access. Памяти на сервере много (256 GB, 1 TB), и она физически распределена: часть «принадлежит» CPU 0, часть — CPU 1. Доступ CPU 0 к своей памяти быстрее, чем к памяти CPU 1.

Многие production-проблемы вокруг high-memory систем — из-за NUMA. Postgres медленнее на 2-socket сервере, чем ожидаешь. Spark executor работает в 2 раза быстрее на single-socket. Странные latency spikes в Redis, который не должен иметь спайков. Это всё про NUMA.

В этом уроке — что такое NUMA, как Linux её видит, как использовать numactl, и когда NUMA topology critical в data engineering.


NUMA: память локальная и удалённая

В однопроцессорной системе (один CPU socket) вся RAM подключена к этому CPU через memory controller. Время доступа примерно одинаково ко всем адресам — это UMA (Uniform Memory Access).

В многосокетных системах CPU connected через interconnect (Intel UPI, AMD Infinity Fabric). Каждый CPU имеет свой memory controller и свою «локальную» память. Доступ CPU 0 к локальной памяти — быстрый (60-100 ns). Доступ CPU 0 к памяти, физически подключенной к CPU 1 — идёт через interconnect (130-300 ns) — remote access.

NUMA: local vs remote memory
CPU Node 0Socket 0. 32 cores. Свой memory controller. Прямо подключены 128 GB RAM (local memory)
InterconnectUPI (Intel) или Infinity Fabric (AMD). Связывает CPU sockets. Bandwidth GB/s, latency 50-200 ns
CPU Node 1Socket 1. 32 cores. Свой memory controller. 128 GB local
CPU 0 -> local RAMПрямо через свой memory controller. ~100 ns latency, full bandwidth
CPU 0 -> remote RAMЧерез interconnect к CPU 1's memory controller. ~150-300 ns latency, ограниченный bandwidth

Числа: остальной access обычно в 1.5-2x медленнее. Bandwidth тоже ограничен — если все CPU 0 ядра шарят память на CPU 1, interconnect забит.

# Узнать NUMA topology:
numactl --hardware
# available: 2 nodes (0-1)
# node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
# node 0 size: 128928 MB  -- 128 GB local
# node 0 free: 100000 MB
# node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
# node 1 size: 130000 MB
# node 1 free: 110000 MB
# node distances:
# node   0   1
#   0:  10  21       <- 0 to 0 baseline, 0 to 1 ~2x
#   1:  21  10

node distances — относительная стоимость. 10 — baseline (local). 21 — примерно 2x медленнее (remote). Иногда показывают 32-40 для 4-socket систем.


Linux NUMA policy — куда выделять память

Когда процесс делает malloc() на NUMA-системе, где kernel физически выделяет страницы? Это решает NUMA policy:

NUMA policies в Linux
defaultDefault policy. Выделять с node, где процесс сейчас выполняется (first-touch policy). Если процесс на CPU 0, malloc даёт страницы из node 0
bindЖёстко привязать процесс к node. numactl --membind=0 process -- все аллокации только из node 0
preferredПредпочитать конкретный node, но fallback на другой при нехватке. numactl --preferred=0 process
interleaveRound-robin между nodes. Каждая следующая allocation на следующем node. Хорошо для bandwidth, плохо для latency
# Запустить процесс с привязкой к node 0:
numactl --cpunodebind=0 --membind=0 ./my-app
# CPU только на node 0 cores, память только из node 0 -- максимально cache-friendly

# Только память на node 0, CPU где угодно:
numactl --membind=0 ./my-app

# Interleave (для bandwidth-heavy):
numactl --interleave=all ./my-app

# Текущая policy процесса:
numactl --show
# policy: default
# preferred node: current
# physcpubind: 0 1 2 3 ...
# cpubind: 0 1
# membind: 0 1

Когда NUMA проблема

В большинстве случаев Linux работает «достаточно хорошо» с default first-touch policy. Проблемы возникают в:

NUMA проблемные сценарии
Big single-process appsБД с большим shared_buffers, JVM heap >32GB, ML inference servers. Память аллоцируется в одном node, threads могут быть на другом
Thread migrationLinux scheduler может перенести thread с одного CPU на другой. Если thread изначально аллоцировал на node 0, потом мигрировал на node 1 -- его память remote. Latency растёт
Bandwidth-boundMemory bandwidth bound -- например, ML training, аналитика. Если все cores читают из одного node -- interconnect bottleneck
Page cacheЕсли file читается на node 0, кэш в node 0. Если другой процесс на node 1 читает тот же файл -- remote access to cache. Может быть проблемой

Типичный паттерн:

  1. Запускаете PostgreSQL с shared_buffers = 32 GB на 64-core 2-socket сервере.
  2. Постгрес backend process аллоцирует shared_buffers большую часть на node 0 (где запустился init процесс).
  3. Connection приходит на CPU 33 (node 1). Backend обрабатывает.
  4. Каждый доступ к shared_buffers — remote access, ~200 ns вместо 100 ns.
  5. Производительность в 1.5-2x хуже, чем на single-socket.
Requests и limits: как Kubernetes управляет ресурсами с учётом NUMA

Инструменты для NUMA

numastat — статистика по node

# Stats по nodes:
numastat -m
# Per-node memory statistics
#                     Node 0       Node 1
# MemTotal:        131072.0     131072.0   <- 128 GB на каждом
# MemFree:           5000.0       8000.0
# MemUsed:         126072.0     123072.0

# Per-process NUMA:
numastat -p PID
# Per-node process memory usage
# Node 0   Node 1   Total
# 12000.0   8000.0  20000.0    <- 60% на node 0, 40% на node 1

Watch — сколько remote access

# perf для подсчёта remote vs local DRAM access (Intel):
perf stat -e node-loads,node-load-misses ./my-app
# Или специфичные events:
perf list | grep -i numa
# uncore_imc_X/cas_count_read - чтения memory controller

# bcc tools (eBPF):
sudo /usr/share/bcc/tools/numamove
# Показывает migration статистику

Сравнение local vs remote

# Простой benchmark:
# Запуск с обязательным local memory:
numactl --cpunodebind=0 --membind=0 sysbench memory --memory-block-size=4M --memory-total-size=10G run
# Mem operations: 5000 MB/s

# Запуск с remote (CPU node 0, memory node 1):
numactl --cpunodebind=0 --membind=1 sysbench memory --memory-block-size=4M --memory-total-size=10G run
# Mem operations: 3000 MB/s    <- 1.5x slower

NUMA balancing — автоматическая миграция

Linux имеет automatic NUMA balancing — kernel сам пытается мигрировать страницы и threads, чтобы они были на одном node.

Auto NUMA balancing
Periodic samplingKernel периодически (каждые 1-2 сек) сэмплирует обращения к памяти процессов. Помечает страницы PROT_NONE -- следующий доступ вызывает 'NUMA hint' fault
If remote access detectedЕсли страница на node A, обращается thread на CPU node B -- это remote access
Migrate pageKernel мигрирует страницу с node A на node B -- ближе к потоку. Или мигрирует thread на node A -- ближе к данным
Trade-offМиграции дорогие. На latency-sensitive системах могут вызывать spikes. Иногда лучше отключить
# Включён ли NUMA balancing:
sysctl kernel.numa_balancing
# 1 -- включён по умолчанию

# Отключить (для latency-critical workloads):
sudo sysctl kernel.numa_balancing=0

# Статистика NUMA balancing:
grep numa /proc/vmstat
# numa_pages_migrated
# numa_pte_updates
# numa_huge_pte_updates

Best practices для DE

1. Знайте свой топология

Перед deploy на multi-socket — проверьте, что вы знаете:

  • Сколько NUMA nodes
  • Сколько cores и RAM на каждом
  • node distances
numactl --hardware
lscpu | grep -i numa

2. Используйте —cpunodebind для memory-bound workloads

# PostgreSQL backend:
numactl --cpunodebind=0 --membind=0 postgres -D /var/lib/postgresql/data

# Spark executor:
numactl --cpunodebind=0 --membind=0 ./spark-executor.sh

# Redis:
numactl --cpunodebind=0 --membind=0 redis-server

Это говорит kernel: запускать процесс только на cores node 0, аллокации только из node 0. Никаких remote accesses.

3. Для bandwidth-heavy — interleave

Если ваш workload — ML training или OLAP scan — больше bandwidth важнее низкого latency. --interleave=all:

numactl --interleave=all ./ml-train
# Аллокации round-robin между nodes -- утилизирует bandwidth обоих

4. Контейнеры и Kubernetes

Docker по умолчанию не учитывает NUMA. На многосокетных нодах поды могут страдать. Решения:

  • Kubernetes CPU Manager с static policy + NumaAffinity (от 1.32+).
  • Topology Manager — балансирует pods по NUMA nodes.
  • VPA (Vertical Pod Autoscaler) с NUMA awareness.
# Запуск Docker с NUMA pinning:
docker run --cpuset-cpus="0-7,16-23" --memory=64g ...
# Это привязывает к cores node 0 (если topology такой)

5. Disable NUMA для специфичных workloads

Иногда проще выключить NUMA detection вовсе. Это объединит весь RAM в один node (логически), но физически remote access останется medlennym.

# Kernel boot parameter:
# numa=off  -- в /etc/default/grub GRUB_CMDLINE_LINUX

Не рекомендуется — лучше использовать numactl правильно.


Реальный пример: Spark на multi-socket

У вас 2-socket сервер, 128 cores total, 512 GB RAM (256 на каждом node). Запускаете Spark с 8 executors по 16 cores, 32 GB heap каждый.

Без NUMA awareness:

  • Executor 1 запустился на node 0, аллоцировал 32 GB heap на node 0.
  • Linux scheduler переносит threads executor 1 на cores node 1 (для load balancing).
  • Каждый JVM access к heap — remote, 200 ns vs 100 ns.
  • Производительность executor падает на 30-50%.

С NUMA pinning:

# В Spark config:
spark.executor.extraJavaOptions=-XX:+UseNUMA
# Или явно через numactl:
spark.executorEnv.NUMA_NODE=0  # для половины executors
spark.executorEnv.NUMA_NODE=1  # для остальной половины
# Wrapper script стартует executor через numactl --cpunodebind=$NUMA_NODE --membind=$NUMA_NODE

Результат: каждый executor работает только на своём node, аллокации local, performance стабильная.


Попробуй сам

# 1. Своя NUMA topology:
numactl --hardware
# Если показывает 1 node -- у вас single-socket или NUMA отключен

lscpu | grep -i numa
# NUMA node(s): 2
# NUMA node0 CPU(s): 0-15,32-47
# NUMA node1 CPU(s): 16-31,48-63

# 2. Память по nodes:
numastat -m | head -20

# 3. Запустите процесс с привязкой и сравните:
# Замерим memory bandwidth с привязкой:
time numactl --cpunodebind=0 --membind=0 \
    dd if=/dev/zero of=/dev/null bs=1M count=10000

# То же самое, но remote:
# Доступ из node 0 cores, память на node 1
time numactl --cpunodebind=0 --membind=1 \
    dd if=/dev/zero of=/dev/null bs=1M count=10000
# Разница может быть в 1.5-2x

# 4. NUMA balancing статус:
sysctl kernel.numa_balancing
# Если 1 -- активный, страницы мигрируют автоматически

# 5. Memory distribution процесса:
PID=$$
numastat -p $PID
# Покажет, сколько памяти на каждом node для вашего shell

# 6. Принудительно сбалансировать память на одном node:
# (для long-running процесса)
migratepages PID 0 1  # перенести страницы с node 0 на node 1

# 7. Watch NUMA-related counters:
watch -n 1 'numastat -m | head -10'

Проверка знанийKnowledge check
Senior спрашивает: 'У нас 2-socket сервер, 128 cores. Postgres на нём работает медленнее, чем на single-socket с 64 cores с теми же настройками. shared_buffers 64 GB, такая же нагрузка. Почему так может быть и что проверить?'
ОтветAnswer
Это классический NUMA effects. Давай разберём. Гипотеза: shared_buffers 64 GB были аллоцированы в одном NUMA node (node 0). Postgres backends могут оказаться на cores другого node (node 1) -- каждый доступ к shared_buffers становится remote access. Это в 1.5-2x медленнее, плюс interconnect bottleneck при многих parallel queries. Что проверить: 1. NUMA topology: numactl --hardware # node 0 cpus: 0-31 # node 0 size: 128 GB # node 1 cpus: 32-63 # node 1 size: 128 GB # distances: 10/21 (1:2 ratio remote vs local) 2. Memory distribution Postgres: PG_PID=$(pgrep -f 'postgres.*main') numastat -p $PG_PID # Если видим: node 0 -- 60 GB, node 1 -- 4 GB -- shared_buffers в node 0 3. Где запускаются backends: ps -eL -o pid,psr,comm | grep postgres # psr -- CPU number. Если backends на cores 32-63 (node 1), но память на node 0 -- проблема 4. Remote access metrics: perf stat -e node-loads,node-load-misses -p $PG_PID sleep 10 # Высокий node-load-misses (remote) -- плохо Решения: 1. NUMA pinning Postgres. Запустить с привязкой к одному node: numactl --cpunodebind=0 --membind=0 postgres -D ... shared_buffers 64 GB будет в node 0, все backends на node 0 cores. Никаких remote access. Минус: используем только 50% CPU. Но performance per query может быть в 2x лучше. 2. Меньше shared_buffers, interleave. Если хочется все 128 cores: numactl --interleave=all postgres -D ... # Round-robin allocation между nodes. Bandwidth max, но latency средняя 3. Two Postgres instances. Радикальное решение: запустить два независимых instances: - PG1 на node 0, slot for tables 1-N - PG2 на node 1, slot for tables N+1-2N - Application logic decides which to query - Каждый instance имеет 64 cores + 128 GB local memory Сложнее в управлении, но maxes scalability. 4. NUMA balancing off для consistency. Если latency spikes из-за migration: sudo sysctl kernel.numa_balancing=0 # Отключает auto migration. Pages stay где аллоцированы. Может ухудшить, может улучшить -- зависит от workload 5. Kernel huge pages. Если используете transparent huge pages (THP) -- на NUMA они могут вызывать дополнительные миграции: echo never > /sys/kernel/mm/transparent_hugepage/enabled # Postgres рекомендует disable THP в любом случае 6. Connection routing. Если application server на отдельной машине -- routing connections к правильным CPU не контролируете. Внутри Postgres можно настроить pg_hba.conf + pg_bouncer для маршрутизации. 7. Disable NUMA вовсе. Last resort: # /etc/default/grub: GRUB_CMDLINE_LINUX="numa=off" # update-grub, reboot # Linux перестанет различать nodes, но физически remote access останется медленнее Best Practice для production Postgres на multi-socket: - numactl --cpunodebind=0 --membind=0 для main Postgres - shared_buffers = 25-40% of one node's memory (not total) - max_connections соответствующее cores в одном node - pgbouncer для pooling - Monitoring: numastat, perf node-loads Bottom line: на multi-socket системах NUMA awareness обязательна для memory-heavy DB. Иначе single-socket будет быстрее. У большинства managed databases (RDS, Aurora) NUMA уже учтена. На self-hosted -- надо настроить руками.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Что такое NUMA и когда оно становится критично?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5