Разбор 300 файлов л...
 
Уведомления
Очистить всё

Разбор 300 файлов логов: потоки не ускоряют. fork, Ractor или смириться?

5 Сообщения
4 Пользователи
1 Реакции
620 Просмотры
Василий1992
Сообщения: 7
Автор темы
Дружит с bundler data engineer
Присоединился: 3 года назад
[#29]

Я в основном пишу на Python, но тут достался скрипт на Ruby, который раз в сутки разбирает 300 gzip-файлов с логами по 150-200 МБ, считает агрегаты и пишет итог в ClickHouse. Идёт 2 часа 10 минут. Сервер на 8 ядер, решил распараллелить потоками:

queue = Queue.new
files.each { |f| queue << f }
queue.close

results = Queue.new
threads = 8.times.map do
  Thread.new do
    while (file = queue.pop)
      results << parse_file(file)
    end
  end
end
threads.each(&:join)

Стало 2 часа 4 минуты, в top как был занят один процессор, так и остался. Как я понимаю, это та же история, что с GIL в Python? Что тогда делают в Ruby для такой работы?


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

Да, это GVL. Потоки помогают, когда ждём сеть или диск, а распаковка, регулярки и разбор строк упираются в процессор. Для такого проще всего процессы, и можно без gem'ов, на чистом fork:

require "etc"

slices = files.each_slice((files.size / Etc.nprocessors.to_f).ceil).to_a

readers = slices.map do |slice|
  reader, writer = IO.pipe
  fork do
    reader.close
    writer.write(Marshal.dump(slice.map { |f| parse_file(f) }))
    writer.close
  end
  writer.close
  reader
end

results = readers.flat_map { |r| Marshal.load(r.read) }
Process.waitall

Читать из пайпов надо до waitall, иначе на большом результате дочерний процесс повиснет на записи в полный пайп. Ускорения в 8 раз не ждите, на 8 ядрах обычно выходит раз в пять-шесть.


Ответ
kirill_spb
Сообщения: 27
Собрал свой gem разработчик инструментов
Присоединился: 4 года назад

Ractor для этого пока рано, он экспериментальный, и многие C-расширения внутри него не работают. Я бы тоже взял fork. Если не хочется писать пайпы руками, есть gem parallel, там это одна строка: Parallel.map(files, in_processes: 8) { |f| parse_file(f) }.


Ответ
yarik_1966
Сообщения: 3
Пишет в irb разработчик
Присоединился: 3 года назад

и гляньте размеры файлов. бывает, что 299 по 150 МБ и один на пару гигов, и он один досчитывается в конце, пока остальные ядра курят


Ответ
Василий1992
Сообщения: 7
Автор темы
Дружит с bundler data engineer
Присоединился: 3 года назад

Сделал на fork по примеру Тимура, 8 процессов. Было 2 часа 10 минут, стало 24 минуты, в top теперь заняты все ядра. Гигантский файл и правда нашёлся, 1,9 ГБ, его теперь ставлю в отдельную пачку первым. parallel посмотрел, но тащить зависимость ради пятнадцати строк не стал.


Ответ