
Сегодня пользователи ожидают от веб-приложений мгновенного отклика. Оптимизация производительности становится критически важной задачей для разработчиков. Один из способов повысить эффективность работы серверов и уменьшить время отклика — «прилипание» HTTP-запросов.
Когда приложения обслуживают много пользователей одновременно, каждая миллисекунда задержки может привести к потере клиентов и снижению качества обслуживания. Прилипание запросов позволяет минимизировать количество необходимых операций, эффективнее распределять ресурсы и улучшить пользовательский опыт.
Меня зовут Ринат Фатхуллин, я владелец продукта Platform V SynGX — веб- и обратного прокси-сервера на основе Nginx. Наш продукт полностью заместил Nginx Plus в Сбере, в том числе благодаря расширенной поддержке «липких» сессий.
Здесь я подробно рассмотрю сценарии использования и особенности этого подхода. Статья будет особенно интересна специалистам, которые занимаются обеспечением бесперебойной работы высоконагруженных сервисов.
Зачем нужны «липкие» сессии
В контексте прокси-сервера прилипание, или «липкие» сессии, — это механизм, позволяющий сохранять связь между первым и последующими HTTP-запросами клиента и узлом в группе балансировки, обрабатывающим эти запросы. Наличие связи означает, что все запросы клиента с определёнными свойствами прокси-сервер будет перенаправлять на один и тот же узел в группе балансировки.
Преимущество этого подхода лежит на поверхности. Когда на экземпляр сервиса приходит первый запрос от клиента, сервис начинает запрашивать данные по клиенту у других сервисов и сохранять у себя в кеше. Но сбор данных занимает время, и если запросы клиента каждый раз будут попадать на разные экземпляры сервиса, то собирать придётся многократно, а это нецелесообразно. Если данные уже есть в кеше, то сервис сможет быстрее ответить на клиентский запрос.
Механизм прилипания решает ещё одну важную задачу — распределение клиентских запросов на основании определённых характеристик. Например, зная IP клиентского запроса, мы можем определить страну или город клиента с помощью базы MaxMindDB и перенаправить запрос на узел, который обслуживает все клиентские запросы из этой страны или города.
Как правило, разработчики веб-сервисов сами определяют, необходимо ли им прилипание клиентских запросов, чтобы улучшить клиентский опыт и пользовательские характеристики сервиса. Обычно прилипание запросов требуется в рамках сеанса пользователя по работе с сервисом. Пример такого сеанса — 15 минут, которые человек потратил на посещение интернет-магазина, поиск и отбор товаров, оформление заказа.
Прилипание даёт ряд преимуществ сервисам, которые поддерживают создание сеанса:
Согласованность: все запросы в рамках сеанса обрабатываются одним и тем же узлом, поэтому данные пользователя остаются согласованными. Это особенно важно для приложений, которые хранят данные сеанса локально на сервере.
Производительность: данные пользователя в рамках сеанса не нужно синхронизировать между узлами сервиса, поэтому производительность сервиса может быть выше благодаря сокращению накладных расходов.
Простота: прилипание запросов может упростить архитектуру сервиса, поскольку разработчикам не нужно внедрять распределенные кеши или распределённые базы данных, в которых данные должны быть одинаковыми и согласованными.
Но, несмотря на очевидные преимущества, у прилипания запросов есть и ряд недостатков:
Влияние на масштабируемость: прилипание может приводить к неравномерному распределению нагрузки. Определённые узлы могут быть перегружены, в то время как другие — недостаточно загружены. Этот дисбаланс может повлиять на общую производительность и масштабируемость сервиса.
Недостаточная надежность: в случае сбоя узла сервиса все сеансы, связанные с этим узлом, прерываются, а данные теряются. Это приводит к ошибкам. Реализовать отработку отказа в этих случаях может быть сложно.
Влияние на управление состоянием: сервисы с прилипанием запросов должны управлять состоянием сеанса пользователя на стороне сервера. Это может усложнить развёртывание и масштабирование, особенно в распределённых средах.
Все эти моменты стоит учитывать, если вы планируете воспользоваться возможностями «липких» сессий.
Как используется прилипание в контексте сеансов пользователей
Прилипание запросов может применяться в самых разных случаях. Рассмотрим самые распространённые.
-
Интернет-магазины
Корзины для покупок: если привязать сеанс к определённому узлу сервиса, то все действия, связанные с корзиной (добавление или удаление товаров) будут направляться на один и тот же сервер. Это поможет предотвратить потерю товаров или несоответствие содержимого корзины.
Оформление заказа: поддержание сеанса с одним и тем же сервером при оформлении заказа гарантирует бесперебойную обработку информации об оплате и доставке без перерывов или потери данных.
-
Игровые приложения
Многопользовательские онлайн-игры: поддержание постоянной сессии и своевременное обновление данных для каждого игрока имеют решающее значение. Прилипание запросов обеспечивает последовательное управление состоянием игры, прогрессом игрока и взаимодействиями.
-
Финансовые услуги
Онлайн-банкинг: привязанный к определённому узлу сервиса сеанс гарантирует, что все транзакции и действия пользователя последовательно выполняет один и тот же сервер. Это крайне важно для поддержания безопасности и целостности данных при конфиденциальных финансовых операциях.
Торговые платформы: прилипание запросов помогает эффективно управлять сеансами пользователей и гарантирует, что торговые данные, действия пользователя и обновления рынка последовательно обрабатываются и отображаются.
Какие есть альтернативы прилипанию запросов
Многие современные приложения используют конструкции без состояния и/или распределённое управление сеансами пользователей, чтобы устранить ограничения, связанные с прилипанием запросов:
Приложения без состояния (stateless): данные пользователя в рамках сеанса могут храниться на стороне клиента (например, в файлах cookie или локальном хранилище) или в централизованном хранилище сеансов, таком как Redis или база данных. Такой подход позволяет любому узлу в группе балансировки обрабатывать любой запрос, что повышает надёжность сервиса.
Распределённое кеширование: для централизованного хранения данных сеанса пользователя можно использовать такие инструменты, как Redis, Memcached или другие распределённые кеши. Эти данные доступны для чтения и записи всем узлам в группе балансировки. Такой метод позволяет горизонтально масштабировать сервис.
Глобальные балансировщики нагрузки (GSLB): балансировщики могут обеспечить более интеллектуальную маршрутизацию с учётом географии пользователя, работоспособности сервиса в каком-либо ЦОДе и других факторов, уменьшая потребность в прилипании запросов.
Если прилипание всё же необходимо, следующий шаг — выбор вариантов, которые предоставляет прокси-сервер. Как правило, в большинстве прокси-серверов (Nginx, HAProxy, Envoyproxy и др.) методы прилипания реализованы похожим образом, но по-разному описываются в конфигурации.
Варианты прилипания клиентских HTTP-запросов
Рассмотрим варианты прилипания клиентских HTTP-запросов, которые предоставляет известный веб- и прокси-сервер Nginx. Они представлены директивами, которые задаются в секции http/upstream:
ip_hash
hash
sticky cookie
sticky route
sticky learn
Первый вариант представлен директивой ip_hash
в стандартном модуле Nginx ngx_http_upstream_module. Клиентские запросы объединяются по IP-адресу клиента, указанному на уровне TCP-протокола. Это означает, что запросы с одним и тем же IP клиента будут всегда передаваться на один и тот же узел в группе балансировки.
Помните, что при прохождении клиентских запросов через маршрутизатор IP-адрес клиента с большой вероятностью будет заменён на IP-адрес этого устройства и станет неподходящим для распределения запросов. Также важно, что значение HTTP-заголовка X-Forwarded-For
не используется при выборе узла для передачи запроса при заданной директиве ip_hash
.
Второй вариант представлен директивой hash
в стандартном модуле Nginx ngx_http_upstream_module. Клиентские запросы объединяются по ключу, заданному в качестве обязательного аргумента для директивы. В качестве ключа можно использовать постоянное значение, какую-либо встроенную переменную Nginx, либо их комбинацию. В частности, в качестве ключа можно использовать значение заголовка X-Forwarded-For
(hash $http_x_forwarded_for
). Это означает, что HTTP-запросы с одним и тем же значением ключа будут всегда передаваться на один и тот же узел в группе балансировки.
При использовании методов прилипания с помощью директив ip_hash
и hash
любое добавление или удаление узлов в группе балансировки может привести к перераспределению большинства прилипаний на другие узлы — даже тех, для которых это и не нужно. Для борьбы с перераспределением у директивы hash
есть параметр consistent
, который значительно уменьшает количество нежелательных перераспределений, но не гарантирует, что их полностью не будет.
Все остальные варианты представлены директивой sticky
, заданной в стандартном модуле Nginx ngx_http_upstream_module, с параметрами cookie
, route
или learn
. Причём параметры route
и learn
доступны только в коммерческой версии Nginx Plus.
При использовании директивы cookie
в общем случае предполагается, что клиентские запросы объединяются и перенаправляются на один и тот же узел по наличию в запросах некоторой cookie с определённым значением. Напомню, что одна или несколько cookie могут передаваться в запросе в HTTP-заголовке Cookie
, а в ответе — в HTTP-заголовке Set-Cookie
.
Для рассмотрения будем использовать примерно такую простую конфигурацию Nginx:
http {
server {
listen 8000;
location / {
proxy_pass my_upstream;
}
}
server {
listen 9001;
location / {
return ....
}
}
}
upstream my_upstream {
server 127.0.0.1:9001;
server 127.0.0.1:9002;
sticky cookie my_cookie;
}
Клиентские запросы на Nginx будем с помощью утилиты curl отправлять на сервер, слушающий порт 8000. Этот сервер будет перенаправлять запросы на внутренние серверы, слушающие порты 9001 и 9002. Внутренние серверы будут возвращать разные ответы, из которых станет понятна логика алгоритмов прилипания. Напомню, что директива sticky
с разными параметрами указывается в секции http/upstream
.
Начнём с ситуации, когда в конфигурации задан способ прилипания sticky cookie.
Пусть на Nginx пришёл первый запрос от клиента без cookie. В этом случае Nginx в соответствии с заданным алгоритмом балансировки выберет какой-либо узел и перенаправит запрос на него. При этом после получения ответа от узла Nginx добавит в него заголовок Set-cookie
, в котором укажет значение cookie, равное значению MD5-хеша от IP-адреса и порта узла, на который был отправлен запрос и от которого получили ответ.
Вот листинг команды curl
http://127.0.0.1:8000
:
Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Mon, 27 Jan 2025 10:18:45 GMT
< Content-Type: text/plain
< Content-Length: 2
< Connection: keep-alive
< Set-Cookie: my_cookie=2b46daf0ba503cbe30c3b46dfad73e0a; Path=/
<
* Connection #0 to host 127.0.0.1 left intact
Nginx в ответе установил значение cookie my_cookie=2b46daf0ba503cbe30c3b46dfad73e0a
.
Если следующие запросы будут содержать cookie с этим значением (равным значению MD5-хеша от IP-адреса и порта узла в группе балансировки), то Nginx будет их перенаправлять на узел, соответствующий значению cookie. Если этот узел окажется недоступен, Nginx выберет новый узел и укажет в ответе новое значение cookie.
Почему выбранный узел будет считаться недоступным? За это в Nginx отвечает механизм пассивной проверки работоспособности узла. Для его настройки используются параметры max_fails
и fail_timeout
в директиве upstream/server
, а также директивы с префиксом proxy_next_upstream
. В коммерческих версиях прокси-серверов (в том числе в Nginx Plus) реализован также механизм активной проверки работоспособности, результаты которого тоже будут учитываться при выборе узла. Здесь можно почитать описание этого механизма.
Если нужная cookie в запросе уже есть, то Nginx в ответе не добавляет заголовок Set-cookie
.
Отправим HTTP-запрос curl -v
http://127.0.0.1:8100/
--cookie "my_cookie=2b46daf0ba503cbe30c3b46dfad73e0a"
и получим в ответ:
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.61.1
> Accept: */*
> Cookie: my_cookie=2b46daf0ba503cbe30c3b46dfad73e0a
>
< HTTP/1.1 200 OK
< Date: Mon, 27 Jan 2025 10:21:49 GMT
< Content-Type: text/plain
< Content-Length: 2
< Connection: keep-alive
<
* Connection #0 to host 127.0.0.1 left intact
Как видите, никакие cookie в ответе не заданы.
Что меняется в случае способа прилипания sticky route?
На самом деле немногое. Основное отличие в том, что для поиска нужного узла Nginx использует:
в случае sticky cookie — значение определённой cookie, которое должно равняться MD5-хешу от IP-адреса и порта узла;
в случае sticky route — значение вычисляемой при обработке запроса переменной (это может быть также cookie, заданная как
$cookie_a
), которое должно совпадать со значением дополнительного параметраroute
в директивеupstream/server
.
За передачу клиенту информации о том, что необходимо добавить в запрос для прилипания, отвечает узел, на который будет перенаправлен запрос. Обычно также используется cookie и добавление в ответ заголовка Set-cookie
, значение которой клиент использует при повторных обращениях.
Часть конфигурации при этом может выглядеть так:
map $cookie_jsessionid $route_cookie {
~.+\.(?P<route>\w+)$ $route;
}
map $request_uri $route_uri {
~jsessionid=.+\.(?P<route>\w+)$ $route;
}
upstream test_https2 {
server 127.0.0.1:8082 route=b;
server 127.0.0.2:8082 route=a;
sticky route $route_cookie $route_uri;
}
Что здесь происходит? С помощью первой директивы map
при обработке каждого запроса в переменную $route_cookie
заносится часть значения cookie jsessionid
, если это значение соответствует регулярному выражению. Если значение cookie регулярному выражению не соответствует, то значение переменной $route_cookie
будет пустым.
То же самое происходит с помощью второй директивы map
для переменных $request_uri
и $route_uri
.
Далее при выборе узла для проксирования Nginx:
сначала пытается сопоставить значение переменной
$route_cookie
со значением параметраroute
дляserver
;если это не удаётся — значение переменной
$route_uri
со значением параметраroute
дляserver
.
Если совпадение найдено, то именно на этот узел (upstream/server
) будет перенаправлен запрос.
В директиве sticky route
можно указать несколько переменных, значения которых будут сопоставляться со значением параметра route
.
А для чего нужен метод прилипания sticky learn?
Рассмотрим ситуацию. Клиент делает первый запрос, который прокси-сервер распределяет на один из узлов в группе балансировки. Узел отвечает с заголовком Set-Cookie
, в котором указана cookie my_cookie
. Клиент получает ответ, запоминает значение cookie my_cookie
и все последующие запросы делает с ней. При этом клиент хочет, чтобы все последующие запросы попадали на узел, который ответил на первый запрос (если узел «живой»), независимо от того, что происходит с другими узлами.
Казалось бы, для этих целей можно использовать алгоритм прилипания hash. Но я уже говорил, что у него есть недостаток: любое добавление или удаление узлов в группе балансировки может привести к перераспределению большинства прилипаний на другие узлы — даже там, где это не нужно. В такой ситуации может использоваться алгоритм прилипания sticky learn.
Вот как это работает. В каждом запросе от клиента прокси-сервер проверяет наличие cookie, указанной в параметре lookup
. Если этой cookie в запросе нет, то прокси-сервер выбирает узел в соответствии с заданной стратегией балансировки с учётом информации о доступности или недоступности узлов.
Когда от узла приходит ответ на запрос, прокси-сервер проверяет наличие в нём cookie, заданной с помощью параметра create
. Ниже я привёл пример, где эта переменная указывает на заголовок Set-Cookie и cookie examplecookie
в ней. Если такая cookie есть, то прокси-сервер извлекает её и запоминает в привязке к узлу в группе балансировки, который её выставил. Все последующие запросы от клиента с этой cookie будут перенаправляться на этот же узел. Однако если выбранный узел выведен из балансировки по результатам активной проверки работоспособности, то SynGX выбирает другой узел в соответствии с заданным алгоритмом балансировки, как если бы привязки не существовало.
А как конкретно прокси-сервер запоминает привязку значения cookie к узлу в группе балансировки? Напомню, что параметр learn
для директивы sticky
доступен только в коммерческой версии Nginx Plus, поэтому сказать однозначно я не могу. Однако поскольку функциональность sticky learn доступна в нашем собственном прокси-сервере Platform V SynGX на основе Nginx, то расскажу, как нам удалось его реализовать.
Как Platform V SynGX запоминает привязку значений cookie к узлам в группе балансировки
Platform V SynGX — высокопроизводительный веб- и обратный прокси-сервер, который СберТех разработал на кодовой базе Nginx. В 2024 году мы полностью заместили этим решением коммерческую версию Nginx Plus в Сбере, и сейчас оно установлено более чем на 15 000 серверов в инфраструктуре банка. Platform V SynGX используется в системах c высоким уровнем критичности, работающих в режиме 24/7/365: сайт Сбера, мобильный банк, СберБизнес и других.
Как Platform V SynGX запоминает привязку значений cookie к узлам в группе балансировки? В конфигурации это может выглядеть, например, так:
upstream backend {
server backend1.example.com:8080;
server backend2.example.com:8081;
sticky learn
create=$upstream_cookie_examplecookie
lookup=$cookie_examplecookie
zone=client_sessions:1m
timeout=1h
cookie_max_size=256
gc_period=5m;
}
Что Platform V SynGX будет делать в этом случае?
Всё начинается с первого запроса от клиента, в котором нет cookie, указанной в параметре lookup
. В примере конфигурации выше это cookie с именем examplecookie
. В этом случае Platform V SynGX выберет доступный узел в соответствии с заданным алгоритмом балансировки и перенаправит запрос на него.
Получив ответ от узла, решение проанализирует его на наличие cookie, указанной в параметре create
(в примере выше это cookie с именем examplecookie
). Если такая cookie есть (задана в HTTP-заголовке Set-cookie
), то Platform V SynGX создаcт запись «ключ-значение», которая содержит привязку значения этой cookie к узлу. Ключом будет значение cookie, а значением — узел, куда необходимо проксировать последующие запросы. Все подобные записи Platform V SynGX хранит в распределённой памяти (shared memory), чтобы обеспечить доступ к ним всех рабочих процессов (workers).
Клиент получает cookie в ответ на свой первый запрос, сохраняет её у себя и все последующие запросы делает уже с ней.
Если в запросе от клиента есть cookie (в примере конфигурации выше это cookie с именем examplecookie
), то Platform V SynGX берет её значение и начинает искать соответствующий ей узел в распределённой памяти. Если находит, то перенаправляет на него запрос. Если соответствующего ключа и значения в памяти нет, то Platform V SynGX, как и ранее, выберет доступный узел в соответствии с заданным алгоритмом балансировки и перенаправит запрос на него.
Звучит просто, но могут возникать интересные вопросы:
В какой структуре хранить пары «ключ-значение», чтобы обеспечить быстрый поиск по ключу?
Как сделать так, чтобы несколько рабочих процессов не блокировали или минимально блокировали друг друга при обращении к этой структуре?
Сколько памяти выделять для хранения необходимого количества пар «ключ-значение»?
Как сделать так, чтобы хватало памяти?
Что делать, если выделенная память закончилась, но необходимо добавить новую запись «ключ-значение»?
Начну с предпоследнего вопроса. Ответ может быть такой: удалять старые, давно не используемые пары «ключ-значение». Для этого будем для каждой пары дополнительно хранить время последнего использования и с некоторой периодичностью запускать сборщик мусора.
А как сделать так, чтобы неиспользуемые ключи можно было получить, не тратя время на перебор всей таблицы? Для ответа на этот вопрос мы добавили следующие параметры:
zone=client_sessions:1m
: определяет размер выделенной памяти для хранения всех пар «ключ-значение» (здесь 1 мегабайт);timeout=1h
: определяет, по истечении какого времени считать пару давно не используемой (здесь 1 час);cookie_max_size=256
: определяет максимальный размер значения cookie, используемого в качестве ключа (здесь 256 байт);gc_period=5m
: определяет, как часто запускать сборщик мусора (здесь раз в 5 минут);session_rotate
: определяет, нужно ли освобождать место для новой пары, удаляя самую «древнюю», если выделенная память уже заполнена;fill_limit_minor_log
: определяет пороговую долю активных пар от максимально возможного количества, при которой выдаётся сообщение в журнал ошибок на уровне[warn]
;fill_limit_major_log
: определяет пороговую долю активных клиентских сессий от максимально возможного количества, при которой выдаётся сообщение в журнал ошибок на уровне[error]
;fill_limit_crit_log
: определяет пороговую долю активных клиентских сессий от максимально возможного количества, при которой выдаётся сообщение в журнал ошибок на уровне[crit]
.
Мы также реализовали предоставление метрик о текущем количестве активных записей «ключ-значение» в памяти и общее количество использования этих пар при распределении запросов на узлы.
Подведем итоги
Механизм прилипания HTTP-запросов может улучшить клиентский опыт использования веб-сервиса.
Open-source версия Nginx предоставляет несколько вариантов прилипания, которых может быть вполне достаточно для работы.
Platform V SynGX, как и другие коммерческие версии прокси-серверов, предоставляет дополнительные варианты прилипания, которые могут быть интересны искушённым разработчикам или архитекторам веб-сервисов.
Platform V SynGX обладает рядом функциональных и нефункциональных преимуществ относительно open-source версии Nginx. Если вам интересно больше узнать о них, — обращайтесь: ответим на все вопросы и проведём для вас демонстрацию ?
Комментарии (3)
Licemery
26.06.2025 14:46Ах, ну конечно же миллисекунды задержки им теряют клиентов, а мерзкая капча при каждом входе на сайте, где надо извилины напрягать, а не просто ждать - нет. Преждевременная оптимизация это, заниматься крохоборством, игнорируя слона в комнате. Не говоря уж о том, что вообще-то унизительно доказывать, что не верблюд.
olku
26.06.2025 14:46А как прилипание дружит с Кубером? Интересно практическое применение, а то статья напоминает переработанную доку Nginx.
kulaginds
Не пробовали angie?