Я в основном пишу на 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 для такой работы?
Да, это 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 ядрах обычно выходит раз в пять-шесть.
Ractor для этого пока рано, он экспериментальный, и многие C-расширения внутри него не работают. Я бы тоже взял fork. Если не хочется писать пайпы руками, есть gem parallel, там это одна строка: Parallel.map(files, in_processes: 8) { |f| parse_file(f) }.
и гляньте размеры файлов. бывает, что 299 по 150 МБ и один на пару гигов, и он один досчитывается в конце, пока остальные ядра курят
Сделал на fork по примеру Тимура, 8 процессов. Было 2 часа 10 минут, стало 24 минуты, в top теперь заняты все ядра. Гигантский файл и правда нашёлся, 1,9 ГБ, его теперь ставлю в отдельную пачку первым. parallel посмотрел, но тащить зависимость ради пятнадцати строк не стал.