Stateless vs Stateful: почему ваша стратегия развертывания зависит от этого

У вас запущено два экземпляра одного и того же приложения. Пользователь отправляет запрос. Какой экземпляр его обработает? Если ответ — «любой, без разницы», вы имеете дело с stateless-приложением. Если ответ — «тот же самый, что обрабатывал предыдущие запросы этого пользователя», перед вами stateful-приложение.

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

Что делает приложение stateless

Stateless-приложение не запоминает ничего между запросами. Каждый запрос независим. Приложение получает входные данные, обрабатывает их, возвращает результат и забывает о взаимодействии.

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

Типичные примеры stateless-приложений:

  • REST API, которые читают и пишут в общую базу данных
  • Сервисы обработки изображений, преобразующие файлы и возвращающие результат
  • Сервисы аутентификации, проверяющие токены без хранения сессионных данных
  • Серверные веб-страницы, хранящие всё состояние в куках или параметрах URL

Stateless-приложения наиболее просты в развертывании, масштабировании и восстановлении. Вы можете запускать несколько версий одновременно, постепенно переключать трафик и мгновенно возвращаться назад, если что-то пошло не так.

Что делает приложение stateful

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

Рассмотрим пример интернет-магазина. Пользователь добавляет товары в корзину. Данные корзины хранятся на сервере, обработавшем этот запрос. Если следующий запрос уйдет на другой сервер, тот ничего не будет знать о корзине. Пользователь увидит пустую корзину. Это и есть проблема stateful-приложений.

Stateful-приложения хранят состояние в нескольких местах:

  • В памяти сервера приложений (данные сессии)
  • В локальных файлах на сервере (загруженные файлы, временные данные)
  • Во встроенных базах данных, работающих вместе с приложением
  • В кэшах уровня приложения, которые не разделяются между экземплярами

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

Как stateless-приложения упрощают развертывание

Развертывание stateless-приложения — простая задача. Вы запускаете новые экземпляры с новой версией рядом со старыми. Балансировщик постепенно направляет трафик на новые экземпляры. Как только весь трафик переключен, вы останавливаете старые экземпляры.

Если в новой версии обнаружена ошибка, вы обращаете процесс: направляете трафик обратно на старые экземпляры. Никакой миграции данных, изменений схемы или восстановления сессий. Откат — это просто перенаправление трафика.

Масштабирование работает по той же логике. Нужно больше мощности? Запустите еще экземпляров. Трафик упал? Удалите лишние. Нет необходимости перераспределять данные. Каждый экземпляр идентичен и одноразов.

Именно эта простота делает stateless-дизайн предпочтительным в микросервисной архитектуре. Каждый сервис можно развертывать независимо, не беспокоясь о том, что «помнят» другие экземпляры.

Почему stateful-приложения требуют больше внимания

Развертывание stateful-приложения означает работу с данными, которые нельзя потерять или повредить. Если приложение хранит сессии в памяти, замена всех экземпляров сразу приведет к выходу всех активных пользователей из системы. Если приложение пишет в локальные файлы, эти файлы необходимо перенести, или новая версия должна быть совместима со старым форматом данных.

Распространенные стратегии развертывания stateful-приложений:

Вынести состояние за пределы приложения. Храните сессии в общем кластере Redis или в базе данных. Загруженные файлы храните в объектном хранилище, например S3. Это превращает приложение в stateless с точки зрения развертывания. Приложение можно свободно заменять, потому что состояние находится в другом месте.

Использовать липкие сессии (sticky sessions). Настройте балансировщик так, чтобы он направлял одного и того же пользователя на один и тот же экземпляр. Это работает, но создает проблемы при развертывании. Вы не можете вывести трафик из экземпляра, не прервав работу активных пользователей. Постепенные обновления (rolling updates) замедляются, так как нужно дожидаться истечения сессий.

Использовать StatefulSets или операторы. Kubernetes StatefulSets и операторы баз данных берут на себя сложность развертывания stateful-приложений. Они гарантируют, что экземпляры запускаются и останавливаются в определенном порядке, данные сохраняются, а сетевые идентификаторы стабильны. Но для этого нужно понимать, как работает ваше конкретное stateful-приложение.

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

Реальное влияние на вашу команду

Различие между stateless и stateful влияет не только на технические решения. Оно формирует то, как работает ваша команда.

Stateless-приложения позволяют выполнять быстрые и частые развертывания. Любой член команды может развернуть новую версию, не беспокоясь о потере данных. Откаты безопасны и быстры. Это снижает страх перед деплоем и стимулирует более частые и мелкие релизы.

Stateful-приложения требуют тщательной координации. Развертывания необходимо планировать с учетом миграции данных, обработки сессий и совместимости при откате. Команды часто разрабатывают особые процедуры для stateful-сервисов: окна для деплоя, шлюзы утверждения, ручные проверки. Это замедляет поставку.

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

Краткий чек-лист перед следующим развертыванием

Прежде чем планировать развертывание, задайте себе эти вопросы:

  • Хранит ли приложение какие-либо данные локально, которые должны пережить перезапуск?
  • Хранятся ли пользовательские сессии в памяти или в общем внешнем хранилище?
  • Может ли любой экземпляр обработать любой запрос, или маршрутизация зависит от того, на каком экземпляре находятся данные?
  • Если вы выполните откат к предыдущей версии, будет ли формат данных по-прежнему читаем?
  • Можете ли вы запустить две версии одновременно без конфликтов данных?

Если вы ответили «нет» на все вопросы о локальном хранении и зависимости от сессий, у вас stateless-приложение. Развертывайте свободно. Если вы ответили «да» хотя бы на один из них, вам нужно спланировать управление состоянием до того, как проектировать стратегию развертывания.

Вывод

Stateless-приложения дают вам свободу. Stateful-приложения накладывают ограничения. Ошибка в том, чтобы относиться к ним одинаково. Прежде чем проектировать пайплайн развертывания, поймите, где живут ваши данные. Если они живут внутри экземпляра приложения, ваша стратегия развертывания должна это учитывать. Если они живут снаружи, вы можете считать приложение одноразовым и развертывать с уверенностью.