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



я обнаружил утечку памяти в моем коде 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


Рубин-массой предположительно позволяет мне видеть все объекты в памяти. CSSPool - это CSS парсер на основе racc. / главная / Джейсон / большой.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, просто gem info и тому подобное.



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



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 делает вещи с памятью, которые Валгринд не понимает.



версии:




  • Ruby 2.0.0-p247 (это то, что работает мое приложение Rails)

  • Ruby 1.9.3-p392-ref (для тестирования с ruby-mass)

  • ruby-mass 0.1.3

  • CSSPool 4.0.0 from здесь

  • CentOS 6.4 и Ubuntu 13.10

616   4  

4 ответов:

похоже, вы входите Затерянный Мир здесь. Я не думаю, что проблема с C-привязки в racc либо.

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

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

эта дополнительная память является гибкой; is будет освобожден, как только объект стал GC'Ed. вот почему ваш тестовый случай с 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)

и вуаля:

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

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

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

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

надеюсь, что это помогает.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Что значит ваше RUBY_GC_MALLOC_LIMIT переменная окружения выглядит так?

вот отрывок из блог Сэма Саффрона относительно ленивого подметания и RUBY_GC_MALLOC_LIMIT:

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

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

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

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

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

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

Memory 12404KB
Memory 292516KB

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

Memory 12236KB
Memory 19584KB

Comments

    Ничего не найдено.