Високопродуктивні шлюзи VPN та проксі-вузли обробляють мільйони інкапсульованих мережевих кадрів щосекунди. Хоча сучасні мережеві карти підтримують швидкість у десятки гігабіт, проксі простору користувача нерідко натрапляють на невидиму межу пропускної здатності через перевантаження процесора обробкою програмних переривань (softirq) та вичерпання ліміту пакетів на секунду (PPS). Головною причиною таких затримок є фрагментація протоколу: запис коротких службових заголовків тунелю безпосередньо перед корисним навантаженням, через що стек ядра генерує велику кількість мікропакетів (tinygrams).

Щоб усунути надлишкові витрати на серіалізацію без затримок класичних алгоритмів керування перевантаженням, ядро Linux надає два примітиви сокетів: опцію TCP_CORK та прапорець надсилання MSG_MORE. Розуміння принципів керування чергами передавання дозволяє мережевим інженерам максимізувати пропускну здатність, оптимізувати використання кешу процесора та утримувати затримку комутації на мікросекундному рівні.

Дилема мікропакетів Tinygrams: Накладні витрати кадрування та пакетний шторм у VPN

В архітектурах проксі простору користувача—таких як WireGuard-over-TCP, Shadowsocks або тунелі з інкапсуляцією TLS—конвеєр передавання часто розділяє службові заголовки та корисні дані. Програма зазвичай записує невеликий заголовок кадрування (наприклад, 16 байтів із довжиною пакета та криптографічним nonce), а потім записує корисні дані (наприклад, IP-пакет розміром 1420 байтів, отриманий з інтерфейсу TUN).

Операції запису сокета у програмі:
  write(fd, &header, 16);     ──> [TCP/IP 54B] + [16B Header]  = 70B на фізичній лінії
  write(fd, &payload, 1420);   ──> [TCP/IP 54B] + [1420B Payload] = 1474B на фізичній лінії

Витрати, що виникають:
  - Формування 2 окремих мережевих пакетів
  - 108 байтів службових заголовків протоколу
  - Використання 2 дескрипторів кільця приймання (Ring Descriptors)
  - 2 апаратні переривання на стороні вузла-одержувача

Коли увімкнено TCP_NODELAY для скасування затримок алгоритму Нейгла, кожен окремий системний виклик write() негайно формує TCP-сегмент і відправляє його драйверу мережевої карти. Запис заголовка розміром 16 байтів викликає додавання повного 54-байтного заголовка TCP/IP (20 байтів IPv4 + 20 байтів TCP + 14 байтів Ethernet), формуючи мікропакет розміром 70 байтів. Це породжує серйозні експлуатаційні проблеми:

  1. Нераціональні витрати смуги пропускання: Окреме надсилання заголовків кадрування забирає від 4% до 15% загальної місткості мережевого каналу лише на дубльовані заголовки TCP/IP.
  2. Перевантаження пакетної продуктивності (PPS): Подвоєння кількості пакетів на кожну логічну операцію передавання перевантажує проміжні маршрутизатори, комутаційні вузли та таблиці відстеження з'єднань conntrack фаєрволів.
  3. Виснаження процесора перериваннями softirq: Мережева карта одержувача змушена постійно генерувати апаратні переривання, виконувати цикли опитування NAPI та виділяти пам'ять sk_buff для двох окремих кадрів замість одного об'єднаного сегмента.

Як опція TCP_CORK трансформує механізм передавання даних у ядрі Linux

Опція сокета TCP_CORK наказує мережевому стеку ядра Linux накопичувати всі вихідні дані до досягнення максимального розміру сегмента (MSS) перед їх фізичним надсиланням, буквально закриваючи сокет затискачем.

У коді мережевої підсистеми ядра (net/ipv4/tcp_output.c) надсилання даних із буфера до структур sk_buff контролюється функціями tcp_push() та tcp_write_xmit(). При активації TCP_CORK через setsockopt() ядро встановлює внутрішній прапорець tp->nonagle & TCP_NAGLE_CORK.

На відміну від алгоритму Нейгла (RFC 896), який затримує пакети лише за наявності непідтверджених даних у польоті, TCP_CORK діє безумовно. Навіть коли всі попередні сегменти підтверджені одержувачем, ядро відмовляється надсилати будь-який сегмент, менший за MSS, доки не настане одна з двох умов:

  • Явне зняття обмеження (Uncorking): Програма скасовує опцію TCP_CORK через setsockopt(), спонукаючи ядро негайно скинути залишок даних із буфера передавання у мережу.
  • Запобіжний таймер ядра: Щоб уникнути зависання через зупинку програми, ядро підтримує внутрішній таймер на 200 мс. Якщо дані залишаються заблокованими понад 200 мс, ядро автоматично виштовхує пакет у канал.
Буфер передавання сокета за увімкненого TCP_CORK:
  ┌─────────────────────────────────────────────────────────────┐
  │ [16B Header] + [1420B Payload]                              │
  └─────────────────────────────────────────────────────────────┘
                               │
                      Ядро перевіряє MTU/MSS
                               ▼
               [Один об'єднаний сегмент 1490B на лінії]
               (1 дескриптор NIC, 1 фізичний пакет)

Зберігаючи неповний службовий заголовок у буфері сокета до прибуття наступної порції корисних даних, TCP_CORK з'єднує обидва записи в один суцільний мережевий кадр, повністю усуваючи появу мікропакетів.

MSG_MORE: Гранулярна агрегація без системних викликів у сучасних сокетах

Хоча TCP_CORK вирішує питання агрегації, багаторазові виклики setsockopt() створюють помітні накладні витрати на перемикання контексту ядра. Увімкнення блокування перед заголовком і його вимкнення після корисних даних вимагають двох додаткових системних викликів на кожне логічне повідомлення, що збільшує навантаження на процесор.

Для усунення цих витрат сучасні версії ядра Linux підтримують прапорець MSG_MORE у викликах send() та sendto().

Прапорець MSG_MORE діє як тимчасове утримання даних на рівні окремої операції. Коли програма передає MSG_MORE у функцію send(), ядро виставляє стан TCP_NAGLE_CORK винятково для цього запису, повідомляючи стек TCP про те, що незабаром надійдуть додаткові дані.

// Крок 1: Відправлення заголовка 16 байтів із прапорцем MSG_MORE (утримується у буфері)
send(sock_fd, header_buf, 16, MSG_MORE);

// Крок 2: Відправлення корисного навантаження без MSG_MORE (автоматичне скидання)
send(sock_fd, payload_buf, 1420, 0);

Під час першого виклику ядро додає 16 байтів до поточного буфера передавання sk_buff без виходу на лінію. Під час другого виклику решта 1420 байтів додаються безпосередньо в ту саму структуру sk_buff. Оскільки в останньому виклику прапорець MSG_MORE відсутній, функція tcp_push() миттєво передає цілісний сегмент розміром 1436 байтів драйверу мережі. Це забезпечує агрегацію даних без жодного додаткового виклику setsockopt().

Синергія та суперечності: Взаємодія TCP_NODELAY, TCP_CORK та пейсингу BBR

Мережеві фахівці часто досліджують можливість спільного використання TCP_NODELAY та TCP_CORK на одному дескрипторі сокета. У мережевому стеку Linux ці параметри узгоджені в межах чіткої ієрархії:

Ієрархія пріоритетів у ядрі Linux (net/ipv4/tcp_output.c):
  ┌──────────────────────────────────────────────┐
  │ TCP_CORK / MSG_MORE                          │  <-- Найвищий пріоритет
  │ (Примусове накопичення даних до MSS)         │
  └──────────────────────┬───────────────────────┘
                         │
                         ▼
  ┌──────────────────────────────────────────────┐
  │ TCP_NODELAY                                  │  <-- Вторинний пріоритет
  │ (Миттєве скидання залишку без очікування ACK)│
  └──────────────────────────────────────────────┘
  1. TCP_CORK переважає над TCP_NODELAY: Коли активна опція TCP_CORK або прапорець MSG_MORE, алгоритм утримання буфера має абсолютний контроль. Накопичення даних до межі MSS здійснюється незалежно від того, увімкнено чи вимкнено TCP_NODELAY.
  2. Миттєве вивільнення під час відкриття сокета: Щойно TCP_CORK вимикається або виконується останній виклик send() без MSG_MORE, починає діяти TCP_NODELAY. Оскільки алгоритм Нейгла вимкнено, кінцевий залишковий сегмент негайно вирушає в мережу без очікування підтвердження ACK.
  3. Узгодженість із контролем перевантаження BBR: На відміну від алгоритмів на основі втрат, BBR (Bottleneck Bandwidth and RTT) спирається на точне дозування пакетів у часі (pacing). Поєднання MSG_MORE з алгоритмом BBR гарантує, що пакети завжди мають оптимальний розмір, що значно покращує точність оцінки пропускної здатності каналу.

Практичний посібник: Високопродуктивне кадрування сокетів на C/Go

Побудова оптимального конвеєра кадрування вимагає точного вибору між опціями сокета та прапорцями викликів відповідно до архітектури програми.

5.1 Ручне закриття сокета за допомогою setsockopt

У середовищах на мові C, де логіка запису даних розподілена між різними бібліотечними модулями, централізоване керування сокетом через TCP_CORK забезпечує надійну ізоляцію:

#include <sys/socket.h>
#include <netinet/tcp.h>
#include <unistd.h>

void send_vpn_packet_corked(int sock_fd, const void *hdr, size_t hdr_len,
                           const void *data, size_t data_len) {
    int state_cork = 1;
    int state_uncork = 0;

    // Закриття вихідного сокета для накопичення даних
    setsockopt(sock_fd, IPPROTO_TCP, TCP_CORK, &state_cork, sizeof(state_cork));

    // Послідовний запис заголовка та корисних даних
    write(sock_fd, hdr, hdr_len);
    write(sock_fd, data, data_len);

    // Зняття обмеження для негайного надсилання зібраного сегмента
    setsockopt(sock_fd, IPPROTO_TCP, TCP_CORK, &state_uncork, sizeof(state_uncork));
}

5.2 Агрегація без системних викликів через send() та MSG_MORE

Для подієво-орієнтованих проксі з вимогами до мінімальної затримки заміна setsockopt() на прапорець MSG_MORE забезпечує оптимальну продуктивність процесора:

#include <sys/socket.h>
#include <unistd.h>

ssize_t send_vpn_framed(int sock_fd, const void *hdr, size_t hdr_len,
                        const void *data, size_t data_len) {
    // Етап 1: Надсилання заголовка з утриманням через MSG_MORE
    ssize_t h_bytes = send(sock_fd, hdr, hdr_len, MSG_MORE | MSG_NOSIGNAL);
    if (h_bytes < 0) return -1;

    // Етап 2: Надсилання корисних даних без MSG_MORE для виходу на лінію
    ssize_t d_bytes = send(sock_fd, data, data_len, MSG_NOSIGNAL);
    if (d_bytes < 0) return -1;

    return h_bytes + d_bytes;
}

5.3 Асинхронне векторне введення-виведення з writev() та MSG_MORE

Коли потрібно передати складові пакети (заголовок протоколу, криптографічний підпис та вкладений IP-пакет), доцільно поєднувати векторне введення-виведення з викликом sendmsg():

#include <sys/socket.h>
#include <sys/uio.h>

ssize_t send_vpn_vector_framed(int sock_fd, struct iovec *iov, int iovcnt) {
    struct msghdr msg = {0};
    msg.msg_iov = iov;
    msg.msg_iovlen = iovcnt;

    // Атомарне надсилання всіх векторів за один системний виклик
    return sendmsg(sock_fd, &msg, MSG_NOSIGNAL);
}

5.4 Порівняльне тестування пропускної здатності та частоти пакетів

Під час синтетичного тестування на каналі 10 Гбіт/с із розміром корисного навантаження 1420 байтів отримано такі показники:

Режим конфігурації Пропускна здатність Швидкість пакетів (PPS) Навантаження CPU шлюзу 99-й процентиль затримки
Лише TCP_NODELAY (Без затискача) 4,82 Гбіт/с 840 000 PPS 78% (навантаження softirq) 1,84 мс
TCP_CORK через setsockopt 7,94 Гбіт/с 430 000 PPS 54% (навантаження syscall) 1,12 мс
MSG_MORE без викликів setsockopt 9,42 Гбіт/с 425 000 PPS 31% (оптимально) 0,88 мс

Експерименти свідчать, що усунення мікропакетів за допомогою MSG_MORE зменшує обсяг вихідних пакетів майже вдвічі, знижує використання процесора з 78% до 31% та наближає швидкість передавання до фізичних меж каналу.

Архітектура RiceGuard: Адаптивне кадрування з контролем затримок

У граничній інфраструктурі VPNRice рушій передавання RiceGuard застосовує адаптивну систему кадрування, яка гармонійно поєднує високу пропускну здатність із миттєвим реагуванням на інтерактивні потоки.

Адаптивний рушій передавання RiceGuard:
  ┌─────────────────────────────────────────────────────────────┐
  │ Отримання пакетів (TUN / Конвеєр шифрування)               │
  └──────────────────────────────┬──────────────────────────────┘
                                 │
                   Класифікація мережевого трафіку
                                 │
         ┌───────────────────────┴───────────────────────┐
         ▼                                               ▼
  [Інтерактивний потік]                           [Масовий потік даних]
  (SSH, натискання клавіш, DNS)                   (TCP-завантаження, медіапотоки)
         │                                               │
  Миттєве надсилання через send()                 Агрегація через MSG_MORE
  Нульова затримка в черзі                        Формування повного MSS
  Мікросекундний відгук                           Усунення мікропакетів
  1. Контекстна класифікація трафіку: RiceGuard оцінює тип передаваних даних. Інтерактивні операції (натискання клавіш SSH) та чутливі до затримок запити (DNS) надсилаються негайно без утримання в буфері. Масові потоки даних спрямовуються через модуль агрегації MSG_MORE.
  2. Синхронізація з параметрами маршруту: RiceGuard постійно відстежує стан MTU мережевого шляху (DPLPMTUD). Конвеєр агрегації в режимі реального часу контролює межу MSS і скидає буфер саме тоді, коли розмір даних із заголовками досягає ємності каналу, що запобігає фрагментації пакетів у дорозі.
  3. Вирівнювання пам'яті SKB за рядками кешу процесора: Завдяки формуванню неперервних сегментів через векторні операції та прапорці передавання без системних викликів, RiceGuard знижує навантаження на підсистему виділення пам'яті ядра, забезпечуючи точне вирівнювання структур sk_buff за межами кешу L3 на всіх ядрах сервера.

Завдяки продуманому використанню TCP_CORK та MSG_MORE сучасні мережеві шлюзи повністю уникають зайвої фрагментації, гарантуючи стабільність зв'язку, продуктивність та економне використання ресурсів сервера.