HTTP — это язык, на котором браузер разговаривает с сервером, а сервисы между собой. Сам смысл запросов почти не менялся десятилетиями: GET /orders/42, заголовки, тело, код ответа. А вот то, как эти запросы упаковываются и бегут по сети, за это время переписали трижды. Так появились HTTP/1.1, HTTP/2 и HTTP/3 — три версии одного протокола, каждая быстрее предыдущей.
Хорошая новость: ваш код почти всегда остаётся прежним. Разбираться в версиях нужно не чтобы переписывать логику, а чтобы понимать, почему одна страница грузится рывками, а другая плавно, и что за галочку «HTTP/2» вы включаете на балансировщике. Пойдём по порядку — от самого старого к самому новому.
Проще всего разница видна на одной шкале времени: пока буксует один ответ, важно, что в это время происходит с остальными.
Числа условные, важно, кто кого ждёт. Один потерянный пакет большого ответа A задерживает маленький B по-разному: в HTTP/1.1 B ещё даже не начинался — очередь в соединении; в HTTP/2 кадры B уже доехали, но TCP отдаёт байты строго по порядку и держит их до переотправки; в HTTP/3 поток B не ждёт ничего — QUIC знает про потоки и тормозит только пострадавший.
HTTP/1.1: просто, но по очереди
HTTP/1.1 — версия, на которой интернет прожил бо́льшую часть своей истории. Она текстовая: запрос — это буквально строки, которые можно прочитать глазами. Открыли соединение, послали GET /page, получили ответ. Удобно для отладки, легко для понимания.
Раннее неудобство HTTP/1.0 — на каждый запрос новое соединение — здесь уже решено механизмом keep-alive: соединение остаётся открытым, и по нему можно послать несколько запросов подряд, не переустанавливая связь каждый раз. Это экономит время: установка соединения сама по себе не бесплатна (подробнее — в статье про соединения).
Но есть фундаментальное ограничение: ответы по одному соединению приходят строго по очереди. Если первый ответ большой или сервер над ним задумался, все остальные ждут за ним, даже если давно готовы. Это называется head-of-line blocking — «блокировка головой очереди»: один медленный элемент впереди держит всех, кто за ним, как одна застрявшая машина держит целую полосу.
Строго говоря, слать запросы не дожидаясь ответа HTTP/1.1 умеет — это называется конвейеризацией (pipelining). Легче от неё, правда, не становится: отвечать сервер всё равно обязан в том же порядке, в каком его спросили, так что застрявший первый ответ по-прежнему держит остальные. Вдобавок конвейер регулярно ломали кривые посредники на пути, и браузеры в итоге выключили его у себя совсем. Так что на практике всё и выглядит как «послали — дождались — послали следующий».
Как с этим жили? Браузеры открывали к одному сайту несколько параллельных соединений — обычно шесть. Пока по одному качается картинка, по другому летит стиль, по третьему скрипт. Костыль работает, но дорогой: каждое соединение — это отдельная установка связи, отдельная память на сервере, отдельные накладные расходы. Шесть — потолок, а на тяжёлой странице ресурсов сотни.
HTTP/2: много потоков в одной трубе
HTTP/2 родился именно чтобы убрать эту очередь. Главная идея — мультиплексирование: по одному-единственному соединению одновременно едет много независимых запросов и ответов, не мешая друг другу.
Чтобы это стало возможным, протокол сделали бинарным. Вместо текстовых строк данные режутся на маленькие пронумерованные кусочки — кадры (frames). Каждый кадр помечен, к какому потоку он относится. Сервер и клиент отправляют кадры вперемешку, а на другой стороне собирают их обратно по номерам. Аналогия — не колонна машин на однополосной дороге, а посылки на общей ленте конвейера: едут вперемешку, но у каждой свой адрес, и на выходе их раскладывают по получателям.
В одном соединении кадры разных потоков идут вперемешку, а номер в начале кадра говорит, к какому потоку его отнести на приёме.
Что это даёт на практике:
- Одно соединение вместо шести. Меньше установок связи, меньше нагрузки на сервер — а параллелизм при этом выше, чем был на шести соединениях.
- Сжатие заголовков. У каждого запроса ворох повторяющихся заголовков (одни и те же cookie, user-agent, host). HTTP/2 их сжимает и не гоняет одно и то же по сто раз — заметная экономия там, где запросов много.
- Server push — сервер мог сам, не дожидаясь запроса, дослать клиенту то, что тому наверняка понадобится (например, стиль к странице). На практике идея не прижилась: сервер не знал, что у клиента уже лежит в кэше, и слал лишнее; Chrome убрал поддержку push в 2022 году, в версии 106, а nginx в 2023-м. В описаниях HTTP/2 она всё ещё встречается.
Важно: сам смысл запросов не изменился. Те же методы, заголовки, коды ответов — поменялась только «упаковка». Поэтому переход на HTTP/2 обычно не требует трогать код сервиса.
Что осталось нерешённым в HTTP/2
HTTP/2 убрал очередь на уровне самого HTTP. Но он по-прежнему живёт поверх TCP — транспорта, который гарантирует, что байты придут целыми и строго по порядку (разбор TCP — в статье про TCP и UDP).
И вот тут прячется ловушка. TCP отдаёт данные наверх строго по порядку. Если в пути потерялся один пакет, TCP останавливает выдачу всего, что пришло после него, и ждёт, пока потерянный кусок доедет заново. А в этом «всём, что после» лежат кадры разных потоков. Получается: логически потоки независимы, но один потерянный пакет тормозит их все — потому что все они делят одно TCP-соединение.
Это снова head-of-line blocking, только спустившийся на уровень TCP. HTTP/2 честно убрал блокировку у себя, но не мог достать до транспорта под собой. На хорошей сети разница незаметна, а вот на мобильной связи с потерями пакетов HTTP/2 иногда работал не лучше, чем несколько отдельных соединений HTTP/1.1.
Три ограничения, из-за которых «одно соединение» не всегда благо.
Потолок одновременных потоков. Сервер объявляет предел (обычно сто), и запросы сверх него ждут в очереди на стороне клиента. Для браузера сто — много, для сервиса, который шлёт тысячу параллельных запросов в один адрес, — уже предел: дальше начинается ожидание, которое выглядит как медленный сервис при незагруженной сети.
Управление потоком. У каждого потока и у соединения есть окно — сколько данных можно отправить без подтверждения. Окно по умолчанию невелико (64 КБ), и на больших ответах поток упирается в него, пока получатель не подтвердит прочитанное. На быстрых каналах с большой задержкой это ограничивает скорость сильнее, чем полоса; настраивается на сервере и в клиенте.
Одно соединение прибивает к одному узлу. Это главное и самое неожиданное. Балансировщик распределяет соединения, а не запросы; при HTTP/1.1 клиент с пулом из двадцати соединений раскладывается по всем узлам, а при HTTP/2 у него одно соединение — и все запросы идут на один узел, выбранный при подключении. Отсюда картина: узлов десять, нагрузка на одном, задержка выросла, пул «выродился» до одного соединения.
Лечится это тремя способами. Балансировщик уровня приложения, который умеет распределять запросы внутри соединения (так делает прокси, понимающий HTTP/2). Или клиент, которому явно велено держать несколько соединений к одному адресу. Или обнаружение сервисов на стороне клиента: клиент сам знает список узлов и открывает соединение к каждому — так работает балансировка в gRPC.
Когда новая версия не нужна
HTTP/3 дороже по процессору (шифрование и сборка потоков делаются в пользовательском коде, а не ядром) и проходит не везде: сети, которые режут UDP, оставят вас без соединения — поэтому клиенты всегда умеют откатиться на HTTP/2. Выигрыш он даёт там, где есть потери пакетов и большая задержка: мобильный интернет, дальние регионы. Внутри дата-центра, где потерь почти нет, он не даёт ничего.
HTTP/2 между своими сервисами тоже часто бесполезен: запросы там идут по постоянным соединениям из пула, размер ответов небольшой, потерь нет — то есть ни мультиплексирование, ни сжатие заголовков не спасают от того, чего нет. Зато появляется описанная выше привязка к одному узлу. Практическое правило: внутри — HTTP/1.1 с пулом или gRPC (который берёт HTTP/2 осознанно, вместе со своей балансировкой), снаружи — HTTP/2 и HTTP/3 на прокси.
HTTP/3: сменить фундамент под протоколом
Чтобы убрать блокировку окончательно, нужно было заменить сам транспорт. Так появился HTTP/3 — тот же бинарный мультиплексируемый HTTP, но поверх QUIC вместо TCP.
QUIC — это транспорт, построенный на UDP (быстром протоколе «отправил и забыл», без гарантии порядка) с досыпанной поверх надёжностью. Ключевое отличие: QUIC знает про потоки. Если теряется пакет одного потока, страдает только этот поток — остальные едут дальше, их никто не держит. Тот самый TCP-level head-of-line blocking исчезает, потому что TCP под протоколом больше нет.
Бонусом QUIC быстрее устанавливает соединение. В TCP сначала идёт рукопожатие транспорта, потом отдельно рукопожатие шифрования — два круга задержки. QUIC складывает их в одно: шифрование встроено в него изначально, и связь поднимается за меньшее число обменов. На мобильной сети, где каждый круг до сервера ощутим, это заметно ускоряет первый ответ.
Цена — QUIC ходит по UDP, а некоторые старые сети и межсетевые экраны относятся к UDP настороженно. Поэтому HTTP/3 обычно работает как ускорение поверх: клиент и сервер умеют HTTP/2, а при возможности переключаются на HTTP/3, откатываясь обратно, если QUIC не проходит.
Переключение это не магия, у него есть конкретное место. Самый ходовой способ — заголовок Alt-Svc в ответе: сервер отвечает как обычно, по HTTP/2, и заодно приписывает «меня же можно найти по HTTP/3 вот на этом порту»; клиент это запоминает и следующее соединение пробует уже через QUIC. Второй способ — запись HTTPS в DNS: там то же самое написано заранее, и клиент узнаёт про HTTP/3 ещё до первого соединения, не тратя лишний круг. Так что когда HTTP/3 «почему-то не включается», искать надо именно их: отдаёт ли балансировщик Alt-Svc и заведена ли запись в DNS.
Где это применяется
Для backend-сервиса версия HTTP — это не абстракция, а вполне конкретные точки, где она всплывает.
Первое и главное — gRPC работает поверх HTTP/2. Мультиплексирование и бинарные кадры для gRPC не бонус, а требование: множество параллельных вызовов и потоковая передача держатся именно на этом. Если вы поднимаете gRPC-сервис, HTTP/2 у вас уже включён, хотите вы того или нет.
Второе — где включается версия. Обычно не в коде приложения. Наружу смотрит прокси или балансировщик (nginx, Envoy, облачный load balancer), и HTTP/2 или HTTP/3 терминируется на нём: браузер общается с балансировщиком по HTTP/2, а тот дальше до вашего сервиса может идти хоть по обычному HTTP/1.1 — на коротком быстром участке внутри дата-центра разница уже не так важна. Где именно рвётся и пересобирается соединение — тема статьи про балансировщики. Ещё одна деталь: браузеры включают HTTP/2 только поверх шифрования, так что на практике он идёт в паре с HTTPS и TLS. Согласуют версию прямо внутри рукопожатия TLS — для этого там есть отдельное поле, ALPN (Application-Layer Protocol Negotiation): клиент в первом же сообщении перечисляет, что понимает (h2, http/1.1), сервер выбирает одно из списка. Отдельного обмена на это не тратится вовсе. Тот самый «тумблер HTTP/2» на балансировщике — это, как правило, ровно список ALPN в его настройках.
Где спотыкаются начинающие:
- Думают, что HTTP/2 меняет смысл запросов. Нет. Методы, заголовки, коды ответов — те же, что в HTTP. Меняется только упаковка на проводе, а ваш обработчик запроса остаётся прежним.
- Считают, что HTTP/2 полностью убил head-of-line blocking. Он убрал его на уровне HTTP, но на уровне TCP при потере пакета блокировка остаётся — окончательно её снимает только HTTP/3 поверх QUIC.
- Пытаются «включить HTTP/3 в приложении». Обычно версия живёт на прокси/балансировщике, а не в коде сервиса. Искать переключатель нужно там.
- Путают версию протокола с шифрованием. HTTP/2 и TLS — разные вещи, просто браузеры требуют их вместе. Версия отвечает за то, как гоняются данные, TLS — за то, что их не прочитать по дороге.
Глубже: gRPC: потоки, коды ошибок и дедлайнырасширенное
Дважды выше сказано, что gRPC работает поверх HTTP/2, и это объясняет, почему он быстрый, но не объясняет, чем он отличается для того, кто его вызывает. Три вещи.
Контракт и потоки. Метод и типы описаны в файле .proto, из него генерируется клиент и сервер на любом языке, и тело идёт бинарным Protobuf, который меньше и быстрее JSON. Каждый вызов это отдельный поток HTTP/2, поэтому gRPC умеет четыре формы: обычный вызов «запрос и ответ», серверный поток (клиент спросил, сервер шлёт ответы по мере готовности), клиентский поток (загрузка частями) и двусторонний поток, где обе стороны шлют сообщения независимо. Именно последняя форма отличает gRPC от REST по существу: чат, подписка на изменения, обмен телеметрией без WebSocket и опроса.
Коды ошибок. HTTP-статус у ответа gRPC почти всегда 200, а результат лежит в конце ответа, в заголовке-трейлере grpc-status. Кодов шестнадцать, и они про смысл, а не про транспорт: NOT_FOUND, INVALID_ARGUMENT, ALREADY_EXISTS, PERMISSION_DENIED, UNAVAILABLE (повторять можно), DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED (аналог 429). Из этого следуют две грабли. Балансировщик и прокси, которые смотрят на HTTP-статус, считают все ответы успешными и не видят ошибок, поэтому им нужно понимать gRPC отдельно. И код UNAVAILABLE это единственный, который повторяют по умолчанию, а повтор UNKNOWN или INTERNAL может продублировать операцию.
Дедлайны. В gRPC срок задаёт клиент при каждом вызове, и он передаётся по цепочке: сервис A вызвал B с дедлайном две секунды, B вызывает C и передаёт остаток. Это тот самый бюджет таймаутов из статьи про надёжность, только встроенный в протокол: заголовок grpc-timeout идёт с запросом, а сервер по истечении получает отменённый контекст и обязан прекратить работу. Клиент без дедлайна ждёт вечно, и это первое, что проверяют в новом клиенте.
Что остаётся снаружи. Браузер напрямую gRPC не говорит (нужен gRPC-Web с прокси), из curl его не потрогать без grpcurl, отладка по логам сложнее из-за бинарного тела, а HTTP/2 требует, чтобы балансировщик умел балансировать по запросам, а не по соединениям, иначе все вызовы одного клиента уйдут на один экземпляр по одному долгоживущему соединению. Поэтому gRPC берут между сервисами внутри системы, где обе стороны свои, а наружу отдают REST.
Важно понимать, что для gRPC мультиплексирование и потоки — не ускорение, а способ работы. Он построен прямо на потоках HTTP/2: один вызов — один поток, и это позволяет ему то, чего в обычном HTTP нет.
Потоковые вызовы в обе стороны. Кроме привычного «запрос — ответ» есть поток от клиента (отправляем множество сообщений, получаем один ответ), поток от сервера (один запрос, поток ответов — так делают подписки на изменения) и двусторонний поток (обе стороны пишут независимо, как в WebSocket, но с типизированными сообщениями и внутри одного соединения).
Отмена вызова. Клиент, которому ответ больше не нужен, закрывает свой поток — и сервер об этом узнаёт, то есть может прекратить работу. В обычном HTTP/1.1 отмена выглядит как обрыв соединения, и сервер часто продолжает считать.
Дедлайны вместо таймаутов. Клиент передаёт не «сколько я жду», а «до какого момента это имеет смысл», и дедлайн передаётся по цепочке вызовов дальше. Нижний сервис знает, что осталось 40 миллисекунд, и может не начинать работу, которая столько не уложится.
Отсюда и практический вывод: gRPC не «HTTP/2 для сервисов», а другая модель общения. Поэтому он приносит с собой свою балансировку (по запросам, а не по соединениям), свои коды ошибок и свой способ описывать контракт.
Коротко
- HTTP/1.1 обрабатывает запросы по очереди, HTTP/2 гоняет много потоков в одном соединении, HTTP/3 переносит это на QUIC поверх UDP.
- Версия включается на балансировщике, а не в коде; выигрыш HTTP/2 внутри системы съедает одно соединение на всех, если балансировать по соединениям.
- gRPC это Protobuf и HTTP/2 с четырьмя формами вызова; ошибки в трейлере
grpc-status, а не в HTTP-статусе, повторять можно толькоUNAVAILABLE; дедлайн задаёт клиент и он передаётся по цепочке. - Наружу отдают REST, gRPC оставляют между своими сервисами: браузер и
curlего напрямую не говорят, а прокси должны пониматьgrpc-status. - У HTTP/2 три ограничения: потолок одновременных потоков, небольшое окно управления потоком и главное — одно соединение прибивает клиента к одному узлу балансировщика.
- Лечится это прокси, распределяющим запросы, несколькими соединениями или балансировкой на стороне клиента, как в gRPC.
- HTTP/3 дороже по процессору и режется сетями без UDP, а HTTP/2 внутри дата-центра обычно не даёт ничего; для gRPC потоки — не ускорение, а способ работы, вместе с отменой вызова и дедлайнами.
Что почитать дальше
Версии HTTP стоят на транспорте под ними, поэтому логично разобрать сам транспорт: TCP и UDP — там же живёт QUIC, на котором держится HTTP/3. Полезно понять и то, что именно версии оптимизируют — установку и переиспользование соединений: keep-alive, рукопожатия, цена нового соединения. Дальше — сам прикладной слой HTTP (методы, коды, заголовки) и шифрование поверх него, HTTPS и TLS, в паре с которым HTTP/2 и HTTP/3 обычно и работают.