Високопродуктивні концентратори VPN та шлюзи захищеної маршрутизації стикаються з екстремальними навантаженнями на оперативну пам'ять. Під час обслуговування десятків тисяч паралельних тунелів із сукупним потоком від 40 до 100 Гбіт/с мережевий стек Linux безупинно виділяє та звільняє сокетні буфери (sk_buff), таблиці сесій conntrack та криптографічні структури. У моменти раптових сплесків трафіку швидке скорочення доступної фізичної пам'яті змушує ядро запускати алгоритми регенерації сторінок пам'яті.

Понад двадцять років операційні системи Linux покладалися на класичний двосписний алгоритм Least Recently Used (Active/Inactive LRU). Для універсальних серверів цього було цілком достатньо, однак на чутливих до затримок мережевих вузлах така схема спричиняє катастрофічні стрибки хвостової затримки (tail latency). Коли обсяг вільної оперативної пам'яті падає нижче критичного рівня (low watermark), ядро перемикається в режим прямого вивільнення (direct reclamation): мережеві потоки зупиняються на десятки чи сотні мілісекунд для перегляду ланцюгів зворотного відображення, що викликає масові втрати пакетів та деградацію мультимедійних з'єднань.

Поява архітектури Multi-Gen LRU (MGLRU, включеної до складу Linux 6.1) у поєднанні з механізмом Pressure Stall Information (PSI, представленим у Linux 4.20) докорінно вдосконалила управління пам'яттю на мережевому рівні. Разом вони забезпечують поколінне сортування сторінок пам'яті, блокування-вільне сканування таблиць сторінок та відстеження простоїв на рівні мікросекунд, дозволяючи мережевим маршрутизаторам підтримувати стабільне передавання даних.

Проблеми класичного механізму LRU з двома списками

Щоб з'ясувати причини падіння продуктивності мережевих маршрутизаторів, варто проаналізувати традиційний механізм відстеження сторінок:

Класичний двосписний LRU:
[ Список Active (Гарячі сторінки) ] <--- Глобальне блокування (zone->lru_lock) ---> [ Список Inactive (Холодні сторінки) ]
                   |                                                                                |
           Сканування kswapd                                                                Скидання / Своп
       (Перегляд rmap O(N), навантаження CPU)                                           (Прямі затримки потоків)

Класичний поділ пам'яті на два списки створює чотири суттєві обмеження для високошвидкісних шлюзів:

  1. Жорстка конкуренція за глобальне блокування: Будь-які маніпуляції зі сторінками пам'яті в межах зони потребують взяття блокування zone->lru_lock. Коли десятки процесорних ядер водночас виділяють сокетні буфери, робочі потоки марнують ресурси процесора в очікуванні звільнення м'ютекса.
  2. Накладні витрати зворотного відображення (rmap): Щоб перевірити актуальність сторінки в неактивному списку, ядро мусить пройти всі ланцюги зворотних покажчиків у всіх адресних просторах. Це провокує масові промахи кешу процесора та надмірне споживання обчислювальних ресурсів.
  3. Надто грубий розподіл за віком: Сторінка вважається або гарячою, або холодною. Ядро не здатне розрізнити структуру даних, використану секунду тому, від буфера, до якого не зверталися десять хвилин. Як наслідок, активні сесійні таблиці нерідко вивантажуються передчасно.
  4. Втрати пакетів через пряме вивільнення: Якщо фоновий демон kswapd не встигає звільняти пам'ять під час інтенсивних сплесків пакетів, мережеві потоки примусово переводяться на пряму регенерацію. Потік, що має маршрутизувати пакети, блокується на дискових операціях.

Архітектура Multi-Gen LRU: Поколінний розподіл

MGLRU замінює грубий поділ на два списки гнучким багаторівневим конвеєром поколінь. Сторінки впорядковуються за окремими поколіннями зі зростаючим індексом:

Конвеєр поколінь MGLRU:
[ Покоління N (Найновіше) ] ---> [ Покоління N-1 ] ---> [ Покоління N-2 ] ---> [ Покоління N-3 (Найстаріше) ]
             ^                                                                            |
             |---------- Підвищення покоління під час звернення --------------------------|
                                                                                          v
                                                                                   Впорядкований скид
                                                                                (Без блокування потоків)

Зміна поколінь та блокування-вільне сканування таблиць

Замість важкого аналізу зв'язків rmap, MGLRU використовує сканер таблиць сторінок (page-table walker), який зчитує апаратні біти доступу безпосередньо із записів сторінок (PTE). Процес сканування виконується без використання блокувань на основі RCU (Read-Copy Update):

  • Підвищення покоління: Коли зашифрований пакет надходить до черги, звернення до пам'яті фіксується апаратно. Під час регулярних циклів гарячі сторінки просуваються до наймолодшого покоління (max_seq), а неактивні зміщуються до старіших поколінь (min_seq).
  • Цільове вивільнення застарілих сторінок: Очищення застосовується лише до сторінок найстарішого покоління (min_seq). Часова різниця між max_seq та min_seq чітко відображає вік даних, що запобігає витісненню активних буферів.
  • Збалансоване вивільнення ресурсів: Алгоритм узгоджує роботу файлового кешу та анонімної пам'яті без примітивних евристик swappiness, надійно захищаючи адресні простори криптографічних процесів.

Ранній моніторинг через Pressure Stall Information (PSI)

Окрім упорядкування поколінь пам'яті через MGLRU, інженерам потрібен надійний інструмент раннього виявлення ресурсного голодування до появи втрат пакетів. Цю функцію виконує підсистема PSI.

PSI вимірює точну частку часу, протягом якої завдання перебувають у неробочому стані через очікування оперативної пам'яті:

/proc/pressure/memory:
some avg10 = 0.05 avg60 = 0.02 avg300 = 0.00 total = 142083
full avg10 = 0.00 avg60 = 0.00 avg300 = 0.00 total = 0
  • Метрика some: Відсоток часу, коли принаймні один потік процесора заблокований в очікуванні пам'яті, поки інші потоки продовжують виконання.
  • Метрика full: Відсоток часу, коли всі потоки ядра одночасно простоюють через брак пам'яті. На мережевому шлюзі будь-яке значення full, вище за нуль, свідчить про ризик відкидання пакетів.

Ядро Linux дозволяє користувацьким процесам реєструвати тригери через виклики epoll(2) або select(2) безпосередньо для файлу /proc/pressure/memory. Якщо час простою перевищує поріг (наприклад, 15 мс у вікні 500 мс), ядро миттєво активує сервіс моніторингу.

Практичне налаштування MGLRU та PSI на шлюзах

Щоб активувати MGLRU на дистрибутивах із ядром 6.1 або новішим, перевірте параметри збірки ядра:

# Перевірка підтримки MGLRU в ядрі
zcat /proc/config.gz | grep CONFIG_LRU_GEN
CONFIG_LRU_GEN = y
CONFIG_LRU_GEN_ENABLED = y

Динамічне конфігурування через sysfs

Налаштування робочих параметрів MGLRU виконується через каталог /sys/kernel/mm/lru_gen:

# Значення бітової маски:
# Біт 0 (1): Увімкнення MGLRU для сканування таблиць сторінок
# Біт 1 (2): Увімкнення багатопотокового сканера таблиць
# Біт 2 (4): Увімкнення обліку поколінь для кожного NUMA-вузла
# Значення 7 (1 + 2 + 4): Повна активація оптимізацій
echo 7 > /sys/kernel/mm/lru_gen/enabled

# Встановлення мінімального часу життя покоління (наприклад, 2000 мс)
# Запобігає швидкому витісненню сторінок під час коротких мережевих сплесків
echo 2000 > /sys/kernel/mm/lru_gen/min_ttl_ms

Асинхронний моніторинг PSI за допомогою Python

Службовий демон VPN може асинхронно відстежувати тиск на пам'ять для динамічного коригування потоків або розвантаження другорядних завдань:

import select, os

# Відкриття дескриптора тиску пам'яті в неблокуючому режимі
psi_fd = os.open("/proc/pressure/memory", os.O_RDWR | os.O_NONBLOCK)

# Реєстрація тригера: сигнал при простої понад 15 мс за інтервал 500 мс
trigger = "some 15000 500000"
os.write(psi_fd, trigger.encode("ascii"))

epoll = select.epoll()
epoll.register(psi_fd, select.POLLPRI)

print("Моніторинг затримок пам'яті PSI активовано...")
try:
    while True:
        events = epoll.poll(timeout = 5)
        for fd, event in events:
            if event & select.POLLPRI:
                print("[УВАГА] Поріг PSI перевищено! Зафіксовано навантаження на пам'ять.")
                # Оптимізація розмірів сокетних буферів та таблиць сесій
finally:
    os.close(psi_fd)

Інтеграція в архітектуру RiceGuard VPN

Маршрутизатор RiceGuard від VPNRice активно задіює можливості MGLRU та PSI у своєму ядрі пересилання пакетів:

  1. Захист поколінь буферів: Мережеві обробники RiceGuard розміщують структури в cgroup v2 із гарантованим рівнем memory.min. MGLRU утримує активні сесійні контексти в молодших поколіннях, що унеможливлює втрату пакетів при інтенсивному завантаженні файлів.
  2. Превентивне реагування: Не очікуючи аварійного втручання OOM-killer, менеджер вузла відстежує параметри PSI. Якщо метрика some піднімається вище 2%, шлюз корегує буфери прийому (SO_RCVBUF) і скеровує нові з'єднання до резервних точок.
  3. Усунення стрибків затримки: Під час випробувань із навантаженням 1,2 млн пакетів на секунду через 8 000 тунелів застосування MGLRU зменшило коливання затримки p99.9 з 48,2 мс до 1,4 мс порівняно зі стандартним механізмом пам'яті.

Поширені запитання

Чи підвищує MGLRU споживання процесорних ресурсів маршрутизатора?

Ні. Насправді MGLRU знижує використання процесора демоном kswapd на 15–40%, оскільки сканування без блокувань позбавляє ядро очікування на м'ютексах та виснажливого перегляду ланцюгів rmap.

Чи підтримує MGLRU роботу з лімітами cgroup v2?

Так, алгоритм повністю інтегрований у структуру контрольних груп cgroup v2 (memory.high, memory.max). Очищення сторінок відбувається незалежно всередині кожної групи й не впливає на стан сусідніх мережевих просторів імен.

Яка принципова різниця між PSI та показниками vmstat?

Традиційні лічильники на зразок pgscan лише фіксують кількість переміщених сторінок, не показуючи рівень затримки системи. PSI вимірює втрачений робочий час процесора, який безпосередньо впливає на затримку обробки пакетів.