Подтвердить что ты не робот

Поиск причины утечки памяти в Ruby

Я обнаружил утечку памяти в моем коде Rails - то есть я обнаружил, что код утечек, но не почему. Я сократил его до теста, который не требует Rails:

require 'csspool'
require 'ruby-mass'

def report
    puts 'Memory ' + 'ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"'.strip.split.map(&:to_i)[1].to_s + 'KB'
    Mass.print
end

report

# note I do not store the return value here
CSSPool::CSS::Document.parse(File.new('/home/jason/big.css'))

ObjectSpace.garbage_collect
sleep 1

report

ruby-mass предположительно позволяет мне видеть все объекты в памяти. CSSPool - это синтаксический анализатор CSS, основанный на racc. /home/jason/big.css - это файл CSS размером 1,5 МБ.

Это выводит:

Memory 9264KB

==================================================
 Objects within [] namespace
==================================================
  String: 7261
  RubyVM::InstructionSequence: 1151
  Array: 562
  Class: 313
  Regexp: 181
  Proc: 111
  Encoding: 99
  Gem::StubSpecification: 66
  Gem::StubSpecification::StubLine: 60
  Gem::Version: 60
  Module: 31
  Hash: 29
  Gem::Requirement: 25
  RubyVM::Env: 11
  Gem::Specification: 8
  Float: 7
  Gem::Dependency: 7
  Range: 4
  Bignum: 3
  IO: 3
  Mutex: 3
  Time: 3
  Object: 2
  ARGF.class: 1
  Binding: 1
  Complex: 1
  Data: 1
  Gem::PathSupport: 1
  IOError: 1
  MatchData: 1
  Monitor: 1
  NoMemoryError: 1
  Process::Status: 1
  Random: 1
  RubyVM: 1
  SystemStackError: 1
  Thread: 1
  ThreadGroup: 1
  fatal: 1
==================================================

Memory 258860KB

==================================================
 Objects within [] namespace
==================================================
  String: 7456
  RubyVM::InstructionSequence: 1151
  Array: 564
  Class: 313
  Regexp: 181
  Proc: 113
  Encoding: 99
  Gem::StubSpecification: 66
  Gem::StubSpecification::StubLine: 60
  Gem::Version: 60
  Module: 31
  Hash: 30
  Gem::Requirement: 25
  RubyVM::Env: 13
  Gem::Specification: 8
  Float: 7
  Gem::Dependency: 7
  Range: 4
  Bignum: 3
  IO: 3
  Mutex: 3
  Time: 3
  Object: 2
  ARGF.class: 1
  Binding: 1
  Complex: 1
  Data: 1
  Gem::PathSupport: 1
  IOError: 1
  MatchData: 1
  Monitor: 1
  NoMemoryError: 1
  Process::Status: 1
  Random: 1
  RubyVM: 1
  SystemStackError: 1
  Thread: 1
  ThreadGroup: 1
  fatal: 1
==================================================

Вы можете увидеть память собирается путь. Некоторые счетчики идут вверх, но никаких объектов, специфичных для CSSPool, нет. Я использовал метод индекса "ruby-mass" для проверки объектов, на которые есть ссылки, например:

Mass.index.each do |k,v|
    v.each do |id|
        refs = Mass.references(Mass[id])
        puts refs if !refs.empty?
    end
end

Но опять же, это не дает мне ничего связанного с CSSPool, просто информация о гемах и тому подобное.

Я также попытался вывести "GC.stat"...

puts GC.stat
CSSPool::CSS::Document.parse(File.new('/home/jason/big.css'))
ObjectSpace.garbage_collect
sleep 1
puts GC.stat

Результат:

{:count=>4, :heap_used=>126, :heap_length=>138, :heap_increment=>12, :heap_live_num=>50924, :heap_free_num=>24595, :heap_final_num=>0, :total_allocated_object=>86030, :total_freed_object=>35106}
{:count=>16, :heap_used=>6039, :heap_length=>12933, :heap_increment=>3841, :heap_live_num=>13369, :heap_free_num=>2443302, :heap_final_num=>0, :total_allocated_object=>3771675, :total_freed_object=>3758306}

Насколько я понимаю, если на объект не ссылаются и происходит сборка мусора, то этот объект следует очистить из памяти. Но, похоже, это не то, что здесь происходит.

Я также читал об утечках памяти уровня C, и, поскольку CSSPool использует Racc, который использует код C, я думаю, что это возможно. Я запустил свой код через Valgrind:

valgrind --partial-loads-ok=yes --undef-value-errors=no --leak-check=full --fullpath-after= ruby leak.rb 2> valgrind.txt

Результаты здесь. Я не уверен, подтверждает ли это утечку уровня C, так как я также читал, что Ruby работает с памятью, которую Valgrind не понимает.

Используемые версии:

  • Ruby 2.0.0-p247 (это то, что запускает мое приложение Rails)
  • Ruby 1.9.3-p392-ref (для тестирования с рубиновой массой)
  • рубиновая масса 0.1.3
  • CSSPool 4.0.0 отсюда
  • CentOS 6.4 и Ubuntu 13.10
4b9b3361

Ответ 1

Похоже, вы входите в The Lost World здесь. Я не думаю, что проблема связана с c-привязками в racc.

Управление памятью Ruby элегантно и громоздко. Он хранит объекты (называемые RVALUE s) в так называемых кучках размером около 16 КБ. На низком уровне RVALUE представляет собой c-struct, содержащую union различных стандартных представлений объектов ruby.

Итак, кучи хранят объекты RVALUE, размер которых не превышает 40 байт. Для таких объектов, как String, Array, Hash и т.д., Это означает, что маленькие объекты могут вставляться в кучу, но как только они достигнут порога, будет выделена дополнительная память за пределами кустов Ruby.

Эта дополнительная память гибкая; будет освобожден, как только объект станет GCed. Вот почему ваш тестовый файл с big_string показывает поведение вверх по памяти:

def report
  puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`
          .strip.split.map(&:to_i)[1].to_s + 'KB'
end
report
big_var = " " * 10000000
report
big_var = nil 
report
ObjectSpace.garbage_collect
sleep 1
report
# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 11788KB

Но кучи (см. GC[:heap_length]) сами не выпущены обратно в ОС, после их приобретения. Послушай, я сделаю смутное изменение в твоей тестовой папке:

- big_var = " " * 10000000
+ big_var = 1_000_000.times.map(&:to_s)

И, voilá:

# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 57448KB

Память больше не выводится обратно в ОС, потому что каждый элемент массива, который я ввел, соответствует размеру RVALUE и хранится в рубиновой куче.

Если вы проверили вывод GC.stat после запуска GC, вы обнаружите, что значение GC[:heap_used] уменьшается, как ожидалось. У Ruby теперь много кучи, готово.

Подведение итогов: Я не думаю, что код c протекает. Я думаю, что проблема заключается в представлении огромного изображения base64 в вашем css. Я понятия не имею, что происходит внутри парсера, но похоже, что огромная строка заставляет количество кучи рубина увеличиваться.

Надеюсь, это поможет.

Ответ 2

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

Ваша проблема, однако, по-видимому, связана с тем, что Ruby фактически не освобождает память обратно в операционную систему после ее приобретения.

Выделение памяти

Хотя программисты на Ruby не часто беспокоятся о распределении памяти, иногда возникает следующий вопрос:

Почему мой процесс Ruby остался таким большим даже после того, как я очистил все ссылки на большие объекты? Im/sure/GC запускалась несколько раз и освобождала мои большие объекты, и я не теряла память.

Программист может задать тот же вопрос:

Я освобождаю() -ed много памяти, почему мой процесс все еще такой большой?

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

Библиотеки/среды выполнения пользовательского пространства реализуют распределитель памяти (например, malloc (3) в libc), который берет большие куски памяти ядра2 и делит их на более мелкие части для использования приложениями пользовательского пространства.

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

Выпуск памяти обратно в ядро также имеет свою стоимость. Распределители памяти пользовательского пространства могут удерживать эту память (в частном порядке) в надежде, что она может быть повторно использована в том же процессе и не возвращать ее ядру для использования в других процессах. (Ruby Best Practices)

Таким образом, ваши объекты вполне могут быть собраны мусором и возвращены обратно в доступную память Ruby, но поскольку Ruby никогда не возвращает ОС неиспользуемой памяти, значение rss для процесса остается тем же, даже после сбора мусора. Это на самом деле по замыслу. По словам Майка Перхама:

... И поскольку MRI никогда не возвращает неиспользуемую память, наш демон может легко взять 300-400 МБ, если он использует только 100-200.

Важно отметить, что это по сути дизайн. История Rubys в основном используется в качестве инструмента командной строки для обработки текста и поэтому ценит быстрый запуск и небольшой объем памяти. Он не был разработан для длительных процессов демон/сервер. Java делает подобный компромисс в своих клиентских и серверных виртуальных машинах.

Ответ 3

Это может быть связано с функцией "Lazy Sweeping" в Ruby 1.9.3 и выше.

Lazy sweeping в основном означает, что при сборке мусора Ruby только "отметает" достаточно объектов, чтобы создать пространство для новых объектов, которые ему нужно создать. Он делает это, потому что, пока работает сборщик мусора Ruby, ничего больше не делает. Это называется сборкой мусора "Остановить мир".

По существу, ленивый подметание сокращает время, необходимое Ruby для "остановки мира". Вы можете больше узнать о ленивом подметании здесь.

Как выглядит ваша переменная среды RUBY_GC_MALLOC_LIMIT?

Вот выдержка из блога Sam Saffron относительно ленивой подметания и RUBY_GC_MALLOC_LIMIT:

GC в Ruby 2.0 поставляется в двух разных вариантах. У нас есть "полный" GC, который запускается после того, как мы выделим больше, чем наш malloc_limit и ленивый разверток (частичный GC), который будет работать, если у нас когда-нибудь закончится свободное место в наших кучах.

Леничная развертка занимает меньше времени, чем полный GC, но только выполняет частичный GC. Цель состоит в том, чтобы чаще выполнять короткий GC, тем самым увеличивая общую пропускную способность. Мир останавливается, но за меньшее время.

malloc_limit установлен в 8MB из коробки, вы можете поднять его, установив RUBY_GC_MALLOC_LIMIT выше.

Является ли ваш RUBY_GC_MALLOC_LIMIT чрезвычайно высоким? Mine установлен в 100000000 (100 МБ). Значение по умолчанию составляет около 8 МБ, но для приложений с рельсами они рекомендуют его немного выше. Если ваш слишком высокий, это может помешать Ruby удалять мусорные объекты, потому что он считает, что у него достаточно места для роста.

Ответ 4

Опираясь на объяснение @mudasobwa, я наконец нашел причину. Код в CSSPool проверял очень длинный URI данных на наличие escape-последовательностей. Это вызовет scan URI с регулярным выражением, которое соответствует escape-последовательности или одному символу, map эти результаты с unescape, а затем join его обратно в строку. Это эффективно выделяло строку для каждого символа в URI. Я изменил его для gsub escape-последовательностей, которые, похоже, дают те же результаты (все тесты пройдены) и значительно сокращают используемую конечную память.

При использовании того же тестового примера, который был первоначально опубликован (за Mass.print вывода Mass.print), это результат до изменения:

Memory 12404KB
Memory 292516KB

и это результат после изменения:

Memory 12236KB
Memory 19584KB