Дилема затримки: чому апаратні черги драйверів руйнують роботу AQM

На високопродуктивних VPN-шлюзах та серверах транзиту шифрованого трафіку системні адміністратори нерідко стикаються з несподіваною деградацією зв'язку. Навіть коли на мережевому інтерфейсі налаштовано сучасні механізми активного керування чергами (Active Queue Management, AQM) — такі як FQ-CoDel або CAKE — під час пікового навантаження виникають сплески затримки від 50 до 150 мілісекунд. Голосовий трафік (VoIP) тремтить, у телеметрії ігрових сесій з'являється джитер, а швидкість передавання TCP коливається.

Причина цієї проблеми криється не в програмній дисципліні обслуговування черг (qdisc). Затримка накопичується нижче за стеком — безпосередньо в кільцевому буфері передавання (TX ring) драйвера та фізичної мережевої плати (NIC).

Сучасні гігабітні та 10-гігабітні контролери Ethernet використовують довгі кільця дескрипторів (зазвичай 1024, 2048 або 4096 позицій), щоб гарантувати відсутність простоїв обладнання на максимальній швидкості каналу. Для звичайного локального TCP-трафіку ядро Linux застосовує механізм TCP Small Queues (TSQ), який обмежує накопичення пакетів на рівні сокетів. Проте VPN-шлюзи працюють з інкапсульованим трафіком:

[ Клієнтські пакети TCP/UDP ] ---> [ Тунельний рушій VPN (WireGuard / IPsec) ]
                                                    |
                                                    v
                                   [ Зовнішні інкапсульовані UDP-датаграми ]
                                                    |
                                                    v
                                          [ Egress-стек Linux ]

Для ядра Linux інкапсульований тунельний потік виглядає як звичайні датаграми UDP, які позбавлені зворотного сокетного контролю перевантаження. Коли VPN-рушій генерує пакети великими порціями, буфер TX драйвера заповнюється миттєво.

Поки пакети заблоковані у фізичній пам'яті адаптера, алгоритми керування qdisc не здатні ні змінити їхній порядок, ні скинути пакети відповідно до моделі CoDel. Чутливий до затримок інтерактивний трафік (DNS-запити, квитанції WireGuard, голос) опиняється в хвості гігантської черги за масивними блоками завантаження файлів.

Внутрішня будова dql: адаптивний автомат вимірювання байтів

Для ліквідації буферблоту на рівні драйверів у ядро Linux інтегровано механізм Byte Queue Limits (BQL), побудований на бібліотеці Dynamic Queue Limits (DQL) у файлі lib/dynamic_queue_limits.c.

Розроблений Томом Гербертом (Tom Herbert), механізм BQL переносить точку накопичення черги з апаратного драйвера назад у контрольовану дисципліну qdisc ядра. На відміну від застарілих схем, що рахували кількість пакетів, BQL працює суворо з кількістю байтів. Це критично, оскільки короткий пакет у 64 байти займає той самий апаратний дескриптор, що й агрегований GSO-кадр розміром 64 кілобайти, але потребує в тисячу разів менше часу для передавання по дроту.

+-------------------------------------------------------------+
|              Програмна дисципліна черги (qdisc)             |
|                  FQ-CoDel / CAKE / PFIFO_FAST               |
+-------------------------------------------------------------+
                               |
                   netdev_tx_sent_queue(dev_queue, bytes)
                               v
+-------------------------------------------------------------+
|             Апаратний кільцевий буфер драйвера (TX Ring)    |
|    Максимальна місткість: 1024 дескриптори                  |
|    Динамічний ліміт BQL: ~3 000 – 12 000 байтів             |
+-------------------------------------------------------------+
                               |
                  (Фізичне передавання в кабель)
                               |
                 netdev_tx_completed_queue(dev_queue, pkts, bytes)
                               v
+-------------------------------------------------------------+
|                 Автомат розрахунку dql.c                    |
|      Вимірювання часу вивільнення та оновлення ліміту       |
+-------------------------------------------------------------+

Алгоритм підтримує стан адаптації на основі трьох ключових показників:

  1. num_queued: загальна кількість байтів, переданих адаптеру, завершення обробки яких ще не підтверджено перериванням.
  2. adj_limit: поточна динамічна межа обсягу даних у буфері драйвера перед призупиненням черги ядра.
  3. lowest_slack: найменший зафіксований запас байтів у кільці протягом вимірювального вікна.

Цикл функціонування dynamic_queue_limits.c

Щоразу, коли стек надсилає пакет у драйвер, викликається функція netdev_tx_sent_queue(). Якщо значення num_queued перевищує розрахований ліміт, BQL тимчасово зупиняє передавання з черги пристрою (netif_tx_stop_queue()). Драйвер більше не отримує нових пакетів — надлишковий трафік накопичується у qdisc, де FQ-CoDel контролює пріоритети.

Коли мережевий адаптер відправляє кадри у фізичний канал і надсилає переривання про завершення (NAPI poll), драйвер викликає netdev_tx_completed_queue(). Алгоритм миттєво оцінює стан черги:

  • Залишковий надлишок (slack > 0): якщо протягом інтервалу в буфері драйвера постійно залишалися невідправлені дані й адаптер жодного разу не простоював, попередній ліміт був завищеним. BQL зменшує adj_limit, примусово звужуючи апаратний буфер для скорочення мілісекунд затримки.
  • Ризик простою (starvation): якщо буфер випорожнився швидше, ніж надійшла нова порція даних, адаптер міг знизити пропускну здатність. BQL поступово збільшує adj_limit, гарантуючи повне навантаження лінії передавання.

Завдяки цьому зворотному зв'язку в кільцевому буфері фізичної плати постійно підтримується рівно стільки байтів, скільки потрібно для безперервного завантаження каналу (часто лише 4–12 кілобайтів на інтерфейсі 10 Гбіт/с, що становить менше 50 мікросекунд очікування).

Значення BQL для тунельних шлюзів UDP VPN

На шлюзах під керуванням WireGuard, RiceGuard або IPsec оптимізація буферизації BQL надає вирішальні переваги:

1. Нейтралізація великих сплесків GSO/GRO

Сучасні тунельні системи спираються на механізм Generic Segmentation Offload (GSO). Коли клієнт отримує масивний потік даних, ядро групує пакети в супер-кадри розміром до 64 КБ перед шифруванням. Без BQL лише кілька таких кадрів здатні заповнити чергу адаптера на десятки мілісекунд. З активним BQL драйвер приймає рівно стільки байтів, скільки здатний відправити негайно, а решта супер-кадрів очікує в черзі qdisc.

2. Ефективне сортування FQ-CoDel

Коли черга формується в ядрі, а не в темному буфері плати, алгоритм FQ-CoDel хешує кожен окремий потік за 4-кортежем (адреси та порти). Критичні для користувача пакети (підтвердження сесій, голосовий трафік, пінги) потрапляють у власні розріджені черги і передаються першими, випереджаючи завантаження великих файлів.

3. Захист ієрархічної структури RiceGuard

У шлюзах з архітектурою RiceGuard, де реалізовано динамічну пріоритизацію за тунелями та віртуальними каналами, BQL гарантує, що встановлені пріоритети клієнтів миттєво втілюються в каналі без змішування в неконтрольованій апаратній пам'яті.

Моніторинг та перевірка BQL у середовищі Linux

Перевірити статус BQL на працюючому сервері VPN можна за допомогою віртуальної файлової системи sysfs. Для кожного фізичного порту та кожної черги передавання параметри доступні за шляхом /sys/class/net/<інтерфейс>/queues/tx-<N>/byte_queue_limits/:

# Перегляд параметрів BQL для першої черги інтерфейсу eth0
ls -la /sys/class/net/eth0/queues/tx-0/byte_queue_limits/

# Доступні метрики:
# - limit: поточний розрахований DQL ліміт у байтах
# - limit_max: верхня межа байтів
# - limit_min: нижній поріг для запобігання простоям
# - hold_time: час утримування стабільного значення (у мілісекундах)
# - bytes: лічильник відправлених байтів
# - packets: лічильник переданих пакетів

Для спостереження за адаптацією ліміту в реальному часі під навантаженням:

watch -n 1 'for q in /sys/class/net/eth0/queues/tx-*/byte_queue_limits/limit; do
  echo -n "$q: "; cat "$q";
done'

Якщо каталог byte_queue_limits відсутній у директорії черги, це свідчить про те, що використовуваний драйвер не має вбудованої підтримки BQL. Більшість промислових драйверів для серверів (e1000e, igb, ixgbe, i40e, ice, bnxt_en, mlx4, mlx5) мають повноцінну підтримку BQL з версії ядра Linux 3.3.

Практичні рекомендації для налаштування шлюзу VPN

Для досягнення мінімальної затримки та стабільного пінгу на шлюзах дотримуйтеся таких інженерних правил:

1. Використовуйте FQ-CoDel або CAKE

BQL сам по собі не впорядковує пакети за пріоритетами, а лише забезпечує умови, за яких програмний диспетчер працює ефективно. Встановіть FQ-CoDel на фізичному вихідному інтерфейсі:

# Призначення fq_codel на фізичний інтерфейс сервера
sudo tc qdisc replace dev eth0 root fq_codel ecn

2. Не зменшуйте штучно розмір апаратного кільця

Поширена помилка — ручне зменшення черги через ethtool -G eth0 tx 128. За наявності BQL у цьому немає потреби: ядро самостійно обмежує глибину буфера відповідно до швидкості каналу, зберігаючи запас дескрипторів для захисту від переривань та стрибків навантаження CPU.

3. Контроль на зовнішньому фізичному інтерфейсі

Пам'ятайте, що віртуальні тунельні інтерфейси (wg0 або tun0) не мають фізичного кільця драйвера. Механізм BQL функціонує на фізичній мережевій платі (eth0), через яку транспортується вихідний зашифрований потік UDP до магістрального провайдера.

Поєднання BQL на рівні драйвера та FQ-CoDel на рівні ядра гарантує передбачувану мінімальну затримку для всіх клієнтських тунелів навіть за максимальної утилізації каналу зв'язку.