Stateful -> Stateless
Видела ситуации, когда изначально сервис всегда поднимался в одном экземпляре, о масштабировании речи не было, в нем накрутили stateful-штук (шедулеры, очереди), но со временем возникала необходимость в масштабировании, и нужно было переделывать.
Как stateful переводят в stateless?
➡️ Перенос временного состояния во внешнее хранилище (например, Redis), тогда несколько копий микросервиса могут обращаться туда
➡️ Вынос работы локальных шедулеров в общее хранилище (Quartz в кластерном режиме с общей БД, отдельный сервис-планировщик, публикующий задачи в Кафку)
➡️ Перенос состояния процесса и результатов промежуточных шагов в БД
Если выполняется долгий процесс и все состояние хранится в памяти, то его промежуточные шаги можно сохранять в БД.
Для сложных процессов используют workflow engine, например Temporal или Camunda, но я к сожалению с ними не работала.
Бывает, что внутри сервиса написали что-то вроде очереди, которая хранится в памяти конкретно этого экземпляра, и горизонтально масштабироваться с ней затруднительно.
Если у сервиса есть БД, и задания в очереди тесно связаны с записями в БД (и их не так много, чтобы сильно нагрузить БД) то логичным решением будет вариант с хранением задач и их статусов в БД.
Для этого нам понадобится таблица для задач с полями:
id
payload //бизнес-данные или ссылки на них в других таблицах
status ('NEW', 'PROCESSING', 'SUCCESS', 'FAILED')
created_at
updated_at
locked_until
error_text
В одной транзакции обработчик:
- выбирает новую задачу через SELECT ... FOR UPDATE SKIP LOCKED, чтобы несколько экземпляров обработчиков не мешали друг другу
- устанавливает взятой задаче статус PROCESSING и коммитит транзакцию
Затем обрабатывает задачу без блокировок и устанавливает ей статус SUCCESS. При ошибке задача переводится в FAILED, RETRY или обратно в NEW (в зависимости от политики повторных попыток).
Если обработчик упал до того, как завершил обработку, задача останется в статусе PROCESSING. Нужно предусмотреть, чтобы по истечении какого-то времени такие задачи снова были взяты в работу наравне с NEW. Для этого можно сразу заполнять поле locked_until - время, до которого задача считается принадлежащей этому обработчику.
➡️ Перестройка процесса с использованием очереди RabbitMQ или лога Кафки
Есть подход с топиком Кафки для себя же: сервис сам выступает продюсером и отправляет сообщения в Кафку, и сам же читает и обрабатывает их.
Но тут есть над чем подумать:
🔴Идемпотентность обработчика: что будет, если обработчик повторно вычитает одну и ту же таску из топика.
Возможно, в повторной обработке нет ничего страшного. Например, мы повторно сформируем отчет по клиенту или повторно отправим снимок обновленных данных другому сервису.
Если же повторно обрабатывать нельзя, то придется как-то фильтровать дубли, тут обычно появляется запись в БД и фильтрация по уникальным идентификаторам (подробнее в посте про идемпотентный консьюмер).
🔴Откуда берутся данные для отправки в Кафку.
Если данные для задач берутся из БД, то как будто бы мы сами усложняем себе жизнь: БД -> polling -> Kafka -> тот же сервис 🤔
Если Kafka нужна только для того, чтобы сервис обработал записи из собственной БД, то возможно проще сразу использовать таблицу с очередью задач в БД, описанную выше.
При этом в stateless-сервисе могут остаться какие-то внутренние технические очереди. Главное, что память конкретного экзепляра микросервиса не является единственным местом, где хранятся бизнес-задачи.
Конечно, это не все шаги, могут быть вопросы с сессиями пользователей, локальными файлами, счетчиками, WebSocket-подключениями и др.
Поделитесь, был ли у вас опыт перевода stateful в stateless или спрашивали ли вас про stateless на собесах?