Consul, Lusnoc, Rufus и фоновые задачи
В прошлой статье я обещал рассказать о конкретном использовании распределенных блокировок с использованием нашей собственной Ruby-библиотеки Lusnoc. Про это и поговорим.
О чем речь?
В большом количестве проектов часто требуется выполнять "регулярные задачи". Вот небольшой список регулярных задач, который я собрал по нескольким нашим проектам:
- обновление курсов валют, каких-то статических списков, баз из чужих источников или проверка обновлений
- регулярная очистка старых данных в базе или какая либо их пометка
- ежедневная/еженедельная/ежемесячная рассылка
- всё что раньше принято было класть в
cron
и управлять через whenever
Некоторые задачи можно выполнить несколько раз подряд безо всякого вреда, но такие как рассылка еженедельных дайджестов должны выполняться строго один раз.
Для наших любимых "бэкграунд процессоров" sidekiq и delayed job(❤️) уже написаны гемы решающие эту проблему тем или иным способом. Но, как обычно, хочется собственного 🚲 обобщенного решения ведь у нас есть как проекты с sidekiq
так и с delayed_job
и ходят легенды про обычные скрипты, оставшиеся в заброшенных уголках крона...
Структура стандартного проекта
Для того чтобы всё это гармонично работало у нас есть типовая структура информационной системы:
- web - сервис, исполняющий Rails приложение
- jobs - сервис который крутит
sidekiq
илиdelyaed job
или что-то подобное - assistant - сервис, который "живет постоянно" и держит соединение с
RabbitMQ
илиKafka
- 👉scheduler - сервис, который ставит регулярные задачи
- вокруг еще есть редисы, брокеры и базы, но они вроде как не входят в "проект"
Для реализации сервиса scheduler
мы используем гем Rufus который исправно выполняет свои функции на нескольких проектах.
У Rufus::Scheduler
есть куча параметров таких как управление многопоточностью, обработка ошибок и многое другое, но мы его используем не для выполнения задач, а для постановки - выполнение происходит в других сервисах.
Находим себе проблемы и побеждаем их!
В RNDSOFT все сервисы (по крайней мере в современных проектах) уже запакованы в контейнеры и запущены в нескольких экземплярах для обеспечения отказоустойчивости. И именно тут кроется проблема - если мы запустим 2 контейнера scheduler
то все задачи будут запущены 2 раза...
И здесь нам на помощь приходит мьютекс, да не простой, а межпроцессный! О его реализации я рассказывал в прошлой статье. Идея простая - назовем право ставить задачи ресурсом и тогда можно применить классическое эксклюзивное владение:
Работаем пока владеем ресурсом. В случае любых ошибок, таймаутов, потери владения - завершаемся и другой процесс, ожидающий блокировку, берёт на себя работу. Если при запуске не смогли завладеть ресурсом - тоже завершаемся. При любом завершении наш движок контейнеризации Docker
перезапустит контейнер.
Заключение
В жизни скрипт немного сложнее, лучше затюнен Rufus
, есть обработка сигналов семейства SIGTERM
и надежнее обработка ошибок - ведь тут самое важное быстрее завершиться при любом отклонении от нормы.
Задачи тоже бывают разные - мы рассмотрели одну из самых очевидных, но у меня есть гораздо более извращенный пример, когда рабочий поток порождался в каждом (КАЖДОМ, Карл!) процессе рельсы (имеется ввиду prefork модель как в Unicorn
и Passenger
) и эти процессы через Lusnoc боролись за право выполнять эксклюзивное действие. Так себе идея, но работает 💪