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 «включённым на бумаге».
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.