HTTP/1.1, HTTP/2 и HTTP/3: чем отличаются, как работают и как включить в Traefik

На этой неделе я включил HTTP/3 на Traefik в своём кластере, и по дороге пришлось заново разложить в голове, чем три живущие одновременно версии HTTP отличаются друг от друга и зачем вообще нужна третья, если вторая появилась десять лет назад. Это та самая раскладка: как устроена каждая версия, какую проблему предыдущей она решает, когда одна быстрее другой, как всё это настраивается в Traefik и на какие грабли я наступил или успел заметить заранее.

Статья самостоятельная, но опирается на две части серии «Сети для самых маленьких»: про UDP и про TCP. Там подробно про соединения, потери и окна; здесь будем на них ссылаться.


Что остаётся неизменным

HTTP — протокол запросов и ответов: клиент просит ресурс, сервер отвечает статусом, заголовками и телом. Методы (GET, POST), коды (200, 404), заголовки (Host, Content-Type, Cookie), URL — всё это семантика HTTP, и она одна во всех трёх версиях. Приложению, которое работает с запросами на уровне «метод, путь, заголовки, тело», версия безразлична: веб-сервер или прокси принимает запрос по любой из них и отдаёт приложению одно и то же.

Различается как семантика упакована в байты и как эти байты едут по сети. Это и есть вся история: три попытки доставить те же запросы быстрее.


HTTP/1.1: текст поверх TCP

HTTP/1.1 (1997 год, сейчас RFC 9112) — текстовый протокол. Запрос — строки, разделённые \r\n, пустая строка — конец заголовков, дальше тело:

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: curl/8.21.0
Accept: */*

Ответ — так же:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 12345
Cache-Control: max-age=3600

<!doctype html>...

Всё это едет внутри TCP-соединения: надёжного потока байтов, в котором стек сам следит за доставкой и порядком. HTTP/1.1 полагается на это полностью: у него нет ни своих контрольных сумм, ни нумерации, он просто пишет текст в поток и читает из него. Разметка потока — через Content-Length или chunked-кодирование (тело кусками с длиной перед каждым), потому что TCP границ сообщений не хранит.

Что добавила версия 1.1 к совсем первой 1.0:

  • Постоянные соединения. В HTTP/1.0 на каждый запрос открывалось новое TCP-соединение с рукопожатием. В 1.1 соединение по умолчанию живёт дальше (keep-alive), и следующий запрос идёт в то же.
  • Заголовок Host. Чтобы на одном адресе жили сотни сайтов, запрос говорит, к какому из них он.
  • Chunked-передача, диапазоны (Range), кеширование (ETag, Cache-Control), 100 Continue.

Проблема: одна очередь

В одном соединении HTTP/1.1 запросы идут строго по очереди: отправил запрос, дождись ответа, только потом следующий. Спецификация разрешала конвейер (pipelining: отправить несколько запросов, не дожидаясь ответов), но ответы всё равно должны приходить в порядке запросов, промежуточные прокси конвейер ломали, и браузеры его выключили навсегда.

Современная страница — это сотня ресурсов: HTML, десяток скриптов, стили, шрифты, картинки. По одному соединению с RTT 50 мс сто запросов заняли бы пять секунд одними ожиданиями. Поэтому браузеры открывают по шесть соединений на хост и гоняют запросы параллельно. Это помогает, но:

  • шесть соединений — шесть TCP-рукопожатий и шесть TLS-рукопожатий (о TLS ниже), шесть медленных стартов TCP, которые разгоняются независимо и конкурируют друг с другом;
  • большой ресурс в одном соединении блокирует все запросы, которые стояли за ним в этой очереди, — это head-of-line blocking на уровне приложения;
  • заголовки в каждом запросе повторяются текстом целиком: Cookie на килобайт, User-Agent, Accept-* — в каждом из ста запросов.

Поколение фронтенд-разработчиков выросло на обходах этих ограничений: склейка скриптов в один файл, спрайты из картинок, встраивание маленьких ресурсов в HTML, раздача статики с нескольких доменов (domain sharding), чтобы получить больше шести соединений. HTTP/2 сделал эти приёмы ненужными, а некоторые — вредными.

Посмотреть HTTP/1.1 живьём проще всего так:

$ curl -v --http1.1 https://www.example.com/ -o /dev/null
> GET / HTTP/1.1
> Host: www.example.com
> User-Agent: curl/8.21.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: text/html; charset=utf-8
...

HTTP/2: кадры и потоки в одном соединении

HTTP/2 (2015, RFC 9113) вырос из SPDY Google и решает ровно проблемы предыдущего раздела, не меняя семантику: те же методы, заголовки и коды, другая упаковка.

Бинарные кадры

Текст заменён кадрами — бинарными блоками с девятибайтовым заголовком:

 байты:   3         1       1        4
        ┌─────────┬───────┬───────┬─────────────┬──────────────┐
        │ Length  │ Type  │ Flags │ Stream ID   │ полезная     │
        │         │       │       │             │ нагрузка     │
        └─────────┴───────┴───────┴─────────────┴──────────────┘

Типы кадров: HEADERS (заголовки запроса или ответа), DATA (тело), SETTINGS (параметры соединения), WINDOW_UPDATE (управление потоком), RST_STREAM (отменить один запрос), PING, GOAWAY (закрываемся), PRIORITY, PUSH_PROMISE. Парсить бинарный формат проще и быстрее текста, а главное, у каждого кадра есть Stream ID.

Потоки: мультиплексирование

Поток (stream) — один обмен «запрос — ответ» внутри соединения. Клиент открывает потоки с нечётными номерами, сервер с чётными; кадры разных потоков перемешиваются в одном TCP-соединении как угодно:

TCP-соединение ───►  HEADERS(1)  HEADERS(3)  DATA(1)  HEADERS(5)  DATA(3)  DATA(1)  DATA(5) …
                     запрос 1    запрос 3    ответ 1  запрос 5    ответ 3  ответ 1  ответ 5

Сто ресурсов страницы — сто потоков в одном соединении, все запросы отправлены сразу, ответы приходят в том порядке, в котором готовы. Одно рукопожатие, один медленный старт, одно окно перегрузки на всех. Браузеры держат одно соединение на хост (и переиспользуют его для других хостов с тем же сертификатом и адресом — coalescing).

Внутри соединения есть управление потоком на каждый stream и на соединение целиком (WINDOW_UPDATE, как окно TCP, но прикладное): медленный получатель одного большого ответа не останавливает остальные. Есть приоритеты: браузер подсказывает, что CSS важнее картинки, — на практике серверы их почти не учитывают, и в HTTP/3 схему упростили.

HPACK: сжатие заголовков

Заголовки сжимаются алгоритмом HPACK: статическая таблица из 61 самого частого заголовка (:method: GET, :status: 200, accept-encoding: gzip, deflate — это один байт) и динамическая таблица, в которую обе стороны кладут заголовки, увиденные в этом соединении. Второй запрос с той же Cookie на килобайт передаёт её одним индексом. Обычный gzip для заголовков не подошёл: он уязвим к атаке CRIME, в которой атакующий подбирает секрет по размеру сжатого, а HPACK сжимает только повторы целых значений.

Заголовки стали псевдозаголовками в нижнем регистре: :method, :path, :authority (бывший Host), :scheme, :status. Поэтому в curl -v для HTTP/2 вы видите заголовки строчными буквами.

Что ещё

  • Server push — сервер мог отправить ресурс до того, как клиент его попросил. Хорошо выглядело на слайдах, в жизни чаще мешало (клиент уже имел ресурс в кеше), и Chrome убрал поддержку в 2022-м. Замена — 103 Early Hints с подсказками preload.
  • Только TLS в браузерах. Стандарт допускает HTTP/2 открытым текстом (h2c), но браузеры его не реализуют: промежуточные устройства, разбирающие HTTP/1.1, ломали бы незнакомый бинарный протокол. Версия договаривается внутри TLS-рукопожатия расширением ALPN (Application-Layer Protocol Negotiation): клиент перечисляет h2, http/1.1, сервер выбирает. Без ALPN — HTTP/1.1. h2c живёт между прокси и бэкендами, где TLS не нужен, и в gRPC внутри кластеров.

TLS здесь и дальше — протокол шифрования внутри TCP-соединения, на котором работает HTTPS: он защищает данные одного соединения и, что важно для нас, через ALPN сообщает, какая версия HTTP пойдёт внутри.

Проблема, которая осталась: один TCP на всех

HTTP/2 убрал очередь на уровне приложения, но все потоки по-прежнему едут в одном TCP-соединении, а TCP отдаёт данные приложению строго по порядку. Потерялся один сегмент — и пока он не будет повторён, все сто потоков стоят, хотя их данные, возможно, уже пришли и лежат в буфере. Это head-of-line blocking уровнем ниже, и в сети с потерями HTTP/2 в одном соединении может оказаться медленнее HTTP/1.1 в шести: там потеря в одном соединении тормозила только его.

Второе: TCP и TLS — это два отдельных рукопожатия перед первым байтом. TCP — один круг (SYN, SYN-ACK), TLS 1.3 — ещё один. Итого два RTT до первого запроса, на мобильной сети с RTT 100 мс — 200 мс пустоты.

Третье: TCP живёт в ядре. Чтобы изменить алгоритм управления перегрузкой или добавить расширение, нужно обновить ядра на миллиардах устройств и научить промежуточные устройства не ломать новое. За тридцать лет TCP «окостенел» (ossification): любое отклонение от привычного ломается на каком-нибудь файрволе по дороге.

Увидеть HTTP/2:

$ curl -v --http2 https://www.example.com/ -o /dev/null
* ALPN: curl offers h2,http/1.1
* ALPN: server accepted h2
...
> GET / HTTP/2
> Host: www.example.com
...
< HTTP/2 200
< content-type: text/html; charset=utf-8

Кадры и потоки видно в Wireshark, если дать ему ключи TLS через SSLKEYLOGFILE; в DevTools браузера колонка Protocol показывает h2.


HTTP/3: QUIC вместо TCP

HTTP/3 (2022, RFC 9114) — та же семантика и почти та же модель потоков, что в HTTP/2, но вместо TCP+TLS под ним QUIC (RFC 9000). QUIC — транспортный протокол, который делает заново то, что делали TCP и TLS вместе, и делает это поверх UDP.

Почему UDP: UDP ничего не добавляет к IP, кроме портов (девятая часть серии), и его пропускают все NAT и файрволы. Новый номер протокола поверх IP не прошёл бы через половину интернета. Реализация QUIC живёт в пространстве пользователя — в браузере, в curl, в Traefik, — и обновляется вместе с программой, а не с ядром.

Что в QUIC есть:

  • Надёжность и управление перегрузкой — свои, как в TCP: номера пакетов, подтверждения, повторы, окно, те же алгоритмы (CUBIC, BBR). Но номера пакетов никогда не повторяются: повтор потерянных данных идёт в новом пакете с новым номером, и измерение RTT становится однозначным.
  • Потоки на уровне транспорта. Потери обрабатываются по каждому потоку отдельно. Потерялся пакет с данными потока 5 — стоит только поток 5, остальные идут. Head-of-line blocking транспортного уровня исчез. Это главное.
  • TLS 1.3 встроен. Рукопожатие QUIC и рукопожатие TLS — один обмен: один RTT до первого запроса вместо двух, а при повторном подключении — 0-RTT: запрос в первом же пакете. Шифруется почти всё, включая номера пакетов и большинство заголовков QUIC; промежуточным устройствам видны только адреса, порты и идентификатор соединения — и ломать им нечего. Это сознательная защита от окостенения.
  • Идентификатор соединения вместо четвёрки адресов и портов. Телефон перешёл с Wi-Fi на сотовую сеть, адрес сменился — соединение QUIC продолжается с новым адресом (connection migration). В TCP это невозможно по определению: соединение и есть четвёрка адресов.
  • QPACK — сжатие заголовков, как HPACK, но устойчивое к приходу кадров не по порядку: динамическая таблица обновляется отдельным потоком с подтверждениями.

Чего QUIC стоит: шифрование и вся логика транспорта на процессоре приложения, а не в ядре с аппаратными ускорениями. HTTP/3-сервер тратит на тот же трафик в полтора-два раза больше CPU, чем HTTP/2. Для CDN это решённая проблема, для одиночного прокси — расход, о котором надо знать.

Как браузер узнаёт, что можно по HTTP/3

UDP-порт не проверишь «на всякий случай»: это задержка на каждое подключение к серверу без HTTP/3. Поэтому первое подключение всегда идёт по TCP (HTTP/2 или 1.1), и сервер в ответе присылает заголовок:

Alt-Svc: h3=":443"; ma=2592000

«На этом же хосте есть HTTP/3 на UDP-порту 443, помни 30 дней». Следующие соединения браузер открывает по QUIC, и в DevTools протокол меняется на h3. Если UDP не прошёл (файрвол, корпоративная сеть), браузер молча возвращается к TCP и запоминает неудачу: HTTP/3 в принципе не может сломать сайт, он может только не включиться. Это и хорошо, и плохо: «у меня работает» не означает, что работает HTTP/3.

Чтобы обойтись без первого TCP-соединения, есть DNS-запись типа HTTPS (RFC 9460): example.com. HTTPS 1 . alpn="h3,h2" говорит клиенту о поддержке HTTP/3 до первого контакта. Браузеры её запрашивают, но публикуют её пока в основном CDN.

Увидеть HTTP/3

Обычный curl в дистрибутивах собран без HTTP/3; нужна сборка с quiche или ngtcp2, например образ ymuski/curl-http3:

$ docker run --rm ymuski/curl-http3 curl -sv --http3-only https://www.example.com/ -o /dev/null
* QUIC connection established
* using HTTP/3
> GET / HTTP/3
...
< HTTP/3 200

А в tcpdump на сервере это выглядит так:

$ sudo tcpdump -i eth0 -n udp port 443 -c 3
IP 198.51.100.7.52133 > 203.0.113.10.443: UDP, length 1250
IP 203.0.113.10.443 > 198.51.100.7.52133: UDP, length 1250
IP 198.51.100.7.52133 > 203.0.113.10.443: UDP, length 1200

Три UDP-датаграммы и ничего больше: ни флагов, ни номеров, ни заголовков. Для диагностики QUIC нужны либо ключи (SSLKEYLOGFILE и Wireshark), либо логи самого сервера.


Сравнение

HTTP/1.1 HTTP/2 HTTP/3
Транспорт TCP TCP QUIC поверх UDP
Формат текст бинарные кадры бинарные кадры
Соединений на хост в браузере 6 1 1
Запросов в соединении одновременно 1 много (потоки) много (потоки)
Блокировка очереди при потере пакета только это соединение все потоки только этот поток
Сжатие заголовков нет HPACK QPACK
Рукопожатий до первого запроса TCP + TLS: 2 RTT 2 RTT 1 RTT, повторно 0
Шифрование по желанию в браузерах обязательно встроено в транспорт
Переживает смену адреса клиента нет нет да
Видно промежуточным устройствам всё, если без TLS заголовки TCP адреса и порты
Где реализован транспорт ядро ядро приложение
Нагрузка на CPU сервера низкая низкая заметно выше

Когда что быстрее

  • Много мелких ресурсов, нормальная сеть — HTTP/2 и HTTP/3 выигрывают у 1.1 в разы: одно соединение, сто потоков, сжатые заголовки. Между собой на сети без потерь они почти равны.
  • Мобильные и Wi-Fi сети с потерями и большим RTT — HTTP/3 заметно лучше HTTP/2: потеря не останавливает всё, рукопожатие короче, переход между сетями не рвёт соединение. Именно для этого его и делали: по данным Google и Cloudflare выигрыш на «плохих» клиентах — десятки процентов времени загрузки.
  • Один большой файл — разницы почти нет: один поток, всё упирается в управление перегрузкой, а оно одинаковое.
  • Внутри дата-центра, прокси → бэкенд — HTTP/1.1 с keep-alive или h2c; HTTP/3 между своими серверами не нужен: сеть без потерь, RTT доли миллисекунды, а CPU дороже.
  • gRPC — протокол вызова процедур поверх HTTP/2 (потоки в обе стороны, бинарные сообщения); ему нужен именно HTTP/2 до самого бэкенда, HTTP/1.1 не подойдёт.
  • WebSocket — двусторонний канал внутри HTTP-соединения для чатов и живых обновлений; исторически работает поверх HTTP/1.1 через Upgrade, и многие браузеры и прокси по-прежнему открывают для него отдельное HTTP/1.1-соединение даже на сайте с HTTP/2.

В сумме: HTTP/2 — обязательный минимум для любого публичного сайта с 2016 года; HTTP/3 — бесплатное улучшение для мобильных клиентов, если инфраструктура пропускает UDP; HTTP/1.1 — для внутренних соединений и старых клиентов.


Настройка в Traefik

Traefik 3 умеет все три версии, и настраиваются они на уровне entrypoint, а не маршрутов: IngressRoute, Ingress или Gateway API сверху не меняются. Примеры — в значениях Helm-чарта traefik/traefik 41.x (тот же синтаксис работает в статической конфигурации файла или флагами).

HTTP/1.1 и HTTP/2

Включены по умолчанию. На TLS-entrypoint Traefik предлагает в ALPN h2 и http/1.1, и версия выбирается клиентом; на entrypoint без TLS работает HTTP/1.1 и, если клиент попросит, h2c. Регулировать почти нечего:

ports:
  websecure:
    http2:
      maxConcurrentStreams: 250   # по умолчанию 250 потоков на соединение

Между Traefik и бэкендом по умолчанию HTTP/1.1 с keep-alive. Если бэкенду нужен HTTP/2 (gRPC), это задаётся на сервисе: Traefik сам переходит на h2c, когда схема сервиса h2c:

# Kubernetes Service бэкенда с gRPC
metadata:
  annotations:
    traefik.ingress.kubernetes.io/service.serversscheme: h2c

Для Gateway API то же делается аннотацией на сервисе или appProtocol: kubernetes.io/h2c в порте сервиса. Если бэкенд за TLS и говорит по HTTP/2, Traefik договорится с ним по ALPN сам.

HTTP/3

Три условия: TLS на entrypoint (без TLS QUIC не существует), UDP-порт, флаг:

ports:
  websecure:
    http3:
      enabled: true
      advertisedPort: 443

Чарт превращает это в --entryPoints.websecure.http3 и --entryPoints.websecure.http3.advertisedPort=443, добавляет в под порт 8443/UDP рядом с 8443/TCP и в Service — 443/UDP рядом с 443/TCP. advertisedPort нужен, потому что контейнер слушает 8443, а в Alt-Svc должен стоять порт, который видит клиент. Заголовок Traefik добавляет сам ко всем ответам entrypoint.

Без Kubernetes, в статическом файле:

entryPoints:
  websecure:
    address: ":443"
    http3:
      advertisedPort: 443
    http:
      tls: {}

Проверка после выкатки, по порядку:

$ kubectl -n traefik get svc traefik          # должен быть 443/UDP
$ curl -sI https://www.example.com/ | grep -i alt-svc
alt-svc: h3=":443"; ma=2592000
$ docker run --rm ymuski/curl-http3 curl -s --http3-only -o /dev/null -w '%{http_version} %{http_code}\n' https://www.example.com/
3 200

И в браузере: DevTools → Network → колонка Protocol, или в консоли performance.getEntriesByType('navigation')[0].nextHopProtocol — должно быть h3 со второй загрузки. В access-логе Traefik версия пишется в поле RequestProtocol: HTTP/1.1, HTTP/2.0, HTTP/3.0; в метриках Prometheus по версиям разбивки нет, только по entrypoint.


Подводные камни

Собрано из своего опыта и из того, на что наступают другие.

UDP 443 должен пройти весь путь

HTTP/3 — это UDP, и его нужно пропустить везде, где раньше хватало TCP: файрвол перед сервером, проброс портов на роутере, Security Group в облаке, NetworkPolicy в кластере. Забыть один участок — и HTTP/3 просто не включится, молча: браузер откатится на TCP, и вы узнаете об этом только из DevTools. Отдельно про NAT: записи для UDP живут по таймеру (в Linux 30 секунд без ответов, 180 с ответами), QUIC держит их своими PING-пакетами, но на экзотических NAT с коротким таймаутом соединения будут рваться — и тоже молча переоткрываться.

Helm и порты с одним номером

Самое неожиданное, на что я наступил. После helm upgrade с включённым HTTP/3 Traefik перекатился, флаги появились, а UDP-портов ни в Service, ни в поде не было, хотя helm get manifest их показывал. Helm накладывает изменения стратегическим патчем Kubernetes, а в нём порты Service сливаются по полю port, порты контейнера — по containerPort, без учёта протокола. Новая запись 443/UDP слилась с существующей 443/TCP и исчезла. Лечится применением Service и Deployment из манифеста релиза через kubectl apply --server-side --force-conflicts: серверное применение различает порты по паре порт+протокол. При следующих helm upgrade без изменения портов проблема не повторяется, но проверять get svc после апгрейда стоит.

MTU и чёрные дыры

QUIC начинает с пакетов 1200 байт — столько гарантированно проходит где угодно — и дальше сам щупает MTU пути, увеличивая размер и наблюдая, доходят ли большие пакеты. В отличие от TCP, ему не помогает MSS clamping на роутерах (подмена максимального размера сегмента в рукопожатии TCP): у UDP нет MSS. Если на пути туннель с MTU 1400 и файрвол, режущий ICMP, HTTP/3-соединение будет жить, пока QUIC не попробует пакет побольше, а потом подвиснет; HTTP/2 через тот же путь работает благодаря clamping. Симптом: «через VPN сайт открывается медленно и рывками, без VPN нормально». Диагностика — DevTools и сравнение h2/h3; лечение — чинить ICMP на пути или выключить HTTP/3 для таких клиентов нечем, кроме как на сервере целиком.

Буферы UDP в ядре

Реализация QUIC в Traefik — библиотека quic-go, и при старте она просит у ядра большие буферы приёма и отправки UDP-сокета. По умолчанию в Linux net.core.rmem_max и wmem_max — около 200 КБ, quic-go хочет 7 МБ, и при отказе работает с тем, что есть: под нагрузкой пакеты теряются в буфере сокета, и QUIC, считая это перегрузкой сети, снижает скорость. На голом сервере — две строки в sysctl.conf:

net.core.rmem_max = 7500000
net.core.wmem_max = 7500000

В Kubernetes это параметры узла, не пода: ставить на всех узлах, где может оказаться Traefik. Проверить, что нужно: kubectl logs Traefik с уровнем DEBUG покажет предупреждение failed to sufficiently increase receive buffer size, если буферов не хватило.

Балансировка и сессии QUIC

QUIC ориентируется на идентификатор соединения, а балансировщик перед несколькими репликами Traefik — на адреса и порты (ECMP на роутере, externalTrafficPolicy: Local в кластере). Клиент сменил сеть или NAT поменял ему порт — пакеты уедут на другую реплику, у которой этого соединения нет. Она ответит, что соединения не знает, и клиент откроет новое. Для веба это незаметно, для долгих загрузок — обрыв и повтор. Так же с 0-RTT: билет для возобновления выдан одной репликой, другая его не примет и проведёт полное рукопожатие — работает, но без выигрыша. Решение для больших инсталляций — балансировка по connection ID, для нескольких реплик Traefik за ECMP — просто знать.

Alt-Svc живёт 30 дней

ma=2592000 — срок, на который браузер запоминает, что здесь есть HTTP/3. Выключили HTTP/3 на сервере — клиенты ещё месяц будут сначала пробовать UDP и только по таймауту идти по TCP; задержка небольшая, но на каждое новое соединение. Серверы, у которых срок настраивается, перед выключением уменьшают ma до минут; Traefik шлёт фиксированные 30 дней, так что остаётся либо выключить и подождать, либо смириться с месяцем лишних попыток.

HTTP/2: потери и Rapid Reset

Две вещи, про которые стоит помнить и без HTTP/3. Первая — поведение на сети с потерями: один TCP для ста потоков, и каждая потеря тормозит всё; для клиентов на плохом Wi-Fi HTTP/2 может ощущаться хуже HTTP/1.1, и это нормальная причина включить HTTP/3. Вторая — атака Rapid Reset (CVE-2023-44487): клиент открывает потоки и тут же их сбрасывает кадром RST_STREAM, не упираясь в лимит maxConcurrentStreams, и сервер тратит ресурсы на обработку. Защита — в самих реализациях; Traefik закрыл её в 2.10.5 и 3.0, так что лечение одно: не сидеть на старых версиях.

HTTP/1.1: smuggling и keep-alive

Текстовый формат с двумя способами задать длину тела (Content-Length и chunked) породил класс атак request smuggling: прокси и бэкенд читают границы запросов по-разному, и часть одного запроса становится началом следующего. Защита — одна и та же версия разбора на всей цепочке и бинарные протоколы между прокси и бэкендом, где неоднозначности нет. Вторая классика — keep-alive: прокси держит соединение к бэкенду открытым, бэкенд закрывает простаивающие через 5 секунд, прокси отправляет в закрытое соединение запрос и получает ошибку. В Traefik таймаут простоя к бэкендам задаётся в serversTransport (forwardingTimeouts.idleConnTimeout), и он должен быть меньше, чем у бэкенда.

Наблюдаемость

tcpdump показывает для HTTP/3 только UDP-датаграммы, Wireshark без ключей — то же. Статистика по версиям — только из access-лога (RequestProtocol). Если вы привыкли смотреть проблемы в захвате трафика, с HTTP/3 придётся переучиваться на логи сервера и SSLKEYLOGFILE на клиенте.


Что в итоге

  • Три версии — одна семантика и три способа упаковки. HTTP/1.1 — текст и очередь, HTTP/2 — кадры и потоки в одном TCP, HTTP/3 — те же потоки в QUIC, где каждый поток теряет пакеты сам по себе.
  • HTTP/2 убрал очередь приложения и повторяющиеся заголовки; HTTP/3 убрал очередь транспорта и лишний круг рукопожатия, пережил смену сети, но стоит CPU и требует UDP на всём пути.
  • В Traefik первые две версии есть всегда, третья — флаг на TLS-entrypoint, UDP-порт и advertisedPort. Проверять Alt-Svc, curl --http3-only и колонку Protocol в браузере.
  • Грабли: UDP на файрволах и NAT, слияние портов при helm upgrade, MTU в туннелях без помощи clamping, буферы ядра для quic-go, репликация за ECMP, месячная память Alt-Svc. Всё это не ломает сайт, потому что клиент всегда может вернуться на TCP, но делает HTTP/3 «включённым на бумаге».

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.