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:
| Структура | Ключ | Назначение |
|---|---|---|
| List | queue:<name> | Рабочие очереди. Client делает LPUSH, server — BRPOP |
| Sorted Set | schedule | Отложенные задачи. Score = Unix timestamp выполнения |
| Sorted Set | retry | Задачи на повтор. Score = время следующей попытки |
| Sorted Set | dead | Задачи, исчерпавшие все попытки (max 10 000, хранятся 6 месяцев) |
| Set | queues | Индекс имён всех очередей |
| Set | processes | ID активных 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
- Mike Perham, How does Sidekiq work?
- Sidekiq Wiki
- Sidekiq Redis Data Model