Concurrency и масштабирование

Предпосылки: Sidekiq: дизайн задач, Ruby concurrency, потоки.

Дизайн задач | Тестирование и практики

Задачи спроектированы: маленькие, атомарные, идемпотентные. Их стало десять тысяч в час, а один Sidekiq-процесс с пятью потоками обрабатывает 30 в минуту. Пользователи ждут email 15 минут. Как обработать больше?

Потоки и GVL

Sidekiq использует потоки для параллельной обработки задач. Настройка concurrency определяет количество Processor-потоков:

# config/sidekiq.yml
:concurrency: 5

Ruby-потоки работают под GVL, но для I/O-bound задач это не проблема: GVL освобождается при блокирующем I/O, и потоки эффективно чередуют CPU и O overlap. Для CPU-bound задач потоки не дают ускорения — здесь помогут дополнительные процессы.

Concurrency: сколько потоков?

Больше потоков — больше I/O overlap, но больше потребление ресурсов:

Память. Каждый поток имеет свой стек и может держать свои Ruby-объекты. 50 потоков потребляют заметно больше памяти, чем 10.

Пул соединений к базе. Каждый поток может одновременно выполнять запрос к PostgreSQL — значит, нужно столько же соединений. Правило: database.yml pool ≥ concurrency.

# database.yml
production:
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

Redis connections. У Sidekiq есть не только соединения рабочих Processor-потоков, но и служебные подключения для внутренних операций. Поэтому суммарное число Redis-соединений всегда больше concurrency, а точное значение зависит от версии, Capsules и включённых Pro-механизмов.

Типичный подход: для I/O-bound задач 10–25 потоков, для CPU-bound — мало потоков (2–5), зато несколько процессов.

Несколько процессов: CPU-параллелизм

Каждый Ruby-процесс имеет свой GVL. Два процесса = два GVL = два ядра CPU работают одновременно. Это способ обойти ограничение GVL для CPU-bound нагрузки.

# Два процесса, каждый с 5 потоками
bundle exec sidekiq -c 5  # процесс 1
bundle exec sidekiq -c 5  # процесс 2

Запуск нескольких процессов — через systemd, Procfile, docker-compose или Kubernetes replicas. Sidekiq не управляет дочерними процессами: каждый процесс независим, со своим подключением к Redis.

Дополнительное преимущество: изоляция. Если один процесс убит OOM-killer — остальные продолжают работать.

Приоритеты очередей

Не все задачи одинаково важны. Платёж важнее аналитики, email важнее resize аватара. Sidekiq обрабатывает очереди по приоритету, и есть два режима:

Strict — очереди проверяются в фиксированном порядке:

:queues:
  - critical
  - default
  - low

Sidekiq забирает задачи из critical, пока она не опустеет, потом из default, потом из low. Риск: если critical постоянно наполняется, low никогда не обработается (starvation).

Weighted — вероятностный выбор:

:queues:
  - [critical, 3]
  - [default, 2]
  - [low, 1]

Из шести обращений к Redis в среднем три достанутся critical, два — default, одно — low. Все очереди получают внимание, но с разной частотой. Starvation маловероятен, хотя при экстремальной нагрузке на приоритетные очереди возможны задержки.

Bulkhead: изоляция через процессы и очереди

Один медленный job-класс, который делает HTTP-запросы к медленному API, может занять все потоки. Пока все потоки ждут ответа от API, задачи из других очередей стоят — это cascading failure внутри одного процесса.

Решение из reliability patternsbulkhead: выделить отдельный процесс для медленных задач:

bundle exec sidekiq -q critical -q default  # процесс 1: быстрые задачи
bundle exec sidekiq -q external_api         # процесс 2: медленные API-вызовы

Медленный API забивает все потоки процесса 2 — процесс 1 продолжает обрабатывать critical и default без задержек.

Backpressure: когда Redis переполнен

Redis настроен с политикой noeviction: при исчерпании памяти вызов perform_async получит exception — это backpressure: система сигнализирует, что не справляется.

Мониторинг глубины очереди (Sidekiq::Queue.new("default").latency) помогает заметить накопление до того, как Redis переполнится. Если latency растёт — задачи добавляются быстрее, чем обрабатываются. Решения: увеличить concurrency, добавить процессы, оптимизировать задачи, или перенести некритичные задачи на off-peak часы.


Потоки, процессы, очереди, Capsules — инфраструктура масштабирования на месте. Остаётся вопрос разработки: RSpec-тест вызывает perform_async, задача уходит в Redis — тест проходит, но ничего не проверяет. И параллельно — выбор API: Rails предоставляет ActiveJob, Sidekiq предоставляет Sidekiq::Job. Это рассматривается в тестировании и практиках.


Дизайн задач | Тестирование и практики

Sources