Sidekiq: архитектура

Предпосылки: Redis Lists, очереди в Redis, message queues, фоновая очередь на LIST, базовое знание Rails.

Следующая тема: жизненный цикл job

Простая очередь из предыдущей практики работает: LPUSH добавляет задачу, BRPOP забирает, воркер обрабатывает. Но цикл обслуживает один тип задачи с одним поведением. Добавь resize картинок — нужен второй цикл или роутинг по типу задачи. Добавь retry при ошибках — воркер ловит исключения, считает задержку, пишет в sorted set. Добавь graceful shutdown — обработчики сигналов. Каждое дополнение — паттерн, который уже знаком из предпосылок, но собирать их руками — строить собственный фреймворк фоновых задач. Sidekiq — готовый фреймворк, который объединяет эти паттерны в одну систему.

Терминология

Job (задача) — единица работы в Sidekiq. Это Ruby-класс с методом perform и одновременно JSON-хеш в Redis. Класс определяет что делать, JSON в Redis — конкретный вызов с аргументами.

class SendEmailJob
  include Sidekiq::Job
 
  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

Термин worker встречается в старом коде и документации — это устаревший синоним job. В точной терминологии Sidekiq: class = job class, выполняющий поток = processor, процесс ОС = process.

Queue (очередь) — Redis LIST с ключом queue:<имя>. У каждого job класса есть очередь по умолчанию (default), которую можно переопределить через sidekiq_options queue: 'critical'.

Три роли

Sidekiq реализует point-to-point модель: несколько consumers конкурируют за задачи из общей очереди. Система состоит из трёх ролей:

Client — Rails-приложение, которое ставит задачи. Вызов SendEmailJob.perform_async(user_id) сериализует аргументы в JSON, прогоняет через client middleware (цепочку обработчиков, которые могут дополнить или отклонить задачу перед отправкой) и выполняет LPUSH в Redis.

Broker — Redis. Хранит очереди, расписание, retry, метаданные процессов. Redis выбран потому, что Sidekiq использует знакомые структуры: LIST для очередей, Sorted Set для отложенных задач и retry, Set и Hash для метаданных. Это те же паттерны из очередей в Redis, собранные вместе.

Server — процесс Sidekiq, который забирает задачи из Redis и выполняет их в потоках. Один сервер обрабатывает задачи параллельно: пока один поток ждёт ответа от SMTP-сервера, другой обрабатывает картинку. Это возможно благодаря тому, что потоки Ruby освобождают GVL при блокирующем I/O (подробнее — Ruby concurrency).

В терминах temporal decoupling: client и server работают независимо. Rails-приложение кладёт задачу и продолжает обслуживать HTTP-запрос. Sidekiq-процесс обрабатывает задачу, когда готов — через миллисекунды, минуты или часы.

Данные в Redis

Каждая роль работает с конкретными ключами в Redis:

СтруктураКлючНазначение
Listqueue:<name>Рабочие очереди. Client делает LPUSH, server — BRPOP
Sorted SetscheduleОтложенные задачи. Score = Unix timestamp выполнения
Sorted SetretryЗадачи на повтор. Score = время следующей попытки
Sorted SetdeadЗадачи, исчерпавшие все попытки (max 10 000, хранятся 6 месяцев)
SetqueuesИндекс имён всех очередей
SetprocessesID активных Sidekiq-процессов
Hash<identity>Метаданные процесса: hostname, pid, concurrency, busy, beat

schedule и retry — это delayed queue на Sorted Set: score задаёт момент, когда задача должна переместиться в рабочую очередь. dead — это Dead Letter Queue: задачи, которые не удалось обработать после всех попыток.

Redis для Sidekiq настраивается с политикой noeviction (при исчерпании памяти Redis возвращает ошибку на запись вместо удаления ключей). Если бы политика была allkeys-lru (удаление наименее используемых ключей для освобождения памяти), Redis мог бы удалить ключ очереди с тысячами задач, чтобы освободить место для нового.

Устройство серверного процесса

Внутри серверного процесса — несколько компонентов, каждый со своей задачей:

flowchart TB
    L["<b>Launcher</b><br>управляет жизненным циклом"]
    M["<b>Manager</b><br>управляет набором Processor-потоков"]
    P1["Processor<br>BRPOP → deserialize → perform"]
    P2["Processor"]
    Pn["...<br>(concurrency, default 5)"]
    Po["<b>Poller</b><br>проверяет schedule и retry sorted sets"]
    H["<b>Heartbeat</b><br>обновляет метаданные в Redis (~10 сек)"]

    L --> M & Po & H
    M --> P1 & P2 & Pn

Processor — рабочий поток. Выполняет цикл: забрать задачу из Redis (BRPOP), десериализовать JSON, выполнить perform. Количество Processor-ов определяется настройкой concurrency (по умолчанию 5 начиная с Sidekiq 7).

Poller — фоновый поток, который периодически проверяет sorted sets schedule и retry. Если score задачи ≤ текущему времени, Poller перемещает её в рабочую очередь (LPUSH). Это тот же паттерн отложенной очереди, но реализованный как отдельный поток внутри процесса.

Heartbeat — поток, который каждые ~10 секунд обновляет метаданные процесса в Redis (hostname, pid, количество занятых потоков). По этим данным Web UI показывает состояние системы, а Pro-версия определяет мёртвые процессы для восстановления задач.

Три способа поставить задачу

# Выполнить как можно скорее: LPUSH в queue:<name>
SendEmailJob.perform_async(user_id)
 
# Выполнить через 5 минут: ZADD в schedule (score = Time.now + 300)
SendEmailJob.perform_in(5.minutes, user_id)
 
# Выполнить в конкретное время: ZADD в schedule (score = timestamp)
SendEmailJob.perform_at(2.hours.from_now, user_id)

perform_async кладёт задачу напрямую в рабочую очередь — Processor заберёт её при следующем BRPOP. perform_in и perform_at кладут задачу в sorted set schedule с нужным timestamp. Когда время наступит, Poller переместит задачу в рабочую очередь.


Архитектура — карта системы: три роли, шесть структур данных в Redis, несколько компонентов внутри процесса. Но как конкретно задача проходит путь от perform_async до завершения perform? Это описано в жизненном цикле задачи.


Следующая тема: жизненный цикл job

Sources