Проект ЦИТадель
HTTP/2 стандартизован в 2015 году, HTTP/3 — в 2022-м. Обе версии сохранили знакомую семантику HTTP: методы, статусы и поля. Менялось прежде всего представление обмена и его связь с транспортом. Одна из главных причин этих изменений — блокировка начала очереди (head-of-line blocking): независимая работа задерживается из-за более ранней. В HTTP/1.1 эта проблема проявлялась в очереди ответов, HTTP/2 устранил её на уровне HTTP, но оставил на уровне TCP, а QUIC перенёс независимые потоки в транспорт. Это важная, хотя и не единственная линия эволюции: новые версии также меняли сжатие полей, приоритеты, установление соединения и его поведение при смене сети.
У сообщения HTTP/1.1 текстовые начальная строка и поля, а тело может содержать произвольные байты. Соединение TCP можно использовать повторно, но без конвейеризации следующий запрос обычно отправляют после ответа на предыдущий. Стандарт разрешает pipelining — несколько запросов без ожидания, — однако ответы всё равно должны идти в порядке запросов. Один медленный ответ поэтому задерживает готовые ответы за ним, а неодинаковая поддержка посредниками и серверами мешала надёжно применять конвейеризацию в браузерах.
Историческим обходом стали несколько параллельных TCP-соединений к одному источнику — часто до шести в браузере. Это создавало несколько независимых очередей, но каждое соединение отдельно проходило установление, управление перегрузкой и занимало ресурсы клиента и сервера. Сайты также распределяли ресурсы по поддоменам и объединяли их в спрайты или крупные пакеты. После появления мультиплексирования эти приёмы перестали быть безусловными оптимизациями: дополнительный домен требует отдельного разрешения и соединения, а чрезмерный пакет ухудшает кэширование и доставляет неиспользуемый код. Объединение всё ещё может быть полезно, но его проверяют измерением, а не применяют по привычке.
HTTP/2 вырос из SPDY; действующая спецификация — RFC 9113. Он оставил семантику HTTP прежней и добавил бинарный слой кадров:
preload и ответ 103 Early Hints.Спецификация допускает HTTP/2 без шифрования (h2c), но обычные веб-браузеры используют HTTP/2 для сайтов поверх TLS. Клиент и сервер выбирают идентификатор h2 через ALPN в рукопожатии, разобранном в статье о TLS.
HTTP/2 разделяет запросы логически, но TCP предоставляет приложению один упорядоченный поток байтов. Если сегмент потерян, уже пришедшие более поздние байты не выдаются приложению до заполнения разрыва. Поэтому потеря перед кадрами одного HTTP/2-потока задерживает также последующие кадры других потоков. TCP может продолжать приём и быстро повторить потерянные данные, однако блокировка доставки приложению остаётся.
У HTTP/1.1 несколько соединений имели независимые потоки TCP, поэтому потеря в одном не останавливала остальные. В HTTP/2 обычно используется одно соединение, и на пути с потерями общая TCP-очередь иногда уменьшает или обращает выигрыш от мультиплексирования. Результат зависит от RTT, характера потерь, алгоритма перегрузки, числа и размера ресурсов; одного процента потерь недостаточно, чтобы заранее назвать победителя. Но причина возможного ухудшения находится ниже HTTP, поэтому исправлять её потребовалось в транспорте.
Развёртыванию нового транспорта мешает окостенение сети: межсетевые экраны, NAT и другие посредники могут отбрасывать незнакомые IP-протоколы или предполагать конкретное устройство TCP. QUIC (RFC 9000) передаёт пакеты в UDP-датаграммах, что позволяет разворачивать его в пользовательском пространстве и проходить через большее число существующих устройств, чем с новым номером IP-протокола. UDP всё же блокируется в части сетей, поэтому запасной путь необходим.
HTTP/3 (RFC 9114) отображает семантику RFC 9110 на QUIC. Каждому запросу и его ответу соответствует один двунаправленный поток, инициированный клиентом; отдельные однонаправленные потоки несут управление соединением и состояние сжатия. Сам QUIC отвечает за мультиплексирование и упорядочение внутри каждого потока.
HTTP/2 не может без изменений перенести HPACK на независимую доставку: декодирование очередного блока может зависеть от обновления общей таблицы, которое ещё не пришло в другом потоке. QPACK выносит обновления таблицы в отдельные однонаправленные потоки и позволяет кодировщику ограничивать число блоков, способных ждать это состояние. Так реализация выбирает компромисс между степенью сжатия и риском блокировки.
Клиент может узнать об HTTP/3 из заголовка Alt-Svc, полученного по HTTP/1.1 или HTTP/2, и запомнить альтернативу. DNS-запись HTTPS с параметром ALPN позволяет сообщить поддержку до первого HTTP-соединения. Эти механизмы не гарантируют использование HTTP/3: клиент учитывает свой кэш, поддержку, доступность UDP и результат попытки соединения.
Задание 1. Инвентаризация. В инструментах разработчика браузера включите колонку протокола и перезагрузите несколько сайтов с отключённым кэшем. Для h3 найдите Alt-Svc или HTTPS-запись DNS и различите «сервер объявил h3» и «запрос действительно ушёл по h3». В терминале проверьте возможности клиента командой curl --version, затем сравните curl -sv --http2 и, если сборка поддерживает QUIC, curl -sv --http3-only.
Задание 2. Сервер с h2 и h3. Для современного nginx нужны собранные модули HTTP/2 и HTTP/3, сертификат и совместимая TLS-библиотека. Минимальный фрагмент внутри одного блока server:
listen 443 ssl; # TCP: HTTP/1.1 и HTTP/2 http2 on; listen 443 quic reuseport; # UDP: HTTP/3 add_header Alt-Svc 'h3=":443"; ma=86400' always;
Проверьте конфигурацию штатной командой nginx до перезагрузки. Подтвердите h2 и h3 отдельными запросами и по журналу сервера. Первый визит может начаться сразу с h3 при подходящей DNS-записи или сохранённой альтернативе, поэтому не используйте номер посещения как доказательство. Заблокировав UDP/443 только в лабораторной среде, сравните curl --http3-only, который должен завершиться ошибкой, с curl --http3 или браузером: при рабочем TCP-пути они должны продолжить по нему. Запишите задержку перехода.
Задание 3. Подготовка измерения. Создайте страницу со ста небольшими ресурсами одинакового размера и запретите их кэширование. Используйте один и тот же клиент, одну версию сервера и один маршрут. Для h2 и h3 убедитесь по трассировке или журналу, что запросы мультиплексируются в одном соединении; цикл из ста отдельных процессов curl этого не проверяет. Для HTTP/1.1 ограничьте параллелизм шестью соединениями. Сделайте не менее десяти повторов каждого варианта и записывайте медиану и высокий перцентиль, а не один лучший результат.
Задание 4. Потери и задержка. На отдельном стенде последовательно задайте 0%, 0,5%, 1% и 3% потерь при фиксированной задержке, например:
$ tc qdisc replace dev eth0 root netem delay 40ms loss 1% $ tc qdisc del dev eth0 root
Имя интерфейса замените фактическим и обязательно удалите правило после опыта. Для каждого режима измерьте HTTP/1.1, h2 и h3, постройте график «потери → полное время» и сохраните число повторных передач. Гипотеза состоит в том, что под потерями h3 меньше задерживает независимые ресурсы, чем одно h2-соединение. Результат может отличаться из-за алгоритмов перегрузки, размещения кадров в пакетах и реализации; это повод разобрать трассу, а не объявить опыт неудачным.
Задание 5*. Внутри QUIC. Экспортируйте сеансовые ключи способом из практикума TLS либо включите qlog в поддерживающем клиенте или сервере. Найдите потерянный пакет и определите, какие потоки содержали его кадры. Проверьте одновременно два эффекта: другие потоки продолжают доставку без ожидания этих байтов, но окно перегрузки соединения реагирует на потерю.
Задание 6*. Миграция. Начните достаточно долгую передачу по h3 и смените доступный сетевой путь. По qlog и журналу сервера установите, сохранился ли идентификатор соединения, прошла ли проверка нового пути и продолжился ли тот же поток. Затем повторите по h2. Если клиент создал новое QUIC-соединение, проверьте ограничения реализации и политики: сам факт поддержки HTTP/3 ещё не доказывает поддержку миграции в выбранном сценарии.