Джоба новогодней ра...
 
Уведомления
Очистить всё

Джоба новогодней рассылки упала по памяти на 300 тысячах пользователей

5 Сообщения
4 Пользователи
7 Реакции
501 Просмотры
georgiy_efi
Сообщения: 7
Автор темы
Разобрался с блоками middle-разработчик
Присоединился: 1 год назад
[#105]

Rails 8.1, Ruby 3.4, данные в MySQL, GoodJob на отдельном PostgreSQL. Перед праздниками запустили рассылку итогов года на 300 тысяч подписчиков:

class HolidayDigestJob < ApplicationJob
  def perform
    User.where(subscribed: true).each do |user|
      DigestMailer.with(user: user).holiday.deliver_now
    end
  end
end

Через сорок минут и 80 тысяч писем контейнер прибили по памяти. Джобу перезапустили, и часть людей получила письмо дважды. Как это должно было выглядеть?


4 ответа
GalinaKorneva
Сообщения: 30
Читает чужие PR бэкенд-разработчица
Присоединился: 4 года назад

Тут две проблемы, и вторая хуже первой. each по relation загружает все 300 тысяч разом, отсюда память, это лечится find_each. Но даже с ним одна джоба на сорок минут - плохая единица работы: упала посередине, и непонятно, кому уже ушло. Я бы разрезала: одна джоба раздаёт пачки, каждая пачка своя джоба, а факт отправки пишется в базу:

class HolidayDigestJob < ApplicationJob
  def perform(campaign_id)
    User.where(subscribed: true).in_batches(of: 1_000) do |batch|
      DigestBatchJob.perform_later(campaign_id, batch.ids)
    end
  end
end

class DigestBatchJob < ApplicationJob
  def perform(campaign_id, user_ids)
    sent = Delivery.where(campaign_id: campaign_id, user_id: user_ids).pluck(:user_id)
    User.where(id: user_ids - sent).find_each do |user|
      DigestMailer.with(user: user).holiday.deliver_now
      Delivery.create!(campaign_id: campaign_id, user_id: user.id)
    end
  end
end

Тогда перезапуск безопасен: кому уже отправлено, тот отфильтруется.


Ответ
TimurK62
Сообщения: 31
Читает чужие PR бэкенд-разработчик
Присоединился: 3 года назад

Для понимания масштаба: объект User с десятком колонок вместе со строками занимает в Ruby порядка нескольких килобайт. 300 тысяч таких - уже гигабайт с лишним, а мейлер добавляет своих объектов на каждое письмо. find_each держит в памяти тысячу записей за раз. Если хочется убедиться, прогони на стейдже memory_profiler на 10 тысячах пользователей, рост будет почти линейный.


Ответ
kostya.sokurov
Сообщения: 73
Держит прод бэкенд-разработчик, senior
Присоединился: 4 года назад

И уникальный индекс на deliveries (campaign_id, user_id). Без него проверка sent не спасёт, если одна пачка вдруг выполнится дважды параллельно.


Ответ
georgiy_efi
Сообщения: 7
Автор темы
Разобрался с блоками middle-разработчик
Присоединился: 1 год назад

Переделал на пачки по тысяче с таблицей deliveries и уникальным индексом. На стейдже прогнал 300 тысяч фейковых адресов на заглушку почты: 300 джоб, воркеры не выходят за 400 МБ, весь прогон 25 минут на четырёх потоках. Специально убил воркер посередине, после перезапуска дублей ноль. К Восьмому марта будет готово)


Ответ