Сегодня пользователи ожидают от веб-приложений мгновенного отклика. Оптимизация производительности становится критически важной задачей для разработчиков. Один из способов повысить эффективность работы серверов и уменьшить время отклика — «прилипание» HTTP-запросов

Когда приложения обслуживают много пользователей одновременно, каждая миллисекунда задержки может привести к потере клиентов и снижению качества обслуживания. Прилипание запросов позволяет минимизировать количество необходимых операций, эффективнее распределять ресурсы и улучшить пользовательский опыт. 

Меня зовут Ринат Фатхуллин, я владелец продукта Platform V SynGX — веб- и обратного прокси-сервера на основе Nginx. Наш продукт полностью заместил Nginx Plus в Сбере, в том числе благодаря расширенной поддержке «липких» сессий. 

Здесь я подробно рассмотрю сценарии использования и особенности этого подхода. Статья будет особенно интересна специалистам, которые занимаются обеспечением бесперебойной работы высоконагруженных сервисов. 

Зачем нужны «липкие» сессии

В контексте прокси-сервера прилипание, или «липкие» сессии, — это механизм, позволяющий сохранять связь между первым и последующими HTTP-запросами клиента и узлом в группе балансировки, обрабатывающим эти запросы. Наличие связи означает, что все запросы клиента с определёнными свойствами прокси-сервер будет перенаправлять на один и тот же узел в группе балансировки. 

Преимущество этого подхода лежит на поверхности. Когда на экземпляр сервиса приходит первый запрос от клиента, сервис начинает запрашивать данные по клиенту у других сервисов и сохранять у себя в кеше. Но сбор данных занимает время, и если запросы клиента каждый раз будут попадать на разные экземпляры сервиса, то собирать придётся многократно, а это нецелесообразно. Если данные уже есть в кеше, то сервис сможет быстрее ответить на клиентский запрос.

Механизм прилипания решает ещё одну важную задачу — распределение клиентских запросов на основании определённых характеристик. Например, зная IP клиентского запроса, мы можем определить страну или город клиента с помощью базы MaxMindDB и перенаправить запрос на узел, который обслуживает все клиентские запросы из этой страны или города. 

Как правило, разработчики веб-сервисов сами определяют, необходимо ли им прилипание клиентских запросов, чтобы улучшить клиентский опыт и пользовательские характеристики сервиса. Обычно прилипание запросов требуется в рамках сеанса пользователя по работе с сервисом. Пример такого сеанса — 15 минут, которые человек потратил на посещение интернет-магазина, поиск и отбор товаров, оформление заказа. 

Прилипание даёт ряд преимуществ сервисам, которые поддерживают создание сеанса:

  • Согласованность: все запросы в рамках сеанса обрабатываются одним и тем же узлом, поэтому данные пользователя остаются согласованными. Это особенно важно для приложений, которые хранят данные сеанса локально на сервере.

  • Производительность: данные пользователя в рамках сеанса не нужно синхронизировать между узлами сервиса, поэтому производительность сервиса может быть выше благодаря сокращению накладных расходов.

  • Простота: прилипание запросов может упростить архитектуру сервиса, поскольку разработчикам не нужно внедрять распределенные кеши или распределённые базы данных, в которых данные должны быть одинаковыми и согласованными.

Но, несмотря на очевидные преимущества, у прилипания запросов есть и ряд недостатков: 

  • Влияние на масштабируемость: прилипание может приводить к неравномерному распределению нагрузки. Определённые узлы могут быть перегружены, в то время как другие — недостаточно загружены. Этот дисбаланс может повлиять на общую производительность и масштабируемость сервиса.

  • Недостаточная надежность: в случае сбоя узла сервиса все сеансы, связанные с этим узлом, прерываются, а данные теряются. Это приводит к ошибкам. Реализовать отработку отказа в этих случаях может быть сложно.

  • Влияние на управление состоянием: сервисы с прилипанием запросов должны управлять состоянием сеанса пользователя на стороне сервера. Это может усложнить развёртывание и масштабирование, особенно в распределённых средах.

Все эти моменты стоит учитывать, если вы планируете воспользоваться возможностями «липких» сессий.

Как используется прилипание в контексте сеансов пользователей

Прилипание запросов может применяться в самых разных случаях. Рассмотрим самые распространённые.

  • Интернет-магазины

    • Корзины для покупок: если привязать сеанс к определённому узлу сервиса, то все действия, связанные с корзиной (добавление или удаление товаров) будут направляться на один и тот же сервер. Это поможет предотвратить потерю товаров или несоответствие содержимого корзины.

    • Оформление заказа: поддержание сеанса с одним и тем же сервером при оформлении заказа гарантирует бесперебойную обработку информации об оплате и доставке без перерывов или потери данных. 

  • Игровые приложения

    • Многопользовательские онлайн-игры: поддержание постоянной сессии и своевременное обновление данных для каждого игрока имеют решающее значение. Прилипание запросов обеспечивает последовательное управление состоянием игры, прогрессом игрока и взаимодействиями.

  • Финансовые услуги

    • Онлайн-банкинг: привязанный к определённому узлу сервиса сеанс гарантирует, что все транзакции и действия пользователя последовательно выполняет один и тот же сервер. Это крайне важно для поддержания безопасности и целостности данных при конфиденциальных финансовых операциях.

    • Торговые платформы: прилипание запросов помогает эффективно управлять сеансами пользователей и гарантирует, что торговые данные, действия пользователя и обновления рынка последовательно обрабатываются и отображаются.

Какие есть альтернативы прилипанию запросов

Многие современные приложения используют конструкции без состояния и/или распределённое управление сеансами пользователей, чтобы устранить ограничения, связанные с прилипанием запросов:

  1. Приложения без состояния (stateless): данные пользователя в рамках сеанса могут храниться на стороне клиента (например, в файлах cookie или локальном хранилище) или в централизованном хранилище сеансов, таком как Redis или база данных. Такой подход позволяет любому узлу в группе балансировки обрабатывать любой запрос, что повышает надёжность сервиса.

  2. Распределённое кеширование: для централизованного хранения данных сеанса пользователя можно использовать такие инструменты, как Redis, Memcached или другие распределённые кеши. Эти данные доступны для чтения и записи всем узлам в группе балансировки. Такой метод позволяет горизонтально масштабировать сервис.

  3. Глобальные балансировщики нагрузки (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)


  1. kulaginds
    26.06.2025 14:46

    Не пробовали angie?


  1. Licemery
    26.06.2025 14:46

    Ах, ну конечно же миллисекунды задержки им теряют клиентов, а мерзкая капча при каждом входе на сайте, где надо извилины напрягать, а не просто ждать - нет. Преждевременная оптимизация это, заниматься крохоборством, игнорируя слона в комнате. Не говоря уж о том, что вообще-то унизительно доказывать, что не верблюд.


  1. olku
    26.06.2025 14:46

    А как прилипание дружит с Кубером? Интересно практическое применение, а то статья напоминает переработанную доку Nginx.