Я в основном на Python, Ruby у нас живёт в мелких скриптах вокруг выгрузок в ClickHouse. Есть лог сервиса в JSON, по строке на событие, файл за сутки около 2 ГБ. Нужно вытащить топ путей с ошибками. На маленьком файле скрипт работает, на суточном машина уходит в своп, в пике видел больше 6 ГБ. Ruby 3.2.
require "json"
lines = File.read(ARGV[0]).split("\n")
events = lines.map { |l| JSON.parse(l) }
errors = events.select { |e| e["level"] == "error" }
by_path = errors.group_by { |e| e["path"] }
by_path.sort_by { |_, list| -list.size }.first(20).each do |path, list|
puts "#{list.size}\t#{path}"
end
Где я столько держу?
Ты держишь файл трижды: одной строкой целиком, массивом строк и массивом хэшей. А нужен тебе только счётчик. Читай построчно и считай на ходу:
require "json"
counts = Hash.new(0)
File.foreach(ARGV[0]) do |line|
next unless line.include?('"level":"error"')
event = JSON.parse(line)
counts[event["path"]] += 1 if event["level"] == "error"
end
counts.sort_by { |_, n| -n }.first(20).each do |path, n|
puts "#{n}\t#{path}"
end
File.foreach отдаёт по строке, и память почти не растёт. Проверку include? я поставил до JSON.parse: парсить каждую строку дорого, а ошибок обычно процент-два. Только сверь со своим форматом, если после двоеточия в логе стоит пробел, подстрока будет другой.
Прогнал оба варианта на синтетическом файле в 500 МБ, Ruby 3.2.2, мерил через /usr/bin/time -v. Исходный: 1,9 ГБ RSS и 41 секунда. Построчный с include?: 38 МБ и 9 секунд. Построчный без include?: те же 38 МБ, но 33 секунды, почти всё время уходит на JSON.parse. То есть по памяти выигрыш даёт foreach, а по времени фильтр по подстроке.
Переделал по образцу kirill_spb, пробелов после двоеточия у нас нет, так что фильтр работает. Суточный файл теперь проходит за 40 секунд и держит меньше 100 МБ, раньше я его на ноутбуке дома в Гатчине вообще не мог дождаться. Обидно, что на Python я бы написал так же неправильно: дело было не в языке, а в File.read.