Ви купили блискучий двосокетний сервер, бо «вдвічі більше CPU» звучить як «вдвічі більше пропускної здатності». Потім у вашій базі даних піднявся p95, стек зберігання почав підгальмовувати, а команда продуктивності прийшла з графіками та тихим розчаруванням.
Це і є NUMA: Non-Uniform Memory Access. Це не баг. Це рахунок, який вам виписують за те, що ви приймаєте сервер за одну велику щасливу купу CPU і RAM.
Що таке NUMA насправді (і чого воно не є)
NUMA означає, що машина фізично складається з кількох «доменів локальності» (NUMA‑вузлів). Кожен вузол — це група ядер CPU і пам’яті, що фізично близькі одне до одного. Доступ до локальної пам’яті швидший(увато). Доступ до пам’яті, приєднаної до іншого сокета, повільніший(увато). Це «увато» важливе, бо справа не лише в затримці; це ще й конкуренція на міжсокетній шині.
На типовому сучасному двосокетному x86‑сервері:
- Кожен сокет має свої контролери пам’яті та канали. Планки DRAM підключені до того сокета.
- Сокети з’єднані між собою інтерконнектом (Intel UPI, AMD Infinity Fabric або подібним).
- PCIe‑пристрої також фізично приєднані до сокета через root complexes. NIC або NVMe пристрій «ближчий» до одного сокета, ніж до іншого.
Отже, NUMA — це не «параметр налаштування». Це топологія. Ігнорування її означає, що ви дозволяєте ОС керувати трафіком, поки ви самі активно створюєте затори.
Також: NUMA не завжди автоматично катастрофа. NUMA підходить, якщо навантаження дружнє до NUMA, планувальнику дали шанс, і ви не саботуєте його поганим pinning’ом або розміщенням пам’яті.
Цитата, яку варто пам’ятати: «Сподівання — це не стратегія.» — Гнр. Гордон Р. Салліван. У країні NUMA «сподіватися, що ядро розбереться» — це просто сподівання.
Чому двосокетний не означає «вдвічі швидший»
Бо вузьке місце змінилося. Додавання другого сокета підвищує пікові обчислювальні потужності та загальну ємність пам’яті, але також збільшує кількість способів стати повільним. Ви не подвоюєте один ресурс; ви зшиваєте два комп’ютери і просите Linux вдавать, що це один.
1) Локальність пам’яті стає виміром продуктивності
У односокетній машині «локальна» пам’ять для кожного ядра по суті однакова. У двосокетній коробці пам’ять має адресу, і ця адреса має дім. Коли потік на сокеті 0 часто читає й пише сторінки, виділені з вузла 1, кожен промах кеша стає поїздкою через сокети. Крос‑сокетні поїздки не безкоштовні; це платна дорога у години пік.
2) Трафік когерентності кеша зростає
Багатосокетність вимагає когерентності між сокетами. Якщо у вас є спільні структури з багатьма записами (локи, черги, «гарячі» лічильники, метадані алокатора), ви отримаєте міжсокетний ping‑pong. Іноді CPU «зайнятий», але корисна робота на цикл падає.
3) Локальність PCIe важить більше, ніж здається
Ваш NIC підключений до конкретного сокета. Ваш NVMe HBA підключений до конкретного сокета. Якщо навантаження призначається переважно на інший сокет, ви вигадали додаткову внутрішню пересідання для кожного пакета або завершення IO. На системах з високим IOPS або високою частотою пакетів ця пересідання стає податком.
4) Ви легко можете зробити ситуацію гіршою «оптимізаціями»
Pinning потоків звучить дисципліновано, поки ви не зафіксуєте CPU, але не пам’ять, або зафіксуєте переривання на неправильний сокет, або змусите все працювати на вузлі 0, бо «так працювало у стенді». Неправильна конфігурація NUMA — це рідкісна проблема, де робити щось іноді гірше, ніж нічого не робити.
Жарт №1 (коротко, по темі): Двосокетні сервери як open‑office: ви отримали «ємність», але тепер все важливе пов’язане з переходами через кімнату.
Факти та історія, які знадобляться на роботі
Це не питання для вікторини. Це аргументи, які допоможуть зупинити погане рішення про покупку або перемогти в суперечці з тим, хто хапається за таблицю в Excel.
- NUMA старше за ваш хмарний стек. Комерційні NUMA‑дизайни існували десятиліттями; ідея старіша за більшість сучасних інструментів продуктивності.
- SMP перестало масштабуватися «безкоштовно». Рівномірний доступ до пам’яті був простішим, але стикнувся з фізичними та електричними обмеженнями при рості числа ядер, а пропускна спроможність пам’яті не росла лінійно.
- Інтегровані контролери пам’яті змінили гру. Перенесення контролерів пам’яті в CPU покращило затримки, але також зробило питання «який CPU володіє пам’яттю» неминучим.
- Міжсокетні лінії розвинулися, але вони все ще повільніші за локальний DRAM. UPI/Infinity Fabric швидкі, але не замінюють локальні канали. Вони також несуть трафік когерентності.
- NUMA — це не лише пам’ять. Linux використовує ту ж топологію для розуміння PCIe‑пристроїв, переривань і доменів планування. Локальність — властивість всієї машини.
- Віртуалізація не знищила NUMA; вона спростила його приховування. Гіпервізори можуть показувати віртуальний NUMA, але ви також можете випадково створити VM, що охоплює сокети, а потім дивуватися, чому вона смикається.
- Transparent Huge Pages взаємодіють з NUMA. THP може зменшити накладні витрати TLB, але також ускладнити розміщення сторінок і їхню міграцію під навантаженням.
- Рішення під час раннього завантаження важливі. Багато алокаторів і сервісів виділяють багато пам’яті при старті; «де вона опиниться» може визначати продуктивність протягом годин.
- Стек зберігання чутливий до NUMA при великій пропускній здатності. NVMe черги, обробка softirq і polling у userspace люблять локальність; перехід між сокетами додає jitter і знижує верхню межу.
Як це провалюється в продакшні: режими відмов
Проблеми NUMA рідко виглядають як «проблема NUMA». Вони виглядають як:
- Зростання p95 і p99 затримки, тоді як середня пропускна здатність виглядає «нормально».
- CPU високе, але IPC низький і сервер ніби рухається в болоті.
- Один сокет завантажений, інший байдуже, бо планувальник зробив те, що ви попросили, а не те, що мали на увазі.
- Зростає відсоток віддалених звернень до пам’яті і тепер ви платите за міжсокетні промахи кеша.
- Дисбаланс IRQ змушує мережеву або NVMe обробку накопичуватися на «неправильних» ядрах.
- Нестабільна продуктивність під навантаженням, бо міграція сторінок і reclaim починають працювати, коли пам’ять розбалансована по вузлам.
NUMA‑проблеми часто мають мультиплікативний ефект. Віддалений доступ до пам’яті додає затримку; це збільшує час утримання локів; це підвищує конкуренцію; це збільшує контекстні переключення; це збільшує промахи кеша; це додає віддалені доступи. Ця спіраль пояснює, чому «вчора було нормально» — популярний початок розслідувань.
Швидкий план діагностики (перший/другий/третій)
Коли система повільна і ви підозрюєте NUMA, не починайте з героїчних бенчмарків. Почніть з топології та розміщення. Ви намагаєтеся відповісти на одне питання: чи CPU, пам’ять і шляхи IO знаходяться на тому ж вузлі для «гарячої» роботи?
Перший: підтвердіть топологію і чи ви охоплюєте сокети
- Скільки існує NUMA‑вузлів?
- Які CPU належать кожному вузлу?
- Чи ваше навантаження зафіксовано або обмежено cgroups/cpuset?
Другий: перевірте локальність пам’яті та віддалені звернення
- Чи більшість пам’яті виділено на вузлі 0, а потоки виконуються на вузлі 1 (або навпаки)?
- Чи зростають лічильники NUMA «miss» та «foreign»?
- Чи ядро часто мігрує сторінки?
Третій: перевірте локальність PCIe і переривань
- До якого NUMA‑вузла приєднаний NIC/NVMe пристрій?
- Чи його переривання потрапляють на CPU того ж вузла?
- Чи черги розподілені розумно по ядрах поруч із пристроєм?
Якщо ви зробите ці три кроки, ви знайдете причину великої частини «таємничих» регресій на двосокетах менш ніж за 20 хвилин. Не всіх. Але досить, аби врятувати ваш вихідний уїк‑енд.
Практичні завдання: NUMA з командами
Це завдання, які я насправді виконую, коли хтось каже «нові двосокетні сервери повільніші». Кожне завдання включає команду, прикладний вивід, що це означає, і рішення, яке з цього випливає.
Завдання 1: Подивитися NUMA‑вузли, відображення CPU і розмір пам’яті
cr0x@server:~$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
node 0 size: 257728 MB
node 0 free: 18240 MB
node 1 cpus: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
node 1 size: 257728 MB
node 1 free: 214912 MB
node distances:
node 0 1
0: 10 21
1: 21 10
Що це означає: Два вузли. Матриця відстаней показує, що віддалений доступ дорожчий за локальний. Зверніть увагу на дисбаланс вільної пам’яті: вузол 0 щільно зайнятий; вузол 1 майже порожній.
Рішення: Якщо ваше навантаження в основному на CPU 0–15, а вузол 0 майже заповнений, ви ризикуєте віддаленими алокаціями або reclaim’ом. Плануйте перерозподіл розміщення пам’яті або CPU.
Завдання 2: Перевірити, до якого вузла прив’язаний PCIe‑пристрій
cr0x@server:~$ cat /sys/class/net/ens5f0/device/numa_node
1
Що це означає: NIC приєднаний до NUMA‑вузла 1.
Рішення: Для високої частоти пакетів віддавайте перевагу запуску мережево‑важких потоків на CPU вузла 1 і направляйте IRQ туди.
Завдання 3: Відобразити NVMe‑пристрої на NUMA‑вузли
cr0x@server:~$ for d in /sys/class/nvme/nvme*; do echo -n "$(basename $d) "; cat $d/device/numa_node; done
nvme0 0
nvme1 0
Що це означає: Обидва контролери NVMe локальні для вузла 0.
Рішення: Тримайте найгарячіші потоки подачі/обробки IO на вузлі 0, якщо ви оптимізуєте під низьку затримку, або принаймні вирівняйте ядра обробки IO з вузлом 0.
Завдання 4: Подивитися розподіл пам’яті по вузлах і лічильники NUMA hit/miss
cr0x@server:~$ numastat
node0 node1
numa_hit 1245067890 703112340
numa_miss 85467123 92133002
numa_foreign 92133002 85467123
interleave_hit 10234 11022
local_node 1231123456 688000112
other_node 100000000 110000000
Що це означає: Немаленькі numa_miss і other_node вказують на віддалене використання пам’яті. Трохи віддаленого доступу нормально; багато — ознака неправильного розміщення.
Рішення: Якщо віддалений трафік росте у вікні повільності, сфокусуйтеся на прив’язці CPU/пам’яті або на тиску на reclaim/міграцію, перш ніж чіпати код програми.
Завдання 5: Переглянути NUMA‑мапу пам’яті процесу (RSS по вузлах)
cr0x@server:~$ pidof postgres
2481
cr0x@server:~$ numastat -p 2481
Per-node process memory usage (in MBs) for PID 2481 (postgres)
Node 0 Node 1 Total
--------------- --------------- ---------------
Private 62048.1 1892.0 63940.1
Heap 41000.0 256.0 41256.0
Stack 32.0 16.0 48.0
Huge 0.0 0.0 0.0
---------------- --------------- --------------- ---------------
Total 62080.1 1924.0 64004.1
Що це означає: Пам’ять цього процесу переважно знаходиться на вузлі 0.
Рішення: Переконайтеся, що найактивніші робітники Postgres виконуються переважно на CPU вузла 0, або явно прив’язуйте алокацію пам’яті до вузла, на якому збираєтеся працювати.
Завдання 6: Перевірити афінність CPU процесу (хтось зафіксував його?)
cr0x@server:~$ taskset -cp 2481
pid 2481's current affinity list: 16-31
Що це означає: Процес зафіксовано на CPU вузла 1, але в Завданні 5 його пам’ять на вузлі 0. Це класичний біль від віддалених звернень.
Рішення: Або перемістіть процес на CPU 0–15, або відновіть локальність пам’яті (рестарт з правильною прив’язкою або обережна міграція, якщо підтримується).
Завдання 7: Запустити навантаження з явною прив’язкою CPU + пам’яті
cr0x@server:~$ numactl --cpunodebind=1 --membind=1 -- bash -c 'echo "bound"; sleep 1'
bound
Що це означає: Ця оболонка (і все, що вона запустить) виконуватиметься на вузлі 1 і виділятиме пам’ять із вузла 1.
Рішення: Використовуйте це для цілеспрямованих тестів. Якщо продуктивність покращується, ви підтвердили, що локальність — вузьке місце, і можете впровадити довготривалу стратегію розміщення.
Завдання 8: Перевірити статус автоматичного NUMA‑балансування
cr0x@server:~$ sysctl kernel.numa_balancing
kernel.numa_balancing = 1
Що це означає: Ядро може мігрувати сторінки між вузлами, щоб покращити локальність.
Рішення: Для деяких чутливих до затримки навантажень автоматичне балансування може додавати jitter. Якщо ви вже керуєте афінністю явним чином, розгляньте вимкнення після тестування.
Завдання 9: Спостерігати активність NUMA‑балансування у vmstat
cr0x@server:~$ vmstat -w 1 5
procs -------------------memory------------------ ---swap-- -----io---- -system-- --------cpu--------
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 18234000 10240 120000 0 0 12 45 610 1200 18 6 74 2 0
4 0 0 18190000 10240 120500 0 0 10 38 640 1600 22 7 68 3 0
6 0 0 18020000 10240 121100 0 0 11 40 780 2400 28 9 58 5 0
7 0 0 17800000 10240 121900 0 0 12 42 900 3000 31 10 52 7 0
8 0 0 17550000 10240 122800 0 0 15 50 1100 4200 35 11 46 8 0
Що це означає: Зростання контекстних переключень (cs) і зменшення простою (id) можуть супроводжувати тиск на міграцію/reclaim. Це грубий сигнал, а не доказ NUMA.
Рішення: Якщо це корелює зі зростанням віддалених статистик пам’яті, розглядайте як «розміщення пам’яті плюс тиск» і дивіться на вільну пам’ять по вузлах і сканування сторінок.
Завдання 10: Перевірити вільну пам’ять по вузлах напряму
cr0x@server:~$ grep -E 'Node [01] (MemTotal|MemFree|FilePages|Active|Inactive)' /sys/devices/system/node/node*/meminfo
Node 0 MemTotal: 263913472 kB
Node 0 MemFree: 1928396 kB
Node 0 FilePages: 2210040 kB
Node 0 Active: 92233728 kB
Node 0 Inactive: 70542336 kB
Node 1 MemTotal: 263913472 kB
Node 1 MemFree: 198223224 kB
Node 1 FilePages: 5110040 kB
Node 1 Active: 12133728 kB
Node 1 Inactive: 8542336 kB
Що це означає: Вузол 0 майже вичерпав вільну пам’ять, тоді як вузол 1 має великий запас. Ядро почне виділяти віддалено або агресивно звільняти на вузлі 0.
Рішення: Або перемістіть робочі потоки на вузол 1 і перезапустіть, щоб пам’ять виділялась там, або свідомо розподіліть алокації по вузлах (interleave), якщо навантаження це витримає.
Завдання 11: Перевірити розподіл IRQ і чи збігаються переривання з локальністю пристрою
cr0x@server:~$ grep -E 'ens5f0|nvme' /proc/interrupts | head
132: 1203345 0 0 0 IR-PCI-MSI 524288-edge ens5f0-TxRx-0
133: 0 1189922 0 0 IR-PCI-MSI 524289-edge ens5f0-TxRx-1
134: 0 0 1191120 0 IR-PCI-MSI 524290-edge ens5f0-TxRx-2
135: 0 0 0 1210034 IR-PCI-MSI 524291-edge ens5f0-TxRx-3
Що це означає: Черги розподілені по CPU (стовпці). Добрий знак. Але треба перевірити, чи ці CPU належать тому ж NUMA‑вузлу, що і NIC.
Рішення: Якщо NIC на вузлі 1, а більшість переривань падає на CPU вузла 0, налаштуйте IRQ афінність (або довіртеся irqbalance, якщо він робить це правильно).
Завдання 12: Визначити, які CPU на якому вузлі (для рішень щодо IRQ)
cr0x@server:~$ lscpu | egrep 'NUMA node\(s\)|NUMA node0 CPU\(s\)|NUMA node1 CPU\(s\)'
NUMA node(s): 2
NUMA node0 CPU(s): 0-15
NUMA node1 CPU(s): 16-31
Що це означає: Чітке відображення CPU по вузлах.
Рішення: При прив’язці потоків або IRQ тримайте «гарячий шлях» на одному вузлі, якщо немає вагомої причини робити інакше.
Завдання 13: Перевірити обмеження cpuset у cgroup (улюблена пастка для контейнерів)
cr0x@server:~$ systemctl is-active kubelet
active
cr0x@server:~$ cat /sys/fs/cgroup/cpuset/kubepods.slice/cpuset.cpus
0-31
cr0x@server:~$ cat /sys/fs/cgroup/cpuset/kubepods.slice/cpuset.mems
0-1
Що це означає: Поди можуть працювати на всіх CPU і виділяти пам’ять з обох вузлів. Це гнучко, але може перетворитися на «випадкове розміщення».
Рішення: Для критичних до затримки подів використовуйте політику, що знає про топологію (Guaranteed QoS, CPU Manager static і NUMA‑aware scheduling), щоб CPU і пам’ять залишалися узгодженими.
Завдання 14: Перевірити, яких NUMA‑вузлів дозволено використовувати процесу
cr0x@server:~$ cat /proc/2481/status | egrep 'Cpus_allowed_list|Mems_allowed_list'
Cpus_allowed_list: 16-31
Mems_allowed_list: 0-1
Що це означає: Процес може виділяти пам’ять на обох вузлах, але може виконуватися лише на CPU вузла 1. Така комбінація часто породжує віддалені алокації при старті і «санує себе» пізніше непередбачуваними шляхами.
Рішення: Для передбачуваної поведінки вирівнюйте Cpus_allowed_list і Mems_allowed_list, якщо лише ви не виміряли вигоду від interleave.
Завдання 15: Використати лічильники продуктивності для швидкої перевірки (LLC misses та stalled cycles)
cr0x@server:~$ sudo perf stat -p 2481 -a -e cycles,instructions,cache-misses,stalled-cycles-frontend -I 1000 -- sleep 3
# time counts unit events
1.000993650 2,104,332,112 cycles
1.000993650 1,003,122,400 instructions
1.000993650 42,110,023 cache-misses
1.000993650 610,334,992 stalled-cycles-frontend
2.001976121 2,201,109,003 cycles
2.001976121 1,021,554,221 instructions
2.001976121 47,901,114 cache-misses
2.001976121 702,110,443 stalled-cycles-frontend
3.003112980 2,305,900,551 cycles
3.003112980 1,030,004,992 instructions
3.003112980 55,002,203 cache-misses
3.003112980 801,030,110 stalled-cycles-frontend
Що це означає: Зростання промахів кеша і frontend stalls вказує на проблеми підсистеми пам’яті. Один лише цей сигнал не кричить «NUMA», але підсилює гіпотезу про локальність у поєднанні з numastat.
Рішення: Якщо промахи кеша корелюють з вищими віддаленими статистиками пам’яті, віддайте пріоритет виправленню розміщення і зменшенню міжсокетного «чату» перед мікрооптимізаціями коду.
Завдання 16: Швидкий A/B тест: запуск на одному вузлі проти spanning
cr0x@server:~$ numactl --cpunodebind=0 --membind=0 -- bash -c 'stress-ng --cpu 8 --vm 2 --vm-bytes 8G --timeout 20s --metrics-brief'
stress-ng: info: [3112] setting timeout to 20s
stress-ng: metrc: [3112] stressor bogo ops real time usr time sys time bogo ops/s
stress-ng: metrc: [3112] cpu 9821 20.00 18.90 1.02 491.0
stress-ng: metrc: [3112] vm 1220 20.00 8.12 2.10 61.0
Що це означає: Ви отримали базову лінію при обмеженні одним вузлом. Повторіть на вузлі 1 і порівняйте. Потім запустіть без прив’язки і порівняйте знову.
Рішення: Якщо результати одного вузла стабільніші або швидші, ніж «вільна розкладка», ваша політика планування/розміщення не тримає локальність. Виправте політику; не купуйте ще CPU.
Жарт №2 (коротко, по темі): Налаштування NUMA — це мистецтво наближення роботи до її пам’яті — бо телепортації в ядро поки що немає.
Три корпоративні міні-історії (анонімізовані, правдоподібні, технічно точні)
Міні‑історія 1: Інцидент через неправильне припущення
Команда платежів перенесла сервіс з чутливою до затримки логікою з старих односокетних машин на нові двосокетні сервери. Нові коробки мали більше ядер, більше RAM і вищу ціну, тож всі очікували легкого покращення. Навантажувальний тест показав нормальну середню пропускну здатність, тож зміни запустили в продакшн.
За кілька годин p99 почав повільно зростати. Не піки — саме дрейф. On‑call побачив CPU 60–70%, мережа ок, диски ок, і припустив, що це «галасливий сусід» у верхньому ланцюжку залежностей. Вони відкотилися. Затримка повернулася до норми. Новий хардвер помітили як «недружній».
Через кілька тижнів міграція повернулася. Цього разу хтось запустив numastat -p і taskset. Сервіс був зафіксований (скрипт деплою) на «останні 16 CPU», бо так поділяли навантаження на старих машинах. На нових машинах «останні 16 CPU» належали до NUMA‑вузла 1. Пам’ять сервісу — виділена на старті — опинилася переважно на вузлі 0 через те, де працював init і як написаний systemd unit.
Отже, найгарячіші потоки працювали на вузлі 1, читали й писали пам’ять на вузлі 0 і при цьому обробляли мережеві переривання від NIC, приєднаного до вузла 1. Навантаження виконувало крос‑сокетні читання для стану програми, а потім крос‑сокетні записи для метаданих алокатора. Це було латентне «коктейльне» отруєння.
Виправлення було нудним: вирівняти афінність CPU і політику пам’яті на старті сервісу, перевірити локальність IRQ NIC і перестати використовувати номери CPU як стабільну семантику. Справжній урок постмортему: двосокетний — це не «більше односокетного». Це топологія, яку треба поважати.
Міні‑історія 2: Оптимізація, що відбилася бумерангом
Команда зберігання, що працювала з NVMe‑інтенсивним сервісом, хотіла зменшити хвіст затримок. Хтось запропонував зафіксувати потоки подачі IO на невеликому наборі ізольованих ядер. Зробили. Затримка покращилася у легких тестах, тож зміни впровадили масштабно.
Під реальним трафіком система почала періодично затримуватися. Не повністю падала — але досить, щоб викликати повтори і помітні користувачам сповільнення. Графіки CPU виглядали «добре»: ті зафіксовані ядра були зайняті, інші — майже простоювали. Це був перший натяк: не хочеться мати половину серверу простоюючою, поки клієнти чекають.
Розслідування показало: NVMe пристрої були приєднані до вузла 0, а зафіксовані IO‑потоки — на вузлі 1. До того ж MSI‑X переривання балансувалися по обох сокетах. Кожне завершення вимагало крос‑сокетних переходів: переривання на вузлі 0, розбудження потоку на вузлі 1, доступ до черг, виділених на вузлі 0, дотик до спільних лічильників, повтор. Коли навантаження зросло, трафік когерентності і віддалені звернення підсилювали одне одного. Зафіксована конфігурація не дозволяла планувальнику «випадково виправити» це, перемістивши потоки ближче до пристрою.
Відкат не був «припиніть pinning». Це був «pin правильно». Вирівняли IO‑потоки і афінність IRQ до вузла 0, потім розподілили черги по ядрах того ж вузла. Хвости затримок стабілізувалися, і пропускна здатність зросла, бо міжсокетний інтерконнект перестав виконувати неоплачувану роботу.
Практичний висновок: pinning — це не оптимізація; pinning — це зобов’язання. Якщо ви не забезпечите локальність по всьому шляху — CPU, пам’ять і IO — pinning зробить лише консистентною погану архітектуру.
Міні‑історія 3: Нудна, але правильна практика, яка врятувала ситуацію
Група платформи даних працювала зі змішаними навантаженнями: база даних, сервіс на кшталт Kafka для логів і набір батч‑джобів. Вони стандартизували двосокетні сервери для ємності, але розглядали NUMA як частину специфікації розгортання, а не як цікавість після інциденту.
Кожен сервіс мав «контракт розміщення» у своєму runbook: який NUMA‑вузол віддавати перевагу, як перевіряти локальність пристроїв і що робити, якщо вузол не має достатньо вільної пам’яті. Вони користувалися невеликим набором команд — numactl --hardware, numastat, taskset, /proc/interrupts — і вимагали доказів у рев’ю змін для будь‑якої зміни pinning’у.
Одного дня, після звичайного оновлення ядра, вони помітили невелике, але постійне регресування p95 на шляху інгесту логів. Нічого критичного. І тут нудна практика дала результат: хтось запустив перевірки розміщення. Оновлення прошивки NIC під час технічного обслуговування змінило слот PCIe, і NIC опинився на вузлі 1, поки потоки інгесту були прив’язані до вузла 0. IRQ пішли за NIC; потоки — ні.
Вони підкоригували афінність відповідно до нової топології і відновили продуктивність без війни. Ніяких героїчних дій. Жодних звинувачень. Просто операції, що враховують топологію. Тикет інциденту закрили з коментарем, який зазвичай ігнорують, доки не знадобиться: «Зміни в апаратному забезпеченні — це зміни в програмному».
Поширені помилки: симптом → корінь → виправлення
Цей розділ навмисно різкий. Це патерни, які повторюються в продакшні.
1) Симптом: один сокет завантажений, інший майже простій
Корінь: Афінність CPU/cpuset обмежує навантаження одним вузлом, або однопотокова «бутилка» змушує серіалізацію. Іноді причина — обробка IRQ, зосереджена на кількох ядрах.
Виправлення: Якщо навантаження масштабується, спочатку розподіліть його по ядрах в межах вузла. Лише потім розширюйтеся на інші сокети, і тоді керуйте політикою пам’яті та локальністю IRQ. Валідируйте taskset -cp, lscpu і /proc/interrupts.
2) Симптом: p99 гірший на двосокетному, ніж на односокетному
Корінь: Крос‑сокетні звернення до пам’яті та churn когерентності кеша (локи, спільні алокатори, «гарячі» лічильники). Часто викликано тим, що потоки працюють на вузлі A, а пам’ять на вузлі B.
Виправлення: Вирівняйте потоки і пам’ять через numactl --cpunodebind + --membind (або через менеджер сервісів/cgroup політику). Зменшіть шаринг між сокетами шляхом шардінгу черг і пер‑потокових лічильників. Перевірте numastat -p і лічильники віддаленого доступу.
3) Симптом: пропускна здатність в порядку, але jitter жахливий
Корінь: Автоматичне NUMA‑балансування і міграція сторінок під навантаженням, або тиск пам’яті по вузлу, що викликає сплески reclaim’у. THP може підсилювати вартість міграції.
Виправлення: Забезпечте достатній запас вільної пам’яті на вузлі, де працює навантаження. Розгляньте вимкнення автоматичного NUMA‑балансування для зафіксованих робочих навантажень після тестування. Слідкуйте за meminfo вузлів і numastat.
4) Симптом: мережевий канал швидко досягає межі або падає під навантаженням
Корінь: Переривання NIC і обробка мережевого стека на неправильному сокеті; прикладні потоки на іншому сокеті; Steer пакетів бореться з прив’язкою CPU.
Виправлення: Підтвердіть NUMA‑вузол NIC через sysfs. Вирівняйте афінність IRQ і ядра додатку. Використовуйте multi‑queue розумно. Перевірте /proc/interrupts і відображення CPU по вузлам.
5) Симптом: NVMe‑затримки стрибком під час інтенсивного IO, навіть якщо CPU в іншому місці простий
Корінь: Потоки відправки/завершення IO далеко від контролера NVMe; пам’ять черги на віддаленому вузлі; переривання лягають по обох сокетах.
Виправлення: Тримайте IO‑шлях на вузлі пристрою. Перевірте контролер numa_node. Налаштуйте розміщення потоків і афінність IRQ. Не «оптимізуйте» фіксацією без перевірки локальності.
6) Симптом: додавання робочих потоків робить повільніше
Корінь: Ви перетнули NUMA‑межу і перетворили локальний контеншн на віддалений; локи і спільні кеші стали дорожчими.
Виправлення: Масштабуйте в межах сокета спочатку. Якщо потрібно більше ресурсів — шардьте навантаження за NUMA‑вузлами (два незалежні пули) замість одного глобального. Ставтесь до міжсокетного шарингу як до дорогого ресурсу.
7) Симптом: контейнери ведуть себе по‑різному на «ідентичних» вузлах
Корінь: Різна топологія PCIe слотів, налаштування BIOS або політики kubelet CPU/memory manager. «Однакова модель» не означає «однакова топологія».
Виправлення: Стандартизуйте налаштування BIOS, перевіряйте lscpu і NUMA‑вузли пристроїв під час провізіонінгу, і використовуйте топологі‑орієнтоване планування для критичних подів.
Чеклісти / покроковий план
Це кроки, що зупиняють двосокетні машини від тихого крадання бюджету затримки.
Чекліст A: Перед деплоєм чутливого до затримки навантаження на двосокет
- Змепіть топологію: зафіксуйте вивід
numactl --hardwareіlscpu. Хочете це в тикеті, а не в чиємусь мозку. - Змепіть локальність пристроїв: для NIC і NVMe зафіксуйте
/sys/class/net/*/device/numa_nodeі/sys/class/nvme/nvme*/device/numa_node. - Визначте модель розміщення: один вузол (переважно), шардінг по вузлах або крос‑вузлове (тільки якщо потрібно).
- Виберіть стратегію афінності: або «не pin’имо, даємо працювати планувальнику», або «pin + bind пам’яті + вирівняти IRQ». Ніколи — «лише pin».
- Перевірка ємності по вузлу: переконайтеся, що обраний вузол має запас пам’яті для page cache + heap + пікових навантажень.
Чекліст B: Коли у вас вже повільний сервер і потрібне швидке виправлення
- Запустіть
numactl --hardware; дивіться на дисбаланс вільної пам’яті по вузлах. - Запустіть
numastat; дивіться на високі і зростаючіnuma_miss/other_node. - Виберіть найгарячіший PID; запустіть
numastat -pіtaskset -cp. Перевірте, чи CPU вузол і вузол пам’яті збігаються. - Перевірте NUMA‑вузол пристрою і
/proc/interrupts. Підтвердіть локальність IRQ для NIC/NVMe. - Якщо невідповідність очевидна: виправте розміщення (рестарт з правильною прив’язкою), замість ганяння за мікрооптимізаціями.
Чекліст C: Розумна довгострокова модель експлуатації
- Зробіть топологію частиною інвентарю: зберігайте NUMA‑мапи та інформацію про підключення PCIe для кожного хоста.
- Стандартизуйте налаштування BIOS: уникайте несподіваних режимів, що змінюють видимість NUMA (та документуйте вибір).
- Побудуйте «NUMA smoke tests»: запускайте прості A/B тести локальності під час провізіонінгу, щоб виявити дивності на ранньому етапі.
- Навчайте команди: «двосокетний ≠ вдвічі швидший» має бути загальновідомим, а не легендою.
- Рев’юйте зміни pinning як код: вимагайте доказів, план відкату і перевірки після зміни.
FAQ
Q1: Чи завжди уникати двосокетних серверів?
Ні. Двосокетні добре підходять для ємності пам’яті, PCIe‑лінків і агрегованої пропускної здатності. Уникайте їх, коли ваше навантаження вкрай чутливе до затримки і сильно шарить спільні структури, і коли ви можете вміститися в потужний односокетний SKU.
Q2: Чи NUMA — це лише проблема баз даних?
Ні. Воно сильно вдаряє по базах даних через великі підписи пам’яті і спільні структури, але також впливає на мережу, NVMe, JVM‑сервіси, аналітику і все, що дає багато промахів кеша.
Q3: Якщо в Linux є автоматичне NUMA‑балансування, навіщо ним перейматися?
Бо балансування реактивне і не безкоштовне. Воно може покращити пропускну здатність для деяких загальних навантажень, але додавати jitter для чутливих до затримки сервісів — особливо якщо ви ще й фіксуєте CPU.
Q4: Що краще: інтерлевінг пам’яті між вузлами чи прив’язка до одного вузла?
Прив’язка зазвичай краща для затримки і передбачуваності, коли навантаження можна утримати в одному вузлі. Інтерлевінг допомагає, коли вам дійсно потрібна пропускна здатність обох контролерів пам’яті і навантаження добре паралелізується.
Q5: Як дізнатися, чи моє навантаження переходить сокети?
Використайте taskset -cp (де воно виконується) і numastat -p (де живе його пам’ять). Якщо CPU‑вузол і вузол пам’яті не збігаються — ви переходите сокети в найгірший спосіб.
Q6: Можу я виправити NUMA‑проблеми без перезапуску сервісу?
Іноді можна пом’якшити ситуацію зміною афінності CPU і перенаправленням IRQ, але розміщення пам’яті часто задається під час алокації. Чисте виправлення часто вимагає рестарту з правильною прив’язкою і достатнім вільним місцем по вузлу.
Q7: Чи допомагає або шкодить використання huge pages NUMA?
Може допомогти, знижуючи тиск на TLB і зменшуючи накладні витрати CPU. Може зашкодити, якщо ускладнює розміщення пам’яті і міграцію під тиском. Вимірюйте; не припускайте.
Q8: Чи змінює hyperthreading поведінку NUMA?
Hyperthreading не змінює, яка пам’ять є локальною. Воно змінює рівень контеншену на ядро. Для деяких навантажень використання меншої кількості потоків на ядро покращує затримку і зменшує міжсокетний контеншен.
Q9: У Kubernetes яка найпоширеніша NUMA‑пастка?
Guaranteed поди з фіксацією CPU, що потрапляють на один вузол, тоді як алокації пам’яті або NIC IRQ розподіляються в іншому місці. Узгодженість має значення.
Q10: Якщо мені потрібні всі ядра на обох сокетах, який найменш поганий дизайн?
Шардіть за NUMA‑вузлом. Запускайте два worker‑пули, де можна — дві алокаційні арени, пер‑вузлові черги і лише за необхідності обмінюйтеся даними між сокетами. Розглядайте міжсокетний інтерконнект як цінний ресурс.
Наступні кроки, які варто виконати
Двосокетні сервери не прокляті. Вони просто чесні. Вони дуже прямо показують, чи ваша архітектура поважає локальність.
- Виберіть один production‑хост і зафіксуйте топологію + локальність пристроїв:
numactl --hardware,lscpuі файлиnuma_nodeдля NIC/NVMe. - Виберіть ваші топ‑3 сервісів, чутливих до затримки, і зберіть:
numastat -p,taskset -cpі/proc/<pid>/statusпро дозволи пам’яті/CPU під час піку. - Визначте політику для кожного сервісу: прив’язка до одного вузла, шардінг по вузлах або вимірюваний інтерлевінг. Запишіть це. Покладіть у runbook.
- Припиніть зміни «лише pin», якщо зміна не вказує також політику пам’яті і локальність переривань. Якщо ви не можете пояснити повний шлях даних — ви не завершили діагностику.
- Проведіть A/B валідацію (обмеження одним вузлом проти дефолтного) до і після наступного оновлення обладнання. Зробіть NUMA частиною приймального тесту, а не частиною постмортему.