Одинаковые потери, разный результат: почему видеозвонок терпит, а загрузка файла встаёт
Вы одновременно держите видеозвонок и качаете файл через тот же канал. На звонке проскакивают короткие артефакты — картинка на долю секунды дёргается, голос чуть похрустывает — но разговор продолжается, собеседник вас слышит и понимает. А загрузка файла в этот момент почти встала: индикатор прогресса еле ползёт, iperf или обычный download-менеджер показывают скорость в разы ниже той, что была минуту назад. Потери на канале одни и те же — а результат для двух разных задач противоположный. Дело не в везении и не в том, что «видео проще передавать» — дело в том, что звонок и файл, скорее всего, едут по сети на принципиально разных транспортных протоколах с разной философией отношения к потерянным данным.
Содержание
- Что происходит с пакетом при потере: TCP переспрашивает, UDP — нет
- TCP на передаче файла: гарантия ценой ожидания
- UDP на звонке: пропущенный кадр — рабочая ситуация, а не авария
- Философия двух протоколов: «поздно, но правильно» против «вовремя, пусть и с изъянами»
- Как увидеть разницу своими руками: одна и та же потеря, два транспорта
- Когда общий канал портит оба сценария одновременно
- Что realtime-протоколы добавляют поверх UDP, чтобы терпеть потери осмысленнее
Что происходит с пакетом при потере: TCP переспрашивает, UDP — нет
Разница начинается не в момент потери, а в самом контракте протокола с приложением, заключённом ещё до того, как ушёл первый байт.
TCP превращает данные в непрерывный пронумерованный поток байт и берёт на себя обязательство: каждый байт дойдёт до приложения на другой стороне строго в исходном порядке. Если сегмент потерян, получатель заметит разрыв в последовательности номеров и дождётся повторной отправки. Пока пропавший сегмент не пришёл повторно, все данные, которые физически уже лежат в буфере приёма и пришли позже него, приложению не отдаются — это head-of-line blocking на уровне TCP-потока. С точки зрения программы, читающей из сокета, поток просто останавливается на месте разрыва, даже если 99% данных после разрыва уже физически на месте.
UDP устроен ровно наоборот: он передаёт независимые датаграммы и не хранит про них никакого состояния сверх факта отправки. Ядро отправителя выполнило sendto() — на этом контракт с приложением закончился. Ядро получателя просто никогда не увидит потерянную датаграмму: не будет ни ошибки, ни таймаута — только тишина там, где должен был быть пакет. Соседние датаграммы, которые физически дошли, отдаются приложению немедленно, без ожидания пропавшей. Head-of-line blocking в UDP отсутствует по конструкции — не потому что протокол «умнее», а потому что он просто не берёт на себя обязательство поддерживать целостность потока.
Именно из этой развилки — «останавливаться и ждать» против «отдавать что есть и идти дальше» — вырастает вся разница в поведении звонка и загрузки файла при одинаковом проценте потерь.
TCP на передаче файла: гарантия ценой ожидания
Передача файла — по FTP, SCP, через HTTP-загрузку или rsync — практически всегда идёт поверх TCP, и это осознанный выбор: файл, в котором из-за потери пропущен один сегмент где-то в середине, не «файл с небольшим дефектом», а испорченный файл. Архив не распакуется, бинарник не запустится, документ может открыться с повреждёнными данными. Поэтому TCP-стек скрывает от передающего файл приложения саму возможность частичной потери — либо данные дойдут все и по порядку, либо соединение явно оборвётся с ошибкой.
Плата за эту гарантию — retransmission и ожидание подтверждения. При потере сегмента отправитель обязан отправить его повторно и дождаться ACK, прежде чем эти данные будут считаться доставленными. Это не единственный удар по скорости: TCP интерпретирует саму потерю как сигнал перегрузки сети и резко режет congestion window — окно, определяющее, сколько данных можно держать «в полёте» без подтверждения. Даже одна случайная потеря, никак не связанная с реальной перегрузкой канала, запускает ту же самую тормозящую машинерию. Механика этого эффекта — почему совсем небольшой процент потерь способен срезать скорость передачи в разы, особенно на каналах с большим RTT — подробно разобрана в статье про то, как 0,3% потерь режут скорость TCP вдвое.
На практике это выглядит как рывками ползущий прогресс-бар: полоса то разгоняется до нормальной скорости, то резко проседает почти до нуля на несколько секунд, пока идёт retransmit и восстановление окна. Со стороны отправителя это видно через счётчик ретрансмитов и текущее окно перегрузки:
ss -tin state established '( dport = :22 or dport = :443 )'
# retrans:N/M — сколько сегментов было переотправлено; cwnd — просевшее окно
Здесь нет «терпимой деградации» — есть жёсткий выбор в пользу целостности данных ценой скорости, потому что для файла целостность важнее скорости почти всегда.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверUDP на звонке: пропущенный кадр — рабочая ситуация, а не авария
Голосовые и видеозвонки, большинство VoIP-сервисов и стриминг в реальном времени почти всегда работают поверх UDP — и тоже не случайно. У этого трафика другой критерий успеха: не «доставить каждый байт», а «доставить достаточно данных вовремя, чтобы поток на приёме выглядел непрерывным». Кадр видео или короткий фрагмент аудио, который пришёл на секунду позже своего места в потоке, для собеседника бесполезен так же, как если бы он вообще не пришёл — ждать его нет смысла, разговор к этому моменту уже пошёл дальше.
Поэтому кодек и приложение поверх UDP спроектированы терпеть потери, а не бороться за их полное устранение. При пропуске кадра или фрагмента звука включается packet loss concealment (PLC) — общий принцип, при котором декодер не показывает «дыру», а сглаживает её: для звука это короткая интерполяция на основе соседних фреймов, для видео — использование предыдущего кадра или предсказанного движения вместо пропавшего. Слушатель или зритель в большинстве случаев замечает лишь лёгкий артефакт — щелчок, микроскопический скачок картинки — а не остановку потока.
Номера последовательности в потоке (например, в RTP) здесь используются не для того, чтобы запросить повторную отправку, а чтобы decoder понял сам факт пропуска — а дальше решение всегда «играть дальше», а не «остановиться и подождать». На это накладывается ещё и jitter buffer на приёмной стороне, сглаживающий разброс времени прихода пакетов, — механика и компромиссы буфера подробно разобраны в статье почему джиттер важнее среднего пинга: та же логика «не ждать любой ценой» там работает применительно к задержке, а не только к потерям.
У этой терпимости есть предел: при небольшом проценте случайных потерь качество деградирует плавно и часто малозаметно, а при потерях, которые становятся систематическими или растут, концентрация артефактов увеличивается, и в какой-то момент разговор становится некомфортным — конкретный порог зависит от кодека, размера jitter-буфера и того, идут ли потери пачками или разрознены, поэтому точную цифру здесь честнее не называть.
Философия двух протоколов: «поздно, но правильно» против «вовремя, пусть и с изъянами»
Если свести разницу к одной фразе, получится примерно так: TCP исповедует принцип «лучше поздно, но правильно», UDP в связке с realtime-приложением — «лучше вовремя, пусть даже с изъянами». Это не оценочное суждение о том, какой протокол лучше — это два разных ответа на вопрос «что дороже: задержка или неполнота данных», и правильный ответ полностью зависит от задачи.
| Критерий | TCP (файл, база данных, API) | UDP + realtime-приложение (звонок, стрим, игра) |
|---|---|---|
| Что важнее | Полнота и порядок данных | Своевременность доставки |
| Реакция на потерю | Ожидание, повторная отправка | Пропуск, скрытие артефакта на уровне кодека |
| Цена ошибки в модели | Секунды задержки | Секунды бесполезных данных |
| Что случится при провале стратегии | Передача зависнет, но не испортится | Поток продолжится, но с деградацией качества |
| Типичные сервисы | HTTP-загрузка, SCP/rsync, репликация БД, SSH | VoIP, видеоконференции, live-стриминг, часть игровых протоколов, DNS |
Развернуть эту же мысль немного шире можно и со стороны самого протокола, а не только сценария его применения — что именно UDP «экономит» по сравнению с TCP и какую ответственность эта экономия перекладывает на приложение, разобрано в статье почему UDP «быстрее» TCP и чем именно вы за это платите. Для темы этой статьи важен один вывод оттуда: UDP не «лучше» — он просто не тратит время на гарантии, которые данному конкретному трафику не нужны, и именно поэтому переживает потери спокойнее, чем TCP.
Практическое следствие для инженера: если вы выбираете транспорт для собственного сервиса, вопрос не «TCP или UDP быстрее», а «что дороже потерять — секунду времени или часть данных в моменте». Телеметрия, которую можно ужать и досчитать позже, — кандидат на UDP. Финансовая транзакция или файл конфигурации, где пропуск байта ломает всё, — однозначно TCP, и никакая скорость UDP не стоит риска.
Как увидеть разницу своими руками: одна и та же потеря, два транспорта
Разница не абстракция — её легко воспроизвести на паре тестовых серверов, если под рукой есть tc netem и iperf3. Схема простая: вносим одинаковую управляемую потерю на интерфейсе и одновременно гоняем TCP-поток (имитирует передачу файла) и UDP-поток (имитирует звонок), сравнивая, что происходит с каждым.
Вносим потерю на исходящем интерфейсе, затем поочерёдно гоняем TCP- и UDP-тест через одну и ту же потерю:
tc qdisc add dev eth0 root netem loss 2%
iperf3 -c 10.0.0.2 -t 20 # аналог загрузки файла (TCP)
iperf3 -u -c 10.0.0.2 -t 20 -b 1M # аналог звонка (UDP, фиксированный битрейт)
TCP-тест покажет просевшую пропускную способность, причём просадка окажется непропорционально больше самих 2% — это тот самый эффект congestion control из предыдущего раздела. UDP-тест в отчёте выдаст Lost/Total Datagrams в районе заданной доли потерь и небольшой Jitter — но сам поток не остановится ни на секунду, просто часть датаграмм не долетит. Это ровно то, что вы наблюдаете на живом звонке и параллельной загрузке: не «сеть ведёт себя по-разному», а два протокола по-разному реагируют на одну и ту же сеть. После тестов верните интерфейс в исходное состояние:
tc qdisc del dev eth0 root netem
Когда общий канал портит оба сценария одновременно
Стоит сделать честную оговорку: описанная выше разница объясняет реакцию на потери конкретных пакетов, но это не единственный механизм, из-за которого звонок может страдать одновременно с загрузкой файла. Если оба потока идут через один перегруженный аплинк, качество звонка может испортиться не из-за собственных потерь, а из-за задержки в разбухшем буфере маршрутизатора — тяжёлая TCP-загрузка заполняет очередь, и голосовые пакеты, которые физически не теряются, стоят в ней за большими TCP-сегментами и приходят с опозданием и скачущим джиттером. Это отдельное явление, bufferbloat, — оно бьёт по обеим сторонам разговора, даже если сеть не теряет ни одного пакета. Механизм и способы его смягчить (приоритизация очередей, AQM вроде fq_codel) разобраны в статье почему скачивание файла убивает параллельный звонок.
Отличить одно от другого просто: если звонок деградирует именно тогда, когда на канале реальные потери (проверяется через mtr) — вы наблюдаете эффект из этой статьи. Если потерь нет, а звонок всё равно рассыпается во время параллельной загрузки — это, скорее всего, bufferbloat, и решение там не про транспортный протокол, а про управление очередями.
Что realtime-протоколы добавляют поверх UDP, чтобы терпеть потери осмысленнее
Голая UDP-датаграмма без какой-либо страховки — не единственный вариант для realtime-трафика. Многие современные протоколы для видеоконференций и стриминга строят поверх UDP собственный, облегчённый слой частичной надёжности — не полный TCP-контракт «доставить всё и по порядку», а компромиссные механизмы, рассчитанные именно на реальное время.
Общая идея нескольких таких механизмов, без привязки к конкретной реализации как единственно верной:
- Forward error correction (FEC) — вместе с полезными данными в поток добавляется избыточная информация, по которой декодер может восстановить один-два потерянных пакета без запроса повторной отправки. Цена — дополнительная полоса на избыточность, которая тратится всегда, даже если потерь в моменте нет.
- Выборочный retransmit для ключевых кадров. Потеря опорного (ключевого) кадра видео портит декодирование следующих кадров, построенных на его основе, а потеря промежуточного — почти незаметна. Часть протоколов избирательно запрашивает повторную отправку именно ключевых фрагментов, оставляя остальное на волю случая, — частичная надёжность вместо полной.
- Адаптивный битрейт и адаптивный jitter buffer. При росте потерь или джиттера кодек и буфер подстраиваются в реальном времени: снижают битрейт, увеличивают буфер, жертвуя задержкой ради стабильности, — тот же компромисс, что и выше, только применённый динамически.
Отдельно стоит упомянуть QUIC — транспорт, на котором построен HTTP/3: пример прямо противоположного решения. Он тоже работает поверх UDP, но достраивает не частичную, а практически полную TCP-подобную надёжность в userspace, а не в ядре ОС — чтобы развивать протокол быстрее, чем меняется ядерный TCP-стек, а не для того, чтобы терпеть потери. Вывод для практики: сам факт «поверх UDP» ещё ничего не говорит о том, как протокол относится к потерям — решает конкретная реализация, и у видеозвонка, стрима и QUIC-соединения это три разных ответа на один и тот же вопрос.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Один и тот же процент потерь, но звонок всё равно рассыпается — значит, дело не в потерях?
Не обязательно, но стоит проверить два момента: систематические ли это потери (растут, идут пачками) и не смешан ли эффект с bufferbloat — задержкой в переполненном буфере, а не реальной потерей пакета. Первое видно по продолжительному замеру mtr, второе — по сравнению поведения при остановленной параллельной загрузке.
Можно ли передавать файл по UDP и получить те же преимущества, что у звонка?
Технически да, но тогда логику надёжности — обнаружение потери, повторную отправку, контроль порядка — придётся реализовать в приложении самому, то есть фактически заново собрать часть функциональности TCP. Оправдано в узких случаях (специализированные протоколы для нестабильных каналов), но для обычной загрузки файла TCP почти всегда практичнее.
Почему тогда просто не сделать TCP умнее, чтобы он не тормозил так сильно при потерях?
Отчасти это уже происходит — алгоритмы вроде BBR оценивают состояние канала не только по факту потери и реагируют мягче. Но базовый контракт TCP — гарантированная доставка и порядок — никуда не девается, и он структурно несовместим с идеей «пропустить и жить дальше».
Звонок и загрузка файла делят один канал — что реально помогает?
Два рычага: приоритизация трафика (QoS/tc, чтобы голосовые пакеты не стояли в очереди за файлом) и ограничение скорости фоновой загрузки на время звонка. Ни один не меняет философию протоколов — они просто не дают одному потоку мешать другому на общем узком месте.
UDP-стриминг с FEC полностью решает проблему потерь?
Нет, FEC снижает чувствительность к единичным и редким потерям, но не отменяет их — при устойчиво высоком проценте избыточности перестаёт хватать, и деградация всё равно наступает, просто порог сдвигается выше.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →