У сучасних мережевих архітектурах мінімізація затримок передавання є настільки ж важливою, як і максимальна пропускна здатність каналу. Корпоративні VPN-шлюзи, резервні тунелі WireGuard поверх TCP і вузли термінації TLS регулярно обробляють багатогігабітний трафік, забезпечуючи миттєвий відгук для інтерактивних пакетів. Проте зі зростанням швидкості фізичних каналів від 1 Гбіт/с до 100 Гбіт/с поширилася прихована проблема: роздування буферів (bufferbloat) у чергах запису мережевого стека ядра Linux.
Коли застосунок передає дані через з'єднання TCP, ядро Linux автоматично масштабує обсяг буфера відправлення (sk->sk_sndbuf). На далекомагістральних лініях із великим добутком пропускної здатності на затримку (BDP) ядро нерідко збільшує цей буфер до 4 МБ, 8 МБ або навіть 16 МБ на кожен сокет. Хоча такий обсяг пам'яті дозволяє повністю навантажити канал, він породжує структурне звуження: програми користувацького простору завантажують мегабайти даних у системні черги набагато швидше, ніж фізична мережа здатна їх відправити.
Таке надмірне накопичення даних створює невидимі застійні черги в оперативній пам'яті ядра. Якщо пріоритетний пакет—наприклад, натискання клавіші в терміналі, аудіодані голосового зв'язку або повідомлення ротації ключів безпеки—має пройти крізь тунель, він опиняється заблокованим позаду мегабайтів файлових архівів, що вже стоять у черзі. Для усунення цієї штучної затримки та відновлення контролю над потоком даних ядро Linux пропонує параметр сокета TCP_NOTSENT_LOWAT.
Вузьке місце черги запису: Зростання BDP і роздування буферів у ядрі
Головна причина роздування буферів під час запису полягає у відсутності прямого зв'язку між циклами обробки подій у просторі користувача та скінченними автоматами TCP ядра. Асинхронний сервіс (на базі epoll або io_uring) відправляє дані щоразу, коли сокет сповіщає про наявність вільного місця в буфері (EPOLLOUT):
- Розрахунок буфера в ядрі: Ядро призначає
sk_sndbuf = 2 * BDP, резервуючи до 16 МБ для запобігання обмеженню вікна перевантаження. - Агресивний запис застосунку: Оскільки ядро підтверджує готовність до запису, доки обсяг даних менший за
sk_sndbuf, застосунок миттєво переміщує свої буфери в системну чергу сокета. - Накопичення застійних пакетів: Дані, які не можуть бути негайно відправлені в кабель (через контроль темпу pacing або обмеження віддаленого вікна), осідають у черзі запису (
sk_write_queue).
Черга запису стандартного сокета без TCP_NOTSENT_LOWAT:
[ Цикл подій застосунку ] ──► write(16 МБ)
│
▼
[ Буфер сокета в ядрі: sk_sndbuf (Виділено 16 МБ) ]
┌─────────────────────────────────────────────────────────────┬─────────────┐
│ Невідправлені дані в черзі ядра (15,5 МБ очікування) │ Дані │
│ (Створюють блокування початку черги для термінових пакетів) │ в дорозі │
└─────────────────────────────────────────────────────────────┴─────────────┘
│
▼ (Поступове відправлення зі швидкістю каналу)
[ Фізичний мережевий адаптер ]
Накопичення застійних черг у ядрі спричиняє дві суттєві небезпеки:
- Блокування початку черги (Head-of-Line Blocking): Програма втрачає можливість скасувати, змінити пріоритет або передати позачерговий пакет, оскільки байти вже зафіксовані в ядрі.
- Масштабне перевитрачання пам'яті: Якщо граничний сервер підтримує 20 000 паралельних сесій, кожна з яких утримує по 8 МБ невідправлених даних у структурах
sk_buff, система витрачає до 160 ГБ пам'яті лише на утримання пасивних черг.
Внутрішня будова TCP_NOTSENT_LOWAT: Механіка ядра та події сокетів
Запропонований розробником ядра Еріком Дюмазе параметр TCP_NOTSENT_LOWAT змінює логіку сигналізації про готовність сокета до запису. Замість перевірки вільного місця в усьому буфері sk_sndbuf, ядро відстежує точний обсяг невідправлених байтів (unsent bytes), що зараз перебувають у черзі.
Ядро обчислює невідправлені дані як різницю між загальною кількістю записаних байтів і кількістю байтів, що вже передані в лінію зв'язку:
$$\text{Невідправлені байти} = \text{write_seq} - \text{snd_nxt}$$
Якщо для TCP_NOTSENT_LOWAT встановлено певне порогове значення (наприклад, 16 КБ або 32 КБ), ядро затримує генерацію події EPOLLOUT, доки обсяг очікуючих даних перевищує це обмеження.
Регульований конвеєр відправлення за умови TCP_NOTSENT_LOWAT = 16 КБ:
[ Буфери застосунку ] ── (Зберігаються в пам'яті процесу для визначення черги)
│
│ 1. Цикл подій чекає на сигнал EPOLLOUT
▼
[ Черга сокета в ядрі: sk_write_queue ]
┌──────────────────────────────────────────────┬────────────────────────────┐
│ Невідправлений залишок: Не більше 16 КБ │ Активні дані в дорозі │
│ (Своєчасне передавання пакетів у ядро) │ (Канал BDP зайнятий повністю)│
└──────────────────────────────────────────────┴────────────────────────────┘
│
▼ (Негайне надсилання в мережевий адаптер)
[ Фізичний мережевий адаптер ]
1. Розділення місткості буфера сокета та обсягу невідправлених даних
Важливо зазначити, що TCP_NOTSENT_LOWAT не зменшує розмір sk_sndbuf і не звужує вікно перевантаження TCP (snd_cwnd). Мережевий стек зберігає спроможність утримувати потужний потік даних на міжконтинентальних маршрутах із високим BDP.
Механізм натомість відокремлює місткість вікна передавання від надлишкового попереднього накопичення байтів застосунком. Ядро продовжує стежити за пакетами підтвердження, проте користувацький простір надсилає нові фрагменти лише в міру звільнення черги.
2. Сигналізація EPOLLOUT за подіями та регулювання потоку даних
У мережевій підсистемі ядра Linux (net/ipv4/tcp_output.c) внутрішня функція tcp_write_xmit() контролює розмір черги:
/* Перевірка всередині tcp_stream_memory_free */
static inline bool tcp_stream_memory_free(const struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);
u32 notsent = tp->write_seq - tp->snd_nxt;
return notsent < tp->notsent_lowat;
}
Щойно мережева плата передає заплановані пакети та просуває покажчик snd_nxt уперед, кількість невідправлених даних зменшується. Коли значення notsent стає меншим за tp->notsent_lowat, ядро активує чергу очікування сокета і повідомляє epoll_wait() про готовність приймати нову порцію байтів.
Баланс затримки та пам'яті: Глобальний sysctl проти налаштування окремих сокетів
Адміністратори можуть активувати обмеження черги запису системно для всього сервера або гнучко задавати параметри для окремих сокетів.
Ієрархія конфігурації для регулювання черг запису TCP:
[ Глобальний параметр Sysctl ]
net.ipv4.tcp_notsent_lowat = 16384 (Базове значення для всієї системи)
│
├─► Автоматично діє для стандартних TCP-сокетів
│
└─► Перевизначається точково через setsockopt():
setsockopt(fd, SOL_TCP, TCP_NOTSENT_LOWAT, &val, sizeof(val))
1. Загальносистемне налаштування через net.ipv4.tcp_notsent_lowat
Найбільш простий метод усунути роздування буферів на сервері полягає у зміні системного параметра sysctl:
# Перевірка поточного значення (зазвичай 0xFFFFFFFF, тобто функція вимкнена)
cat /proc/sys/net/ipv4/tcp_notsent_lowat
# Встановлення ліміту 16 КБ глобально для всіх TCP-з'єднань
sudo sysctl -w "net.ipv4.tcp_notsent_lowat = 16384"
Команда tcp_notsent_lowat = 16384 фіксує межу в 16 КБ для невідправлених даних усіх з'єднань, унеможливлюючи безконтрольне заповнення пам'яті пасивними чергами.
2. Точкове керування сокетами в багатокористувацьких шлюзах
У навантажених шлюзах програмне встановлення лімітів для окремих сокетів забезпечує максимальну гнучкість. Демон може призначити малі ліміти (наприклад, 4 КБ або 8 КБ) для інтерактивних потоків, залишаючи 32 КБ або 64 КБ для масивного мультимедійного трафіку:
int notsent_limit = 16384; /* Ліміт невідправлених даних 16 КБ */
if (setsockopt(client_fd, SOL_TCP, TCP_NOTSENT_LOWAT, ¬sent_limit, sizeof(notsent_limit)) < 0) {
perror("Помилка встановлення TCP_NOTSENT_LOWAT");
return -1;
}
Практичний регламент: Впровадження TCP_NOTSENT_LOWAT у високошвидкісні демони
Адаптація мережевого сервісу до роботи з TCP_NOTSENT_LOWAT вимагає узгодженого керування пам'яттю:
- Збереження даних у пам'яті процесу: Відмова від надмірного попереднього запису. Дані мають перебувати в локальних структурах процесу (кільцевих буферах або сегментованих чергах).
- Чітке дотримання сигналу EPOLLOUT: Отримавши повідомлення
EPOLLOUT, процес записує лише частину даних до наступної межі й повертає керування циклу подій. - Врахування граничного спрацьовування (Edge-Triggered): У режимі
EPOLLETзапис триває в циклі, доки викликwrite()не поверне помилкуEAGAINчиEWOULDBLOCK, що свідчить про досягнення встановленої межі.
Схема циклу подій запису сокета з низькою затримкою:
[ Готові нові дані ] ──► Перевірка внутрішньої черги пріоритетів
│
▼
Сокет готовий до запису (EPOLLOUT)?
/ \
(Так) (Ні)
/ \
▼ ▼
Запис порції даних у сокет Очікування події epoll
│
▼
Функція повернула EAGAIN?
/ \
(Так) (Ні)
/ \
▼ ▼
Скидання статусу Продовження запису в циклі
Переваги мультиплексування: Усунення блокування початку черги
Для протоколів, що поєднують кілька логічних потоків в одному фізичному з'єднанні TCP (HTTP/2, gRPC або інкапсульовані тунелі VPN), параметр TCP_NOTSENT_LOWAT вирішує фундаментальне завдання: повністю усуває взаємне блокування пакетів між простором користувача та ядром.
Розглянемо корпоративний тунель, де водночас передається резервна копія файлів і вводяться команди в терміналі:
- Без TCP_NOTSENT_LOWAT: Файли миттєво займають 8 МБ буфера в ядрі. Натискання клавіші потрапляє в кінець черги на 8 МБ, отримуючи 200 мс непотрібної затримки.
- З TCP_NOTSENT_LOWAT: У ядрі постійно перебуває не більше 16 КБ файлових даних. Щойно надходить термінальний пакет, планувальник негайно розміщує його на початку черги застосунку. За лічені мікросекунди, щойно відправляється блок на 16 КБ, терміновий пакет виходить у мережу без перешкод.
Архітектура RiceGuard: Адаптивне керування буферами на максимальній швидкості
VPNRice впроваджує технологію TCP_NOTSENT_LOWAT безпосередньо в конвеєр передавання даних RiceGuard. Для мобільних клієнтів, корпоративних каналів зв'язку та потокових трансляцій через резервні тунелі TCP ліквідація роздування буферів є ключовим чинником стабільної роботи.
Сервіс RiceGuard постійно аналізує параметри з'єднання—зокрема коливання RTT, ритм відправлення пакетів і вимоги клієнтів. Система автоматично підлаштовує значення TCP_NOTSENT_LOWAT у межах від 8 КБ до 32 КБ, гарантуючи повне навантаження лінії передавання за субмілісекундних затримок черги. Гармонійне поєднання функцій ядра Linux з алгоритмами чергування пакетів забезпечує високу швидкість відгуку, надійну стабільність і надійний захист цифрової конфіденційності для користувачів у всьому світі.



