NUMA awareness: чому сервер втрачає продуктивність
Сучасний багатопроцесорний сервер — це не одна велика машина, а кілька обчислювальних вузлів, кожен зі своєю локальною пам'яттю процесора. Поки застосунок працює в межах одного вузла, звернення до пам'яті йде коротким шляхом, і затримка передбачувана.
Щойно потік починає читати дані з чужого вузла, запит іде міжпроцесорною шиною: затримка зростає в рази, а пропускна здатність ділиться між усіма, хто звернувся туди ж.
Підступність у тому, що операційна система за замовчуванням розподіляє пам'ять «за справедливістю»: вузол сторінки визначається першим зверненням, а планувальник має право перенести процес на інше ядро.
Виходить деградація під навантаженням, яку майже не видно на тестовому стенді — на десятку з'єднань усе гладко, а на піку час відгуку зростає ступенями.
Шукають причину в коді застосунку, у диску або в мережі, хоча винуватець — топологія пам'яті, про яку заздалегідь ніхто не домовлявся.
NUMA awareness — це узгоджене налаштування планувальника, засобів віртуалізації та самого застосунку, за якого обчислення тримаються поруч зі своїми даними.
Потоки закріплюються за вузлами, пам'ять виділяється на потрібному вузлі, а міжпроцесорний трафік стає усвідомленим винятком, а не постійним фоном.
Окрема частина роботи — міграція сторінок: автоматичне вирівнювання іноді допомагає, а іноді нескінченно ганяє одні й ті самі сторінки між вузлами, з'їдаючи саме той ресурс, який намагалися заощадити.
Далі розбираємо, як ми знімаємо реальну топологію конкретного сервера і що змінюємо в конфігурації, щоб відгук перестав залежати від того, на який вузол потрапив потік.
Ефект від налаштування розміщення пам'яті на багатопроцесорному сервері
Ефект від налаштування видно не за середнім часом відгуку, а за його розкидом і за поведінкою сервісу на піку.
Поки процеси та їхні сторінки пам'яті розподілені по всіх вузлах, частина запитів іде на віддалений контролер пам'яті — середнє залишається прийнятним, тому проблему довго не помічають, а хвіст розподілу видає її відразу. Нижче — три зрізи, за якими ми вимірюємо результат.
Розкид часу відгуку
Поки застосунок отримує пам'ять з будь-якого вузла, затримка запиту залежить від того, куди його поклав планувальник, — звідси рваний хвіст розподілу і 95-й перцентиль, який у рази вищий за медіану.
Прив'язка процесів до вузлів і політика виділення локальних сторінок стискають розкид: запити, які обслуговувалися в 3–5 разів довше за інші, укладаються у вузький коридор, а сусідні заміри перестають розходитися.
Стабільність і рівномірність потоків під навантаженням
Потоки розкладаються по ядрах усередині вузла, і кожен працює зі своєю локальною пам'яттю — конкуренція за міжсокетну шину перестає бути вузьким місцем.
Пропускна здатність зростає лінійніше: при подвоєнні числа одночасних задач час відповіді збільшується помітно слабше, а не кратно. Для власника сервісу це виглядає як «сервіс перестав просідати в години пік», хоча код застосунку не змінювався.
Усунений клас відмов
Найдорожчий ефект — зникає клас відмов, за якого процес не отримує локальну пам'ять: вузол витрачає ресурс на витіснення сторінок, і запити чекають не обчислень, а доступу до пам'яті.
Частка таких відмов падає до нуля, а топологію та політики ми фіксуємо в конфігурації — рекомендації виробника платформи (Intel) щодо розміщення пам'яті по вузлах переносимо в шаблони, щоб передбачуваність відгуку зберігалася при зростанні навантаження і додаванні нових служб.
Підходи до NUMA awareness: порівняння режимів і обмежень
На багатопроцесорному сервері звернення до пам'яті свого сокета дешевше, ніж до пам'яті сусіднього, і різниця проявляється на хвостових затримках, а не на середньому відгуку. Хто ділить сторінки між вузлами — ядро з автоматичним розміщенням чи ви вручну — визначається політикою. Реальних варіантів чотири, від повної відмови від налаштування до жорсткої прив'язки.
Порівнюємо за критеріями, які зазвичай і вирішують: передбачуваність відгуку, стійкість до зростання числа потоків, вартість супроводу і поведінка при зміні обладнання.
| Варіант | Коли підходить | Обмеження |
|---|---|---|
| Без налаштування, автоматичне розміщення | Стенди та сервіси без вимог до хвоста затримок | Балансувальник переносить сторінки під навантаженням; відгук плаває тим сильніше, чим більше потоків |
| М'яка перевага вузла | Великий спільний робочий набір, потоки розподілені по всіх сокетах | Локальність не гарантована: частина звернень усе одно йде на віддалений вузол |
| Прив'язка процесів до вузлів | СУБД, брокери, кеші з рівним відгуком | Потрібні заміри топології та супровід; при перекосі потоків частина ядер простоює |
| Жорстка прив'язка процесу і пам'яті | Виділений сервер під один сервіс, чутливий до затримок | Немає запасного шляху: вузол заповнений — розміщення відхиляється, перенесення на інше залізо ламає конфігурацію |
Порядок робіт — від слабкого режиму до строгого: спочатку м'яка політика і заміри, потім прив'язка процесів, і лише потім жорстка. Якщо почати з останньої, виходить зворотна задача: вузли недовантажені, вільна пам'ять є, але недоступна.
Порядок впровадження: від схеми топології до передачі регламенту
Впровадження йде поетапно: кожна зміна підтверджується заміром і залишається оборотною. Починаємо з аудиту топології — без карти вузлів, ядер і прив'язки пристроїв введення-виведення будь-яка правка перетворюється на вгадування.
Далі переносимо висновки в схему розміщення, налаштовуємо, перевіряємо під навантаженням і передаємо регламент експлуатації.
-
Аудит топології. Збираємо карту вузлів і ядер, зв'язків між ними, прив'язки мережевих карт і дискових контролерів. Фіксуємо базові заміри: затримку доступу до локальної та віддаленої пам'яті, розподіл потоків по вузлах.
-
Схема розміщення. Визначаємо, які сервіси прив'язуємо до вузлів жорстко, які отримують лише перевагу, а які залишаємо вільними. Окремо проговорюємо, де потрібна ізоляція — інакше м'яка прив'язка непомітно стане жорсткою.
-
Налаштування. Переносимо схему в конфігурацію: правила прив'язки процесів і обробки переривань, розподіл пам'яті, параметри планувальника. Кожна зміна — окремим кроком із точкою відкату.
-
Навантажувальна перевірка. Ганяємо профіль, близький до робочого: змішані запити, пікові сплески, тривалий фон. Дивимося не лише на середню пропускну здатність, а й на хвостові затримки.
-
Розбір відхилень. Звіряємося з базовими замірами і пояснюємо кожну розбіжність: де спрацювала локальність, де прив'язка звузила ресурси. Спірні правила прибираємо, а не залишаємо «на всяк випадок».
-
Регламент експлуатації. Передаємо карту вузлів, чинні правила прив'язки, список того, що не можна змінювати без повторного заміру, і порядок перевірки після оновлення ядра або заміни заліза.
Що замовник отримує на руки: схеми, профілі, регламенти
Після налаштування NUMA awareness робота закінчується не звітом, а набором артефактів, з якими ваша команда живе далі — без нас. Усе складається в репозиторій конфігурацій і в документацію по серверу, структура та назви узгоджуються з вашими інженерами на етапі здачі.
- Карта топології. Схема розміщення вузлів пам'яті та ядер: видно, які ядра ходять за пам'яттю в сусідній вузол і де втрачається пропускна здатність. За нею планується посадка нових сервісів без повторних замірів.
- Профілі прив'язки. Готові набори для бази даних, кешу і балансувальника: які процеси закріплені за якими ядрами і за якої кількості потоків. Профіль застосовується шаблоном, а не збирається заново.
- Політики доступу. Хто має право змінювати прив'язку, які правки потребують прогону на стенді і що заборонено чіпати на бойовому сервері. Закриває відмови виду «поправили ядра руками».
- Документація по серверу. Паспорт вузла: стан до і після налаштування, реперні заміри по пам'яті та затримках, зв'язок конфігурації зі складом заліза і режимами системи.
- Регламент обслуговування. Порядок планових робіт: як зберегти і відновити профілі, що перевіряти після заміни заліза, порядок приймання нового сервера в той самий профіль.
- Скрипти перевірки. Автотест, що прив'язка застосувалася і тримається після перезапуску сервісів, зі звітом на одну сторінку для чергового інженера.
Кейс: як прибрали деградацію відгуку на серверах з двома процесорами
Клієнт — сервіс, де на кожному сервері стоїть по два процесори, а база працює під управлінням PostgreSQL.
На піковому навантаженні окремі запити стали відповідати в рази довше за інші: процеси ходили за даними в пам'ять сусіднього процесора, і черга на обслуговування зростала швидше, ніж саме навантаження.
Що показали заміри
Частка звернень до віддаленої пам'яті доходила до 34%, і затримки концентрувалися саме на цих запитах: розкид часу відгуку між найшвидшими і найповільнішими операціями відрізнявся майже на порядок.
При виході на пік сервіс не сповільнювався рівномірно, а починав скидати частину запитів через перевищення часу очікування — тобто деградація відгуку виглядала як обрив, а не як плавне зниження.
Що змінили і що отримали
Ми прив'язали процеси бази і набір з'єднань до локальних вузлів пам'яті, задали політику розміщення сторінок і вимкнули автоматичну міграцію, через яку сторінки переїжджали між процесорами в найневідповідніший момент. Додатково розвели по різних вузлах сервіси, що конкурували за одну й ту саму пам'ять.
Частка віддалених звернень упала до 3-4%, розкид часу відгуку скоротився приблизно в шість разів, а пікова пропускна здатність зросла майже вдвічі — без заміни обладнання.
Деградація відгуку на піку, заради якої запускався проєкт, більше не відтворюється ні на стенді, ні в робочому середовищі.
Що робити, якщо сервер уже налаштований вручну і без документації?
Успадкована конфігурація майже завжди багатошарова: частина прив'язок задана параметрами завантаження ядра, частина — в юнітах systemd, частина — командами, які колись виконали вручну і не зафіксували.
Перший крок — нічого не змінювати, а зняти поточну схему розміщення як вона є: які процеси закріплені за вузлами, як розподілені переривання і що реально бачить ядро, а не що записано в старих нотатках.
Зріз робиться до будь-яких правок і зберігається як еталон для порівняння: відомості про топологію і поточні ваги вузлів, список прив'язок по процесах, налаштування груп процесорів, параметри завантаження ядра.
Поруч — метрики локальності: частка звернень до «чужого» вузла і час виконання задач, які в локальну пам'ять не потрапляють. Без цього еталона потім неможливо довести, що стало краще, а не просто інакше.
Окремо розбираємо, що саме компенсувала ручна настройка. Часто вона лікувала симптом — перекіс переривань на один вузол або невдалий вибір вузла планувальником — і при знятті прив'язок дефект повертається.
Тому перевірка перед змінами включає звірку задуманого з працюючим: правило може не застосовуватися взагалі, якщо процес перезапускається з іншого юніта або контейнер ігнорує успадковану маску.
Зворотна сумісність тримається на порядку робіт: змінюємо по одному шару, починаючи з найменш пов'язаного з навантаженням, і зберігаємо готовий відкат до знятого зрізу.
Першим іде пілотний вузол, де порівнюються ті самі метрики локальності та затримки під контрольним навантаженням. Лише після стійкого результату правило переноситься на інші машини.
У підсумку конфігурація з набору ручних здогадок перетворюється на описану і відтворювану: схема розміщення задокументована, кожна зміна проходить через порівняння з еталоном, а повернення до попереднього стану можливе в будь-який момент.
Технічні сумніви: сумісність, ризики, обмеження
Прив'язка процесів до локальних вузлів пам'яті працює і на фізичному сервері, і всередині віртуальної машини: гіпервізор публікує гостю топологію, а ядро гостя передає її далі — у менеджери ресурсів і контейнери.
Межа застосовності проста: якщо VMware або інший гіпервізор маскує топологію чи сам розмазує навантаження по всіх ядрах, виграш усередині гостя буде помітно меншим, ніж на «голому залізі».
Тому ми починаємо з перевірки: чи бачить система кілька вузлів і чи збігається їхня нумерація з реальними сокетами — при незбігу будь-яка прив'язка веде процеси не туди, куди планувалося.
Вплив на вже працюючі сервіси залежить від способу налаштування.
Зміна політики розподілу пам'яті на рівні ядра застосовується до нових процесів і не чіпає поточні, а прив'язка через менеджер ресурсів потребує акуратного перезапуску — під неї ми виділяємо вікно і обкатуємо на одному вузлі кластера, звіряючи затримки до і після.
Кожна зміна оформлюється оборотно: профіль вимикається однією правкою конфігурації, і сервіс повертається до попереднього режиму без перезбирання середовища.
Чи сумісно це з віртуалізацією та контейнерами?
Так, за умови, що топологія не прихована. У контейнерах прив'язка задається на рівні менеджера ресурсів і успадковується процесами; у віртуалках — лише якщо гостю видно окремі вузли.
Якщо топологія прихована, метод не застосовуємо і говоримо про це прямо, а не зображаємо налаштування.
Чи доведеться зупиняти працюючі сервіси?
Не завжди. Зміни політики на рівні ядра підхоплюють нові процеси, і сервіс продовжує працювати, поступово переходячи на нову схему. Перезапуск потрібен там, де управління пам'яттю йде через менеджер ресурсів — такі роботи плануємо у вікно обслуговування.
Як відкочуються зміни, якщо стане гірше?
Конфігурація зберігається в системі керування версіями, відкат — повернення попереднього профілю і перезапуск зачеплених сервісів.
Перед виведенням на весь кластер ми залишаємо еталонні заміри, щоб порівняти поведінку після відкату і переконатися, що повернулися рівно до початкового стану.
Обговоримо задачу: надішліть схему серверів і профіль навантаження
Щоб зрозуміти, що дасть NUMA awareness саме на ваших серверах, не потрібен доступ до робочого контуру — достатньо вихідних даних про залізо і профіль навантаження. Чим повніша картина, тим менше припущень у розрахунку і тим реальніші терміни робіт. Ось що корисно додати до опису задачі.
- вивід
lscpuіnumactl --hardwareз кожного сервера — топологія вузлів, розкладка пам'яті по каналах, відстані між вузлами; - схема розміщення сервісів: що живе в контейнерах, що у віртуальних машинах, де увімкнена міграція і як налаштовані ліміти;
- профіль навантаження — які процеси чутливі до затримки, як навантаження змінюється по годинах, де піки і як довго тримаються;
- поточні прив'язки і параметри ядра: unit-файли з CPUAffinity, політики NUMA-балансування, налаштування планувальника;
- метрики під навантаженням: промахи локальної пам'яті, частка звернень до віддаленого вузла, час відгуку сервісу на піку;
- опис інцидентів: у який момент відгук зростає, що вже пробували і що дало ефект;
- вікна обслуговування і вимоги до відкату — чи можна перезавантажувати вузли, чи є гарячий резерв.
Надсилайте те, що є: повний набір не обов'язковий, відсутнє ми перелічимо окремо і підкажемо, як зняти.
Після розбору схеми і профілю навантаження ми збираємо модель розподілу процесів і пам'яті, відзначаємо, де прив'язка дасть виграш, а де впреться в ліміти, і повертаємося з оцінкою та планом — етапи, очікуваний ефект по затримках і утилізації, терміни робіт. Опишіть задачу, і обговоримо, з чого почати.







