1. Краткое напоминание об HTTP
Этот документ описывает язык конфигурации в реализации указанной выше версии. Он не содержит подсказок, примеров или рекомендаций. За такими сведениями обращайтесь к справочному руководству или руководству по архитектуре. Пронумерованные главы расположены в плоской боковой панели HAProxy для прямого перехода.
Когда HAProxy работает в режиме HTTP, и запрос, и ответ полностью анализируются и индексируются. Это позволяет строить критерии сопоставления практически по любым элементам содержимого.
Однако важно понимать устройство запросов и ответов HTTP и то, как HAProxy разбирает их на части. Тогда будет проще писать корректные правила и отлаживать существующие конфигурации.
Прежде всего, HTTP стандартизован серией RFC, которым HAProxy следует максимально точно:
- RFC 9110: семантика HTTP (объясняет смысл элементов протокола).
- RFC 9111: кеширование HTTP (описывает правила, которым должен следовать кеш HTTP).
- RFC 9112: HTTP/1.1 (представление, правила взаимодействия, безопасность).
- RFC 9113: HTTP/2 (представление, правила взаимодействия, безопасность).
- RFC 9114: HTTP/3 (представление, правила взаимодействия, безопасность).
Кроме того, RFC с 8999 по 9002 определяют транспортный уровень QUIC, используемый протоколом HTTP/3.
1.1. Модель транзакций HTTP
Протокол HTTP основан на транзакциях. Это означает, что каждый запрос приводит ровно к одному ответу. Изначально, в версии 1.0, на соединение приходился один запрос: клиент устанавливал соединение TCP с сервером, отправлял по нему запрос, сервер отвечал, после чего соединение закрывалось. Для нового запроса требовалось новое соединение:
В этом режиме, часто называемом «HTTP close», число установок соединений равно числу транзакций HTTP. Поскольку сервер закрывает соединение после ответа, клиенту не нужно знать длину содержимого: он считает ответ завершённым при закрытии соединения. Это также означает, что при обрыве ответа из-за сетевой ошибки клиент мог ошибочно счесть его полным; раньше это иногда приводило к отображению обрезанных изображений.
Транзакционная природа протокола позволила усовершенствовать его, исключив закрытие соединения между двумя последовательными транзакциями. Однако в таком режиме сервер обязан указывать длину содержимого каждого ответа, чтобы клиент не ждал бесконечно. Для этого используется специальный заголовок «Content-length». Этот режим называется «keep-alive» и появился в HTTP/1.1 (его поддерживают некоторые агенты HTTP/1.0), а соединения, повторно используемые между запросами, называются постоянными:
Преимущества — меньшая задержка между транзакциями, меньшие затраты вычислительных ресурсов сервера и возможность обнаруживать обрезанные ответы. Обычно этот режим быстрее режима закрытия соединений, но не всегда: некоторые клиенты ограничивают число одновременных соединений меньшим значением и потому хуже компенсируют низкое качество сети. Кроме того, некоторым серверам приходится долго держать соединения открытыми в ожидании нового запроса, что при большом числе соединений может потреблять много памяти. Слишком быстрое закрытие может нарушить обработку запросов, пришедших в момент закрытия соединения.
В этом режиме размер ответа нужно знать заранее, что не всегда возможно для динамически формируемого или сжатого содержимого. Поэтому был реализован другой режим — «chunked mode». Вместо объявления полного размера отправитель сообщает только размер следующего фрагмента ответа, уже находящегося в буфере, и может завершить передачу в любой момент фрагментом нулевого размера. В этом режиме заголовок Content-Length не используется.
Ещё одно улучшение обмена — конвейерный режим. Он по-прежнему использует постоянные соединения, но клиент не ждёт первого ответа, чтобы отправить второй запрос. Это удобно при загрузке большого числа изображений страницы:
Очевидно, что устранение сетевой задержки между последовательными запросами может значительно повысить производительность. Многие агенты HTTP некорректно поддерживают конвейерную обработку, поскольку HTTP не позволяет явно связать ответ с соответствующим запросом. Поэтому сервер обязан отвечать строго в порядке получения запросов. На практике после нескольких попыток внедрения в разных клиентах от этого режима полностью отказались из-за ненадёжной работы с некоторыми серверами. Однако серверы обязаны его поддерживать.
Следующее усовершенствование — мультиплексирование, реализованное в HTTP/2 и HTTP/3. В этом режиме несколько транзакций, то есть пар «запрос — ответ», передаются параллельно по одному соединению и продвигаются независимо, каждая со своей скоростью. Для описания таких параллельных обменов в одном соединении протоколы с мультиплексированием ввели понятие потока. Каждому потоку обычно назначается уникальный в рамках соединения идентификатор, по которому обе стороны определяют, куда направлять данные. Клиенты довольно часто запускают множество потоков (до 100, иногда больше) параллельно по одному соединению, позволяя серверу обрабатывать их и отвечать в любом порядке по мере готовности ответов. Главное преимущество мультиплексирования — значительное сокращение числа сетевых обменов и ускорение загрузки страниц в сетях с высокой задержкой. Иногда это заметно на сайтах с большим числом изображений, где все изображения будто загружаются одновременно.
Эти протоколы также повысили эффективность за счёт механизмов сжатия полей заголовков, уменьшающих число передаваемых байтов. Поэтому без соответствующих инструментов их практически невозможно обрабатывать вручную или читать невооружённым глазом, как HTTP/1. По этой причине в литературе, включая этот документ, примеры сообщений HTTP продолжают представлять в синтаксисе HTTP/1 даже для новых версий протокола.
У HTTP/2 есть ограничения архитектуры: например, потеря пакетов затрагивает сразу все потоки. Если клиент слишком долго получает объект, например записывает его на диск, он может замедлить получение и тем самым временно сделать недоступными данные, ожидающие за этим объектом. Это называется блокировкой начала очереди — «head of line blocking», «HoL blocking» или просто «HoL».
HTTP/3 реализован поверх QUIC, который, в свою очередь, работает поверх UDP. QUIC устраняет блокировку начала очереди на транспортном уровне за счёт независимой обработки потоков. При потере данных затронутый поток не влияет на остальные, и все потоки доступны параллельно. QUIC также поддерживает миграцию соединений, но в настоящее время haproxy её не поддерживает.
По умолчанию HAProxy использует режим постоянных соединений: для каждого соединения он обрабатывает запросы и ответы и оставляет соединение открытым, но бездействующим с обеих сторон между окончанием ответа и началом следующего запроса. При получении клиентского соединения HTTP/2 он параллельно обрабатывает все запросы и оставляет соединение в ожидании новых, как обычное постоянное соединение HTTP.
В основном HAProxy поддерживает 3 режима соединений:
keep alive: обрабатываются все запросы и ответы, а соединения со стороны клиента и сервера сохраняются для новых запросов. Это режим по умолчанию, подходящий для современного веба и современных протоколов HTTP/2 и HTTP/3.
server close: соединение с сервером закрывается после ответа.
close: после завершения ответа соединение активно закрывается с обеих сторон.
Кроме того, по умолчанию соединение с сервером может повторно использоваться любым запросом любого клиента, как требует спецификация HTTP. Поэтому необходимые сведения о конкретном клиенте, например исходный адрес, нужно передавать с каждым запросом. При использовании HTTP/2 с сервером HAProxy по умолчанию выделяет это соединение одному клиенту, чтобы избежать риска блокировки начала очереди между клиентами.
1.2. Терминология
Терминология HAProxy со временем немного менялась вслед за развитием протокола HTTP и способов его использования. Изначально существенных различий между соединением, сеансом, потоком и транзакцией не было. Со временем понятия уточнили в соответствии с современными версиями HTTP, хотя ради исторической совместимости некоторые термины сохранились в конфигурации и интерфейсе командной строки.
Следующие определения относятся к текущей версии HAProxy:
Соединение: отдельный двунаправленный канал связи между удалённым агентом (клиентом или сервером) и haproxy на самом низком возможном уровне. Обычно это сокет TCP между парой IP-адресов и портов. На клиентской стороне соединения создаются самыми первыми при подключении клиента к haproxy, и правила уровня соединения применяются раньше всех остальных.
Сеанс: добавляет к соединению контекстные сведения, в том числе информацию транспортного уровня (например, ключи TLS) или переменные. Долгое время этот термин в HAProxy обозначал сквозной обмен HTTP/1.0 между двумя сторонами. Поэтому он сохранился в именах некоторых команд CLI и статистических показателей, хотя теперь они представляют потоки; справка и описания стараются устранить неоднозначность. Термин по-прежнему уместен на сетевом уровне (например, сеансы TCP в операционной системе или через межсетевой экран) и в пользовательских приложениях, не использующих HTTP (например, сеанс telnet или SSH). Его нельзя путать с сеансами приложения, которые хранят полный контекст пользователя в cookie и требуют направления запросов на один сервер.
Поток: в точности соответствует сквозному двунаправленному обмену на уровне приложения, где могут применяться анализ и преобразования. В HTTP он содержит один запрос и связанный с ним ответ, создаётся при поступлении запроса и завершается по окончании доставки ответа. В этом контексте между таким потоком и потоком мультиплексируемого протокола существует соответствие 1:1. При обмене по TCP на соединение приходится один поток.
Транзакция: только пара из запроса и связанного с ним ответа. До появления потоков термин использовали вместе с сеансами, но теперь транзакция и поток находятся в отношении 1:1. Главным образом он встречается в области видимости переменных «txn», действующей на протяжении всей транзакции, а значит, и потока.
Запрос: трафик от клиента к серверу. Термин преимущественно используется в HTTP для указания направления выполнения операций. Он также применяется к операциям TCP, обозначая направление обработки данных. Запросы часто используются в счётчиках как единица трафика или активности. Не каждый запрос получает ответ, например из-за ошибок, но поскольку самопроизвольных ответов без запросов не бывает, запросы остаются подходящей мерой общей активности. В TCP число запросов равно числу соединений.
Ответ: трафик от сервера к клиенту, а иногда от HAProxy к клиенту, если HAProxy сам формирует ответ, например перенаправление HTTP.
Служба: обычно внутренняя обработка в HAProxy, не требующая сервера, например страница статистики, кеш или код Lua, реализующий небольшое приложение. Как правило, служба читает запрос, выполняет операции и формирует ответ.
1.3. Запрос HTTP
Сначала рассмотрим следующий запрос HTTP:
1.3.1. Строка запроса
Строка 1 — это строка запроса. Она всегда состоит из 3 полей:
- Метод: GET.
- URI: /serv/login.php?lang=en&profile=2.
- Метка версии: HTTP/1.1.
Все поля разделяются тем, что стандарт называет LWS — линейными пробельными символами. Обычно это пробелы, но возможны также табуляции либо переводы строки/возвраты каретки с последующими пробелами или табуляциями. Сам метод не может содержать двоеточие (’:’) и состоит только из букв алфавита. Из-за множества комбинаций лучше, чтобы HAProxy сам разбивал строку на части, а не заставлял пользователя писать сложное или неточное регулярное выражение.
URI может иметь несколько форм:
- Относительный URI:
- Абсолютный URI, также называемый URL:
Звёздочка (’*’): эта форма разрешена только с методом OPTIONS и не подлежит ретрансляции. Она используется для запроса возможностей следующего узла.
Сочетание адреса и порта: 192.168.0.12:80. Используется с методом CONNECT для установки туннелей TCP через прокси HTTP, обычно для HTTPS, но иногда и для других протоколов.
В относительном URI выделяют две части. Часть до вопросительного знака называется путём. Обычно это относительный путь к статическим объектам на сервере. Часть после вопросительного знака называется строкой запроса. Она в основном используется в запросах GET к динамическим скриптам и сильно зависит от языка, фреймворка или приложения.
HTTP/2 и HTTP/3 не передают сведения о версии вместе с запросом: предполагается версия используемого протокола, например «HTTP/2». Кроме того, эти протоколы не передают строку запроса целиком, а разбивают её на отдельные поля — псевдозаголовки, имена которых начинаются с двоеточия. HAProxy для удобства собирает их в эквивалентную строку запроса. Поэтому строки запросов в журналах могут немного различаться для HTTP/1.x и HTTP/2 или HTTP/3.
1.3.2. Заголовки запроса
Заголовки начинаются со второй строки. В начале строки находится имя, сразу за ним — двоеточие (’:’). Традиционно после двоеточия добавляют LWS, но это не обязательно. Далее идут значения. Несколько одноимённых заголовков можно объединить в одну строку, разделяя значения запятыми и сохраняя их порядок. Это часто встречается в поле «Cookie:». Заголовок может занимать несколько строк, если последующие строки начинаются с LWS. В примере из 1.3 строки 4 и 5 задают суммарно 3 значения заголовка «Accept:». Наконец, согласно спецификации все LWS в начале и конце заголовка игнорируются и не входят в значение.
Вопреки распространённому заблуждению, имена заголовков нечувствительны к регистру. Их значения также нечувствительны к регистру, если ссылаются на имена других заголовков, как в «Connection:». В HTTP/2 и HTTP/3 имена заголовков всегда передаются в нижнем регистре, что видно в режиме отладки. Внутри HAProxy все имена заголовков приводятся к нижнему регистру, чтобы HTTP/1.x и HTTP/2 или HTTP/3 использовали одно и то же представление, и в таком виде передаются другой стороне. Поэтому запрос HTTP/1.x, введённый с именами в смешанном регистре, доставляется с именами в нижнем регистре.
Первая пустая строка обозначает конец заголовков. Часто говорят о двойном переводе строки, что неточно, хотя двойной перевод строки — одна из допустимых форм пустой строки.
К счастью, HAProxy сам обрабатывает все эти сложные сочетания при индексации заголовков, проверке значений и подсчёте. Поэтому об их конкретном оформлении можно не беспокоиться, но важно не обвинять приложение в ошибке, если оно использует необычные, но допустимые конструкции.
Важное примечание:
1.4. Ответ HTTP
Ответ HTTP очень похож на запрос HTTP. И тот и другой называют сообщениями HTTP. Рассмотрим следующий ответ:
Особый случай в HTTP — так называемые информационные ответы с кодами состояния 1xx. Они не содержат никакой части ответа, а служат сигнальными сообщениями, например чтобы попросить клиента продолжить отправку запроса. При коде 100 запрошенные сведения содержатся в следующем после информационного сообщении ответа с кодом, отличным от 100. Это означает, что на один запрос может быть отправлено несколько ответов и что такое возможно только при включённых постоянных соединениях (сообщения 1xx появились в HTTP/1.1). HAProxy умеет корректно пересылать и пропускать эти сообщения, обрабатывая лишь следующий ответ с кодом, отличным от 100. Поэтому такие сообщения не записываются в журнал и не преобразуются, если явно не указано иное. Сообщения с кодом 101 означают смену протокола в том же соединении, и HAProxy должен перейти в туннельный режим, как после CONNECT. Заголовок Upgrade в таком случае содержит дополнительные сведения о протоколе, на который переключается соединение.
1.4.1. Строка ответа
Строка 1 — это строка ответа. Она всегда состоит из 3 полей:
- Метка версии: HTTP/1.1.
- Код состояния: 200.
- Причина: OK.
Код состояния всегда состоит из 3 цифр. Первая обозначает общую категорию:
- 1xx = информационное сообщение, которое следует пропустить (например, 100, 101).
- 2xx = OK, далее следует содержимое (например, 200, 206).
- 3xx = OK, далее содержимого нет (например, 302, 304).
- 4xx = ошибка по вине клиента (например, 401, 403, 404).
- 5xx = ошибка по вине сервера (например, 500, 502, 503).
Коды состояния выше 599 запрещено передавать при обмене, хотя некоторые агенты могут записывать их в журналы для обозначения внутренних состояний. Подробное значение всех кодов описано в RFC9110. В HTTP/2 и новее нет метки версии, а код состояния передаётся в псевдозаголовке «:status».
Поле причины служит лишь подсказкой и не разбирается клиентами. В нём может быть что угодно, но принято использовать общеизвестные сообщения. Оно может состоять из одного или нескольких слов, например «OK», «Found» или «Authentication Required». В HTTP/2 и новее этого поля нет и оно не передаётся. При передаче ответа из HTTP/2 или новее клиенту HTTP/1 HAProxy формирует стандартное поле причины, соответствующее коду состояния.
HAProxy может самостоятельно выдавать следующие коды состояния:
Приведённые выше ответы с кодами ошибок 4xx и 5xx можно настраивать (см. «errorloc» в разделе 4.2 ). Другие коды состояния можно выдавать намеренно с помощью специальных действий (например, «deny», «return» и «redirect» в разделе 4.3 ).
1.4.2. Заголовки ответа
Заголовки ответа устроены точно так же, как заголовки запроса, поэтому HAProxy использует для обоих одну функцию разбора. Подробнее см. пункт 1.3.2.