Задача 10 из 10
Планировщик отложенных задач
Спроектируйте планировщик отложенных и периодических задач: «напомнить через 3 дня», «списать подписку первого числа», «отправить отчёт каждый час» — на кластере из нескольких инстансов.
Функциональные требования
- Поставить задачу на конкретное время
- Периодические задачи по расписанию
- Отмена и перенос задачи
- Гарантия, что задача в итоге выполнится
Нефункциональные требования
- 50 млн запланированных задач одновременно
- Задача запускается не позже чем через 5 секунд после срока
- Задача не должна выполниться дважды
- Планировщик работает на 5 инстансах, любой может упасть в любой момент
- Пик: 100 000 задач приходятся на одну и ту же секунду — первое число месяца
Нарисуйте архитектуру
Не гонитесь за красотой: на собеседовании от схемы нужно, чтобы по ней было видно путь запроса и где лежат данные. Сначала нарисуйте сами — эталон и разбор ниже.
Блок из палитры — добавить. Тащите мышкой или пальцем. Двойной клик по названию — переименовать. «Связь» — щёлкнуть по одному блоку, потом по другому: получится стрелка.
Схема сохраняется в этом браузере — вкладку можно закрыть и вернуться позже.
1 С чего начать, если ступор
- Наивное «SELECT ... WHERE run_at <= now()» на пяти инстансах даст пятикратное выполнение.
- Нужен способ забрать задачу так, чтобы её гарантированно не забрал никто другой.
- Exactly-once в распределённой системе не бывает — обсудите at-least-once плюс идемпотентность.
- Инстанс забрал задачу и умер: кто и через сколько её подберёт.
- Что произойдёт в полночь первого числа, когда 100 000 подписок наступят одновременно.
2 Эталонная схема
Задачи лежат в БД, а забирают их пачками с SELECT ... FOR UPDATE SKIP LOCKED — тогда пять инстансов разбирают разные задачи, а не дерутся за одни и те же. Выполнение отделено от планирования очередью, поэтому долгая задача не тормозит расписание.
Это один из рабочих вариантов, а не единственно верный. Если у вас иначе, но вы можете объяснить почему — на собеседовании это ровно то, что нужно.
3 Разбор: что должно прозвучать
Забор задач без дублей
- SELECT ... FOR UPDATE SKIP LOCKED: каждый инстанс получает свою пачку, никто не ждёт чужую блокировку
- Альтернатива — аренда: UPDATE ... SET owner, lease_until WHERE owner IS NULL, забираем только то, что обновили
- «Прочитал список, потом обновил» без блокировки — та самая гонка на пять инстансов
Ровно один раз не бывает
- Между «задача выполнена» и «отметка в БД» всегда есть окно, где процесс может упасть
- Честный ответ: at-least-once плюс идемпотентный обработчик с ключом выполнения
- Если обработчик неидемпотентен — нужна отметка «уже выполнено» в той же транзакции, что и эффект
Падение исполнителя
- Аренда с TTL: истекла — задача снова видна другим инстансам
- Долгая задача должна продлевать аренду, иначе её подберут вторым исполнителем
- Счётчик попыток и отправка в «карантин» после N неудач
Пик на границе времени
- 100 000 задач на одну секунду — это всплеск, который нельзя выполнить мгновенно
- Джиттер при планировании размазывает нагрузку по окну
- Отставание в этот момент нормально, если требование «не позже 5 секунд» относится к запуску, а не к завершению
Объём и индексы
- 50 млн задач: индекс по (статус, run_at), выборка только по ближайшему окну
- Выполненные задачи надо уносить из горячей таблицы — партиционирование или архив
- Опрос БД каждую секунду пятью инстансами тоже стоит денег: пачки и разумный интервал
Куда копать дальше, если спросят
- Периодические задачи и пропущенные окна: догонять или пропускать
- Приоритеты задач и защита мелких задач от больших рассылок
- Наблюдаемость: сколько задач просрочено прямо сейчас — главная метрика планировщика
- Выбор лидера: когда он действительно нужен, а когда достаточно SKIP LOCKED
Это была последняя задача
Теория к ним — карточками в боте: кэш, очереди, репликация, шардирование и транзакции.