Ruby + Sidekiq — лучшее решение для выполнения и обработки больших данных

Представьте, что у нас есть 10 тысяч сущностей-х. Для каждого объекта-x мы должны выполнить асинхронный вызов API. Каждый вызов API возвращает 100 объектов-y. Тогда всего у нас есть 10k * 100 = 1_000_000 сущностей-у. Для каждого объекта мы должны сделать еще один асинхронный вызов API и получить результат. Вопрос - как лучше это сделать?

Для контекста: мое ядро ​​имеет 16 потоков.

Моя первая мысль заключалась в том, чтобы разделить объекты-x (10 тыс.) между потоками, чтобы каждый поток мог обрабатывать свое собственное количество объектов-х и объектов-y. Например, если у нас есть 10 тысяч объектов-x, мы могли бы разделить его на 16 (количество потоков) и передать результат каждому потоку. Но потом я понял, что Ruby может одновременно запускать только один поток. Хотя Sidekiq выполняет задания одновременно в отдельных потоках.

Тогда я подумал о том, чтобы разделить объекты-x между заданиями Sidekiq, чтобы, если у нас есть 10 тысяч объектов-х, мы могли бы разделить их на 16 (количество потоков) и передать результат каждому заданию. Но я не знаю об этом. Теоретически мы можем использовать большее количество заданий, чем количество потоков, и я не знаю, будет ли это более эффективно или нет. Что вы думаете?

🤔 А знаете ли вы, что...
Ruby on Rails популярен для быстрого создания MVP (Minimum Viable Product) стартапов и прототипов.


52
1

Ответ:

Решено

Я бы создал два рабочих места в Sidekiq. Один для получения entity-x и постановки в очередь другого задания для каждого возвращенного entities-y. А другое задание затем получает entify-y и обрабатывает его.

В основном так (псевдокод):

class EntityXJob
  include Sidekiq::Job

  def perform(entity_x)
    response = fetch(entity_x)

    response.entities_y.each do |entity_y|
      EntityYJob.perform_async(entity_y)
    end
  end
end

class EntityYJob
  include Sidekiq::Job

  def perform(entity_y)
    response = fetch(entity_y)
    process(response)
  end
end

Чтобы начать все обрабатывать, вам нужно поставить в очередь по одному EntityXJob для каждого entity_x. Как это сделать, зависит от того, откуда вы можете получить список всех entities_x (другой запрос API, уже сохраненный в вашей БД, или файл конфигурации) и от того, как вы хотите запустить обработку (действие в контроллере, другое фоновое задание, CRON). . Если бы у вас были эти идентификаторы в БД, вы могли бы, например, инициировать постановку всех заданий в очередь с помощью Rails Runner следующим образом:

rails runner "EntityX.find_each { |entity_x| EntityXJob.perform_async(entity_x) }"

Обработка этих заданий по одному в Sidekiq позволяет отслеживать ход выполнения в веб-интерфейсе Sidekiq, а невыполненные задания автоматически повторяются Sidekiq в конфигурации по умолчанию.

Какое количество работников подойдет вам лучше всего, зависит от того, как долго этим заданиям придется ждать ввода-вывода из запроса API и от того, насколько сложна обработка каждого задания. На машине с 16 ядрами я бы настроил как минимум 16 процессов Sidekiq, а возможно и больше, потому что большинство рабочих процессов, скорее всего, большую часть времени будут простаивать и ждать ответов API. Ограничивающим фактором в вашем примере, скорее всего, будет оперативная память, а не ядра процессора.

Также имейте в виду, что API может иметь ограничение скорости. В этом случае может потребоваться другой подход, чтобы гарантировать, что рабочие процессы не превысят лимит частоты запросов API.


Параллелизм против параллелизма

Есть разница между процессами Sidekiq (выше я писал рабочие) и потоками Sidekiq. Вкратце и упрощенно:

Из-за GIL (глобальной блокировки интерпретатора) один процесс Ruby может одновременно запускать только один поток на одном ядре ЦП. Когда поток ожидает, например, ввода-вывода, другой поток может быть запущен, пока другой ожидает или приостанавливает работу. Это параллелизм. Но несколько процессов Sidekiq могут работать на разных ядрах ЦП, что обеспечивает реальный параллелизм.

Общее эмпирическое правило для достижения максимальной производительности с помощью Sidekiq: запустите 1 процесс Sidekiq для каждого доступного ядра ЦП, чтобы максимизировать параллелизм. Затем настройте количество потоков на процесс в соответствии со шаблоном рабочей нагрузки ваших заданий и доступной памятью, чтобы максимизировать параллелизм в каждом процессе.