Почему UDP «быстрее» TCP и чем именно вы за это платите
«UDP быстрее TCP» — фраза, которую повторяют так часто, что она стала восприниматься как физический закон, а не инженерный компромисс. На самом деле UDP не быстрее в смысле пропускной способности провода — он просто меньше делает. Не устанавливает соединение, не подтверждает получение, не следит за порядком и не притормаживает при перегрузке сети. Вся эта «экономия» и создаёт ощущение скорости, но каждая выброшенная функция — это ответственность, которая никуда не делась, а переехала в ваше приложение. Разберём, что именно вы получаете и что теряете, если решите гонять трафик по UDP на своём сервере.
Содержание
- Что на самом деле экономит UDP на пути пакета
- Три вещи, которые TCP делает за вас, а UDP — нет
- Потерянный пакет в UDP — это тишина
- Порядок пакетов: пришли, но не по порядку
- Почему UDP не притормаживает и чем это опасно для сети
- Как приложения достраивают надёжность поверх UDP
- Когда UDP на сервере оправдан, а когда это преждевременная оптимизация
Что на самом деле экономит UDP на пути пакета
Разница начинается ещё до первого байта данных. TCP-соединение открывается тройным рукопожатием: SYN, SYN-ACK, ACK — минимум один полный round-trip до того, как уйдёт хоть один бит полезной нагрузки. У UDP рукопожатия нет вообще: приложение вызвало sendto() — пакет ушёл. Если вам нужно отправить один короткий запрос и получить один короткий ответ (классический пример — DNS), экономия на установлении соединения ощутима именно потому, что полезной нагрузки там на один пакет, а накладные расходы TCP-хендшейка сопоставимы по времени с самим запросом.
Дальше — заголовок. Минимальный заголовок UDP — 8 байт: порт отправителя, порт получателя, длина, контрольная сумма. У TCP заголовок начинается с 20 байт и почти всегда обрастает опциями (window scaling, SACK, timestamps) до 32+ байт. На гигабитных потоках с крупными пакетами разница в заголовках не решает ничего, но на потоках с мелкими пакетами — телеметрия, голос, игровые обновления позиций — соотношение заголовка к полезной нагрузке становится существенным.
И главное — TCP-стек ядра держит на каждое соединение связанное состояние: буферы приёма и отправки, окно перегрузки, таймеры повторной передачи, очередь неподтверждённых сегментов. UDP-сокет в ядре — это, по сути, только буфер приёма и минимальная бухгалтерия. Меньше состояния — меньше работы на пакет — меньше задержка на обработку. Вот откуда берётся ощущение «UDP быстрее»: это не пакет летит быстрее по проводу, это сервер и клиент тратят меньше времени на бюрократию вокруг него.
# Сравнить активные TCP- и UDP-сокеты на сервере
ss -tn state established | wc -l
ss -un | wc -l
Три вещи, которые TCP делает за вас, а UDP — нет
Если свести все различия к сути, TCP берёт на себя три обязательства, и UDP не даёт ни одного из них.
Гарантированная доставка. TCP нумерует каждый байт потока и ждёт подтверждения (ACK) от получателя. Не пришло подтверждение вовремя — сегмент отправляется повторно. Приложение, работающее поверх TCP, может считать, что если write() не вернул ошибку, данные рано или поздно дойдут (или соединение явно порвётся с ошибкой). У UDP такого контракта нет: sendto() вернулся успешно — это значит только «ядро приняло пакет и попыталось его отправить», а не «получатель его увидел».
Гарантированный порядок. TCP пересобирает поток байт в исходном порядке на стороне получателя, даже если сегменты пришли вразнобой — на этом построена вся модель «сокет как непрерывный поток». UDP отдаёт приложению датаграммы в том порядке, в котором они физически прибыли на интерфейс, и если один пакет пошёл по другому маршруту и задержался, все датаграммы после него в очереди сокета всё равно будут выданы раньше него, как только он придёт — либо приложение увидит их в неверном порядке, если само не следит за нумерацией.
Контроль перегрузки (congestion control). Это, пожалуй, самое недооценённое отличие. TCP постоянно оценивает состояние сети через окно перегрузки: начинает осторожно (см. отдельный разбор медленного старта TCP), наращивает скорость, пока не увидит признаки перегрузки (потери, рост задержки), и тут же снижает темп. Это не альтруизм — это то, что удерживает интернет от коллапса, когда миллионы соединений делят одни и те же каналы. У UDP этого механизма нет в принципе: сокет отправляет данные с той скоростью, с которой их отдаёт приложение, и ядро не вмешивается, чтобы притормозить отправителя из-за перегрузки где-то на маршруте.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПотерянный пакет в UDP — это тишина
Вот здесь начинается самая практическая часть разговора. В TCP потеря пакета — рабочая ситуация, которую стек обрабатывает сам: не пришёл ACK в течение расчётного времени (RTO) — сегмент уходит повторно, при повторных потерях RTO растёт, соединение возвращается к более консервативному темпу через тот же механизм slow start. Приложение в лучшем случае видит небольшую задержку, но не теряет данные.
В UDP при потере пакета не происходит вообще ничего. Ядро отправителя не знает, что пакет потерян — оно свою часть работы выполнило, как только пакет ушёл на интерфейс. Ядро получателя тоже не знает — оно просто никогда не увидит этот пакет. Никакого сигнала, никакого события, никакой автоматической повторной отправки. Если приложению важно, что данные дошли, обнаруживать пропажу и решать, что с ней делать, обязано само приложение — и это касается всего: от простого «не пришёл ответ — повторим DNS-запрос через таймаут» до полноценных протоколов подтверждения с номерами пакетов и окнами.
Практическая проверка на сервере — посчитать, сколько датаграмм ядро вообще смогло принять и сколько было отброшено из-за переполнения буфера приёма (это тоже потеря, просто локальная, а не сетевая):
# Ошибки и отброшенные UDP-датаграммы на уровне ядра
cat /proc/net/snmp | grep -A1 '^Udp:'
# InErrors — датаграммы, которые ядро приняло на интерфейс,
# но не смогло доставить в буфер сокета (обычно переполнение)
# Если приложение читает медленнее, чем приходят данные —
# растёт очередь и растут потери именно здесь, а не в сети
ss -u -a -n -m
Отдельная тема — потери, которые не видны стандартными инструментами диагностики; про это подробно в статье про потерю пакетов, которую не видно в ping: ICMP echo и реальный UDP-трафик приложения идут по разным очередям QoS на промежуточных узлах, и то, что ping «зелёный», ничего не говорит о потерях в вашем UDP-потоке.
Порядок пакетов: пришли, но не по порядку
Второе следствие отсутствия гарантий — датаграммы могут прийти не в том порядке, в котором ушли. Причина обычно в маршрутизации: у крупных провайдеров пакеты одного потока не всегда идут строго по одному и тому же физическому пути (балансировка нагрузки между линками, ECMP-маршрутизация), и если один пакет попал на путь с чуть большей задержкой, он может прийти позже пакета, отправленного следом.
Для TCP это невидимо — пересборка потока в порядке байт встроена в протокол. Для UDP это видимая приложению проблема. Если вы читаете статистику через RTP (голос, видео) или собственный бинарный протокол (игровой сервер, телеметрия), без явного номера пакета в теле датаграммы вы не отличите «пакет пришёл не по порядку» от «пакет потерян». Именно поэтому все серьёзные протоколы поверх UDP несут sequence number прямо в полезной нагрузке — это не опция, а обязательное условие, чтобы вообще понять, что произошло на приёме. Проверить, как ваш протокол ведёт себя при переупорядочивании, можно локально через tc qdisc add dev eth0 root netem reorder 25% 50% — правило искусственно перемешивает часть пакетов перед отправкой.
Почему UDP не притормаживает и чем это опасно для сети
Это отличие спорят реже всего, а зря — именно оно способно превратить «быстрый» UDP-сервис в проблему для всех вокруг. TCP-отправитель постоянно снижает скорость при признаках перегрузки — это распределённый механизм самоограничения, на котором держится справедливое разделение пропускной способности между множеством соединений, идущих через один и тот же узкий канал. UDP-отправитель ничего подобного не делает: если приложение продолжает вызывать sendto() с той же скоростью, сокет продолжает эту скорость поддерживать вне зависимости от того, что происходит на маршруте — растут задержки, начинаются потери у соседних потоков или нет.
На практике это означает две вещи. Во-первых, если на промежуточном канале начинается перегрузка, TCP-потоки на нём вежливо снижают скорость, а неограниченный UDP-поток — нет, и в результате забирает себе непропорционально большую долю канала за счёт соседей. Во-вторых, если вы сами пишете сервис поверх UDP и гоните данные с постоянной или растущей скоростью без обратной связи от получателя, вы рискуете забить собственный аплинк или канал получателя настолько, что начнёте терять собственные же пакеты — без единого сигнала от ядра о том, что происходит.
Отсюда практическое правило: любой UDP-протокол с заметным объёмом трафика (стриминг, синхронизация, массовая телеметрия) обязан реализовать собственный, пусть упрощённый, механизм контроля темпа — иначе он либо захлебнётся сам, либо помешает соседям по каналу.
Как приложения достраивают надёжность поверх UDP
Раз ядро ничего не гарантирует, всю недостающую логику разработчики протоколов пишут в userspace — и это не редкость, а стандартная практика для целого класса сервисов.
QUIC (транспорт, на котором построен HTTP/3) — самый показательный пример: TCP-подобная надёжность и контроль перегрузки, реализованные поверх UDP на уровне приложения, а не в ядре. Решение осознанное: TCP-стек в ядрах ОС меняется годами, а протокол на UDP можно обновлять вместе с приложением. Плюс QUIC избавлен от head-of-line blocking на уровне транспорта, которым страдает несколько параллельных потоков в одном TCP-соединении. Практика настройки — в статье про HTTP/3 и QUIC на сервере.
RTP и голосовые/видеопротоколы идут другим путём: вместо повторной отправки потерянных пакетов (в реальном времени это бессмысленно — пакет придёт слишком поздно) они несут sequence number и timestamp, чтобы приёмник мог восстановить порядок, обнаружить пропажу и решить, что делать — заполнить тишиной, интерполировать кадр или запросить ключевой кадр заново. Джиттер-буфер на приёме сглаживает неравномерность прихода пакетов ценой небольшой задержки; подробнее — в статье про джиттер и VPN для голосовой связи и игр.
Игровой netcode обычно комбинирует оба подхода: позиция игрока не критична к потере — следующий пакет её всё равно обновит, поэтому дешевле отправить лишний раз, чем гарантировать доставку; событие вроде «игрок выстрелил» критично, и для него поверх того же UDP-сокета реализуют собственный слой подтверждений с ретраями.
Общий паттерн один: если вам нужна надёжность, вы не получаете её бесплатно, выбрав UDP «потому что он быстрее» — вы платите за неё либо временем разработки (свой протокол подтверждений), либо готовой библиотекой, которая эту работу уже сделала (реализация QUIC, RTP-стек).
Когда UDP на сервере оправдан, а когда это преждевременная оптимизация
Выбор транспорта — это не вопрос вкуса, а вопрос того, что критичнее для конкретного сервиса: доставка каждого байта или минимальная задержка здесь и сейчас.
| Сценарий | Разумный выбор | Почему |
|---|---|---|
| DNS-запросы | UDP (фолбэк на TCP для больших ответов) | Короткий запрос-ответ, повтор дешевле, чем держать соединение |
| API, вебхуки, обычный HTTP-трафик | TCP / HTTP(S) или QUIC | Нужна гарантия доставки каждого запроса |
| Голос, видеозвонки | UDP + RTP-подобный протокол | Устаревший пакет бесполезен, важнее низкая задержка |
| Онлайн-игры (позиции, состояние мира) | UDP + свой протокол с приоритетами | Частота обновлений важнее гарантии на пакет |
| Загрузка файлов, бэкапы, репликация БД | TCP | Каждый байт обязан дойти, порядок критичен |
| Стриминг видео (VOD, live) | TCP или QUIC/HTTP-3 | Буфер на клиенте компенсирует задержку, важна целостность |
| Телеметрия с высокой частотой | UDP, если потеря точек допустима | Следующая точка всё равно придёт через секунду |
| VPN-туннели (WireGuard и аналоги) | UDP на транспорте, TCP внутри | Транспорт не должен дублировать контроль перегрузки |
Главная ошибка — выбирать UDP «для скорости» там, где на самом деле нужна TCP-гарантия, а разработчик просто не хочет разбираться, почему TCP-соединение медленнее, чем ожидалось. Чаще всего реальная причина — не сам протокол, а конкретный настраиваемый параметр: слишком консервативный медленный старт на короткой сессии, маленькое окно приёма, алгоритм Нейгла. Переход на UDP тут не решает проблему, а меняет её на другую — вы теряете гарантии доставки, чтобы обойти настройку, которую можно было поправить одной командой sysctl.
Обратная ошибка — тащить TCP туда, где критична задержка последнего пакета, а не сохранность всех предыдущих: голосовой чат поверх TCP будет заикаться сильнее, чем поверх UDP с потерями, потому что TCP честно попытается доставить устаревший пакет прежде, чем отдаст приложению следующий — в реальном времени это хуже, чем просто пропустить кадр.
# Лимиты буферов UDP — частая причина "необъяснимых" потерь при высокой частоте пакетов
sysctl net.core.rmem_max net.core.rmem_default
sysctl -w net.core.rmem_max=26214400
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Если UDP ничего не гарантирует, зачем вообще WireGuard и другие VPN построены на нём, а не на TCP?
Внутри VPN-туннеля обычно и так идёт TCP-трафик приложений со своим контролем перегрузки. Обернуть TCP в TCP (TCP-over-TCP) — известный антипаттерн: при потере пакета внешний и внутренний TCP начинают конкурировать за повторную отправку, задержки растут лавинообразно. UDP как транспорт туннеля просто передаёт байты, а логика надёжности остаётся на уровне внутреннего трафика.
Можно ли сделать UDP надёжным, просто добавив свои ACK, и получить TCP, но быстрее?
Технически да — именно так устроен QUIC, но это означает реализовать заново значительную часть того, что уже есть в TCP: нумерацию, окна, таймеры повторной передачи. Выигрыш появляется не от «магии UDP», а от контроля над деталями протокола под конкретную задачу. Наивный ACK без окна и контроля перегрузки легко даёт протокол хуже TCP, а не быстрее.
Почему при высокой нагрузке UDP-сервер начинает терять пакеты, хотя CPU и сеть выглядят свободными?
Чаще всего дело в буфере приёма сокета: приложение читает данные медленнее, чем ядро их принимает, буфер переполняется, лишние датаграммы отбрасываются молча — это видно в счётчике InErrors из /proc/net/snmp. Решение — увеличить net.core.rmem_max и проверить, не блокируется ли поток обработки на диске или в БД.
Стоит ли использовать UDP для собственного API вместо привычного HTTP, чтобы ускорить сервис?
Почти всегда нет. Экономия на рукопожатии заметна только при очень большом количестве коротких независимых запросов (как в DNS), а стоимость реализации надёжности поверх UDP обычно превышает выигрыш. Если задержка HTTP(S) критична — разумнее сначала попробовать HTTP/3 поверх QUIC, там контроль перегрузки уже написан за вас.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →