awesome-everything EN
↑ Обратно к восхождению

Сети и протоколы

Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединения

Суть HTTP/3 поверх QUIC устраняет HoL-блокировку и выживает при смене IP; WebSocket даёт постоянный двунаправленный канал, но отказывает при backpressure. Оба требуют явного проектирования отказов.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Мобильный пользователь в поезде сообщает, что сайт перезагружается каждый раз, когда поезд проезжает тоннель. Тем временем дашборд в реальном времени на основе WebSocket тихо теряет сообщения, когда один клиент медленно их потребляет. У обеих проблем одна первопричина: транспорт был разработан для стабильных соединений, но реальность нестабильна. HTTP/3 и WebSocket справляются с этим по-разному — и оба отказывают способами, которые нужно явно проектировать.

Путь A: HTTP/3 поверх QUIC

Почему QUIC вместо TCP. TCP мультиплексирует потоки через одно соединение, но один потерянный пакет останавливает весь TCP receive window до повторной передачи. У HTTP/2 поверх TCP именно эта проблема: один медленный поток блокирует все остальные потоки того же соединения. QUIC реализует потоки как независимые сущности — потерянный пакет в потоке A не блокирует потоки B, C или D. Эта независимость потоков — главное преимущество в сетях с потерями (мобильные, межконтинентальные линки).

Объединённое рукопожатие. TCP требует отдельного трёхстороннего handshake (1 RTT) до начала TLS (ещё минимум 1 RTT). QUIC их объединяет: Initial-пакет QUIC несёт TLS ClientHello. Сервер отвечает одним пакетом, содержащим транспортные параметры QUIC и TLS ServerHello. Итого: 1 RTT для нового соединения вместо 2.

0-RTT возобновление. Для тёплых соединений (пользователь возвращается к тому же origin в пределах срока действия session ticket), клиент отправляет данные приложения внутри Initial-пакета QUIC — до того как сервер ответил. Эффективный RTT для первого байта: 0 дополнительных round-trips. Ограничение: 0-RTT безопасен только для идемпотентных методов (GET, HEAD). Запросы POST/PUT должны ждать завершения handshake (сервер отклоняет 0-RTT для неидемпотентных запросов с 425 Too Early).

Максимальный объём ранних данных 0-RTT. Серверы ограничивают количество байт ранних данных (обычно: 16–64 КБ). Если HTTP-запрос + заголовки превышают это, остаток отправляется после handshake. Это ограничение предотвращает использование злоумышленниками 0-RTT для внедрения большой нагрузки до завершения аутентификации.

ХарактеристикаHTTP/2 + TCPHTTP/3 + QUIC
Стоимость нового соединения2 RTT (TCP + TLS)1 RTT (объединённый)
Тёплое соединение (возобновление)1 RTT (TLS PSK)0 RTT (0-RTT early data)
Влияние потери пакетаОстанавливает все потоки (HoL block)Затрагивает только потерянный поток
Смена IPСоединение разрываетсяМигрирует через Connection ID
Управление перегрузкойЯдро (не настраивается по потоку)User-space (настраивается, BBR2, PCC)
Совместимость с middleboxОтличнаяНекоторые firewall блокируют UDP

Миграция соединения (мобильные сети)

Когда телефон Бэа переключается с WiFi на мобильный интернет, её IP-адрес меняется. При TCP+HTTP/2 TCP-четвёрка (src IP, src port, dst IP, dst port) является частью идентификатора соединения — смена IP разрывает соединение. Бэа должна заново установить TCP + TLS + HTTP, заплатив полную стоимость handshake.

При QUIC соединения идентифицируются Destination Connection ID — непрозрачным токеном, выданным Свеном при handshake, выбранным Свеном, присутствующим в каждом заголовке пакета QUIC. Ядро Бэа отправляет фрейм PATH_CHALLENGE с нового IP. Свен отвечает PATH_RESPONSE с тем же Connection ID, подтверждая, что смена IP легитимна (а не злоумышленник подделывает исходный IP Бэа). Все потоки в полёте продолжаются по новому пути. Миграция стоит один дополнительный round-trip (~50 мс в хорошей сети), но позволяет избежать повторного установления всего соединения.

Граничные случаи

Миграция соединения QUIC работает только если балансировщик нагрузки сервера маршрутизирует пакеты с одним и тем же Connection ID к одному и тому же backend-процессу. Это ограничение при развёртывании: stateless балансировщики нагрузки, маршрутизирующие по source IP, нарушат миграцию QUIC. Решение: маршрутизировать QUIC по Connection ID (Nginx QUIC, Caddy, Cloudflare поддерживают это нативно).

Путь B: WebSocket upgrade для коммуникации в реальном времени

HTTP работает по схеме запрос-ответ — один запрос, один ответ. Для двунаправленной коммуникации в реальном времени (чат, совместное редактирование, live-дашборды) клиенту нужно получать сообщения от сервера без нового запроса для каждого. WebSocket решает это, обновляя HTTP-соединение до постоянного полнодуплексного канала.

Upgrade handshake. Клиент отправляет:

GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <base64 random 16 bytes>

Сервер отвечает:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <HMAC of key + magic string>

После 101 TCP-соединение перепрофилируется как WebSocket. Обе стороны обмениваются фреймами асинхронно — без порядка запрос-ответ, без заголовков на каждое сообщение.

Структура фрейма. Каждый WebSocket-фрейм несёт:

  • FIN бит (1 = финальный фрагмент, 0 = следуют ещё фрагменты)
  • Opcode (1=текст, 2=двоичный, 8=закрытие, 9=ping, 10=pong)
  • Маскирование (клиент→сервер обязательно, XOR с 4-байтовым маскирующим ключом — защита от отравления кэша)
  • Длина нагрузки + нагрузка

Большие сообщения фрагментируются по нескольким фреймам (FIN=0 для фрагментов, FIN=1 для финального). Получатель буферизирует до FIN=1 перед доставкой приложению.

Keepalive через ping/pong. У WebSocket нет встроенного keepalive на уровне TCP. Примерно через 75 с без фрейма многие сетевые устройства таймаутируют простаивающие TCP-соединения. Решение: отправлять ping-фреймы каждые 15–30 с. Получатель отвечает pong. Если pong не получен в течение таймаута, соединение мертво и должно быть переустановлено.

Отказ из-за backpressure. Если приложение Бэа медленно потребляет сообщения (например, тяжёлая JavaScript-обработка каждого сообщения), send buffer Свена заполняется. Когда Свен вызывает write() на сокете, ядро возвращает WOULDBLOCK. Наивный серверный код, закрученный на WOULDBLOCK, повышает CPU; наивный код, теряющий сообщения, теряет данные. Правильный паттерн: Свен прекращает читать от Бэа (приостанавливает входящий поток) до опустошения исходящего буфера. Правильные библиотеки WebSocket реализуют это через async/await и API backpressure потоков.

Проследи
1/5

Проследите handshake QUIC + HTTP/3 и сравните с TCP + HTTP/2.

1
Step 1 of 5
Новое соединение, HTTP/3. Что клиент отправляет первым?
2
Locked
Сервер отвечает. Что содержится в первом пакете сервера?
3
Locked
Клиент получил ключи. Может ли он теперь отправить HTTP-запрос?
4
Locked
Потеря пакета в потоке 3 (CSS-файл). Что происходит с потоками 1 и 2 (HTML + JS)?
5
Locked
Телефон пользователя переключается с WiFi на LTE посреди загрузки. Что происходит?
Проследи
1/5

Проследите отказ из-за WebSocket backpressure и правильное решение.

1
Step 1 of 5
Свен рассылает 1 000 сообщений/с 5 000 подключённым WebSocket-клиентам. Один клиент (Бэа) медленный — обработка каждого сообщения занимает 10 мс. Что происходит?
2
Locked
Наивный сервер: spin loop на WOULDBLOCK. Что происходит?
3
Locked
Второй наивный подход: терять сообщения при заполнении буфера. Что происходит?
4
Locked
Правильный подход: распространение backpressure.
5
Locked
Если Бэа никогда не догоняет (офлайн, всегда медленная). Какая стратегия таймаута?
Викторина

Независимость потоков — главное преимущество QUIC над HTTP/2+TCP. Какой конкретный отказ она предотвращает?

Викторина

Почему маскирование WebSocket-фреймов клиент→сервер обязательно, а сервер→клиент — нет?

Вспомните перед уходом
  1. 01
    Миграция соединения QUIC: что такое Destination Connection ID и почему он переживает смену IP?
  2. 02
    Что происходит, если балансировщик нагрузки маршрутизирует QUIC-пакеты по source IP вместо Connection ID при миграции?
  3. 03
    Объясните, как WebSocket backpressure распространяется с уровня приложения вниз до flow control TCP.
Итог

HTTP/3 поверх QUIC решает два ограничения TCP: head-of-line blocking (потерянный пакет больше не останавливает все потоки) и нестабильность при смене IP (Connection ID QUIC переживает переключения WiFi→мобильный через PATH_CHALLENGE/RESPONSE). Объединённый handshake QUIC + TLS стоит 1 RTT для новых соединений и 0 RTT для тёплых возобновлений, но 0-RTT небезопасен для неидемпотентных запросов из-за риска повтора. WebSocket обновляет HTTP-соединение до постоянного полнодуплексного канала с помощью opcode-фреймов и маскирования; его основной отказ — коллапс backpressure при медленном потребителе, заполняющем send buffer — правильное решение — распространение backpressure с уровня приложения до flow control TCP, а не потеря сообщений или spin на WOULDBLOCK.

Связанные уроки
встречается в258
Продолжить восхождение ↑Наблюдаемость: распределённые трейсы, USE/RED и семплирование
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.