Как правильно запустить Python (micro)сервис для работы с NATS?
Что у меня есть сейчас:
- Сервис записи в БД (валидирует входящие данные, ищет
дубликаты)[
FastAPI] - Сервис конвертации и преобразования данных
(забирает данные из сервиса БД и создаёт
Celeryтаски)[FastAPI] - Сервис с
Celeryна борту + к немуRedisв качестве брокера - Сервис фронта (не я пилил, хз что там), который взаимодействует с первыми
двумя апишками [
NestJS] - Собственно сам фронт [
Angular]
Соответственно, как пример: После записи данных в базу я из сервиса БД дёргаю эндпоинты сервиса конвертации, что бы инициировать конвертацию в форматы по умолчанию. Если нужен другой формат, то пользователь жмёт нужные кнопки из интерфейса, а сервис фронта уже дёргает ручки сервиса конвертации. Всё это надо логировать, в будущем понадобится сквозная трассировка и т.п. При обрастании функционалом (и следовательно, увеличении кол-ва сервисов) увеличится взаимодействие, повысится связность. Придётся обрабатывать в коде случаи отваливания части сервисов и т.п. Как я понимаю, если перейти с ручного дёрганья эндпоинтов на событийную модель взаимодействия сервисов, можно будет привести систему к следующему виду:
- Сервис записи в БД
- Сервис ручной инициации конвертации, преобразования - апишка без логики создания задач, просто эндпоинты, создающие эвенты по типу - была запрошена конвертация в такой-то формат.
- Сервис конвертации
- Сервис преобразования данных
- Сервис фронта
- Фронт
Тогда, имея Pub/Sub шину в виде NATS, я могу всё сделать следующим образом. Пользователь отправляет данные в сервис БД, после валидации данные пишутся в БД и в NATS subject пушится сообщение (эвент) с информацией о новых данных. На этот эвент у нас подписаны сервисы конвертации и сервис фронта. Соответственно, в сервисе конвертации должна начаться конвертация, а у пользователя на страничке мигнуть лампочка - данные записаны, всё ок.
Сервис конвертации, при начале конвертации создаёт эвент - началось. На этот эвент подписан фронт - пользователю мигает соответствующая лампочка и дальше в том же духе. С первого взгляда всё просто, но вот при дальнейшем обдумывании у меня возникают вопросы.
При создании REST сервисов я использую FastAPI, для него существует настроенный Docker образ, в котором достаточно положить приложение в определенное место и всё заверте... Упакованные в такие образы апишки способны обслуживать огромное количество пользователей. Однако, из того, что я описал выше видно, что не все сервисы - REST API. Сервисы конвертации и преобразования данных - по факту просто скрипты вынесенные в отдельные контейнеры и я не имею понятия, как грамотно это всё организовать. Если я напишу скрипт, который просто в бесконечном цикле ожидает сообщения (эвенты) в subject'е NATS, то как правильно потом это всё запускать в контейнере, что бы скрипт мог обслуживать сразу n-ное количество эвентов. Нужно ли это вообще? Есть ли примеры, статьи, видео по теме? Всё что нахожу - очередные "Как запустить Flask в докере!"
Как одно из предположений:
В сервисах конвертации и преобразования создавать всё те же Celery таски и держать параллельно сервис с его воркерами, в которых так же пушить в NATS эвенты с инфой о ходе процесса.
Ответы (1 шт):
Чтобы можно было одновременно обрабатывать несколько сообщений из очереди NATS достаточно запустить несколько процессов, которые слушают определенную очередь и находятся в одной группе:
NATS посылает каждое сообщение одному слушателю из группы. Собственно слушатель и будет бесконечно в цикле ждать сообщения из NATS.
Теперь о вариантах как одновременно запускать нескольких слушателей:
- несколько контейнеров
- несколько процессов в контейнере
- python-nats (c asyncio под капотом)
Несколько контейнеров
Конкретный способ зависит от того, что используется для оркестрации. Если docker swarm, то задайте для сервиса количество слушателей с помощью параметра --replicas. Если используете k8s, то используйте ключ spec.replicas в определении Deployment. Если запускаете вручную, то просто делаете docker run ... необходимое количество раз.
Еще одна возможность - использовать docker-compose.
Несколько процессов внутри контейнера
Можно сделать и так хоть и не рекомендуется, так как подход имеет очевидные минусы:
- усложняется устройство самого контейнера (т.к. нужно процессы запускать, за ними следить, перезапускать при сбое и т.д.)
- при сбое контейнера или хоста на котором он запущен вся обработка останавливается
Смотрите на способы запуска нескольких процессов.
python-nats (c asyncio под капотом)
Тут точно не подскажу, т.к не использовал, а документация скудная. Точно будет один слушатель и в лучшем случае будет некий естественный параллелизм, степень которого будет определяться тем, какую долю времени задачи конвертации будут проводить в ожидании ввода-вывода. Т.е. когда выполнение обработчика сообщения от NATS блокируется на вводе-выводе, то python-nats может (теоретически, это то то чего я точно не знаю, по-этому я говорю о "лучшем случае") взять новое сообщение и его обрабатывать. Тут нужно протестировать поведение python-nats (или задать вопрос).