← все задачи

Задача 10 из 10

Планировщик отложенных задач

Спроектируйте планировщик отложенных и периодических задач: «напомнить через 3 дня», «списать подписку первого числа», «отправить отчёт каждый час» — на кластере из нескольких инстансов.

Уровень: Продвинутый На собеседовании: 45–60 минут координацияаренда и блокировкиat-least-once

Функциональные требования

  • Поставить задачу на конкретное время
  • Периодические задачи по расписанию
  • Отмена и перенос задачи
  • Гарантия, что задача в итоге выполнится

Нефункциональные требования

  • 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

Это была последняя задача

Теория к ним — карточками в боте: кэш, очереди, репликация, шардирование и транзакции.