После масштабирования веб-сервиса пользовательская сессия начинает пропадать при каждом втором запросе. Какой механизм маршрутизации мог породить такое поведение?
Такое поведение обычно возникает из-за сессионной привязки на балансировщике, когда запросы одного клиента направляются на конкретный экземпляр сервиса, но привязка нарушается после масштабирования или смены экземпляра. Если состояние сессии хранится только в памяти процесса, запрос, попавший на другой экземпляр, не найдёт это состояние.
Надёжнее хранить сессии во внешнем общем хранилище или использовать самодостаточные токены. Привязка клиента к экземпляру может временно решить проблему, но создаёт неравномерную нагрузку и усложняет отказоустойчивость.
Ранние веб-приложения часто хранили пользовательское состояние в памяти конкретного процесса. При работе одного сервера это было просто и быстро, но горизонтальное масштабирование потребовало распределять запросы между несколькими экземплярами.
Балансировщики получили механизм session affinity, или sticky sessions, чтобы сохранять совместимость с такими приложениями без немедленной переработки их модели состояния. Позже чаще стали применять внешние хранилища сессий и stateless-подход, поскольку они лучше соответствуют динамической инфраструктуре.
Пользователь отправляет несколько запросов к одному виртуальному адресу, а балансировщик распределяет их между экземплярами приложения. Если первый запрос создал сессию на экземпляре A, а следующий попал на экземпляр B, приложение может считать пользователя новым или вернуть ошибку авторизации.
Проблема особенно заметна при масштабировании, перезапуске экземпляров, изменении таблицы маршрутизации или отказе узла. Неверное решение может привести к случайным выходам из системы, неравномерной загрузке и потере доступности при отказе единственного экземпляра, к которому была привязана большая группа клиентов.
При сессионной привязке балансировщик определяет идентификатор клиента или устанавливает специальный cookie и использует его для выбора одного экземпляра. Варианты реализации включают cookie-based affinity, привязку по исходному IP-адресу и детерминированное хеширование некоторого признака запроса.
Привязка не означает, что экземпляр становится надёжным хранилищем состояния. При его отказе сессия всё равно потеряется, если данные не были скопированы во внешнее хранилище. Кроме того, клиенты за общим NAT могут ошибочно выглядеть как один источник при привязке по IP.
Основной вариант для масштабируемого приложения — вынести состояние сессии в общее хранилище, например распределённый кэш или базу данных. Тогда любой экземпляр может обработать запрос, а балансировщик выбирает экземпляр без необходимости сохранять привязку.
Другой вариант — использовать stateless-сессию, когда необходимые утверждения находятся в подписанном токене клиента. Это уменьшает обращения к хранилищу, но усложняет немедленный отзыв токена, увеличивает размер запросов и требует аккуратной защиты ключей и содержимого.
У sticky sessions есть практические компромиссы: они могут снизить количество обращений к хранилищу, но ухудшают балансировку и усложняют автоматическое масштабирование. Время жизни привязки должно учитывать жизненный цикл сессии, а поведение при недоступности экземпляра должно явно предусматривать перенаправление клиента и повторную инициализацию состояния.
В интернет-магазине после увеличения числа реплик часть пользователей периодически теряла корзину. Сначала рассматривались два варианта: включить привязку по cookie или перенести корзину и сессию во внешний кэш.
Привязка по cookie была бы быстрой и почти не требовала изменений приложения, но создавала риск перегрузки отдельных реплик и не решала потерю данных при отказе экземпляра. Внешний кэш потребовал изменения слоя работы с сессиями и контроля времени жизни записей, зато позволил любому экземпляру обработать запрос.
Выбрали внешний кэш с ограниченным временем хранения и удалили зависимость от локальной памяти процесса. После этого масштабирование перестало менять доступность корзины, а отказ отдельной реплики не приводил к массовому завершению пользовательских сессий.
Нет. Они лишь повышают вероятность отправки запроса на тот же экземпляр. При перезапуске, отказе узла, истечении привязки или изменении конфигурации балансировщика запрос может попасть на другой экземпляр. Если состояние было только локальным, проблема сохранится.
Многие пользователи могут выходить в интернет через один NAT, прокси или корпоративный шлюз. В этом случае они будут направляться к одному экземпляру, создавая горячую точку. Кроме того, мобильный клиент может менять сеть и исходный адрес, из-за чего привязка перестанет работать.
Оно должно быть доступно всем репликам, иметь понятную политику времени жизни записей и выдерживать требуемую нагрузку и отказные сценарии. Важно определить поведение при временной недоступности хранилища: безопаснее отказать запросу, чем незаметно создать новую пустую сессию и нарушить бизнес-операцию. Также нужно учитывать согласованность, защиту данных и возможное увеличение задержки по сравнению с локальной памятью.