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 боролись за право выполнять эксклюзивное действие. Так себе идея, но работает 💪