zed.0xff.me
поиск утечек памяти в ruby/rails
0. Введение
Для начала – что такое классические утечки памяти? Это когда мы (или не мы) сделали malloc()
, но забыли сделать free()
. Но на деле эта память уже не используется и должна бы быть давно уже возвращена системе через free()
.
Что такое утечки памяти в ruby? Это висящие в памяти объекты(А), на которые есть ссылки с каких-то глобальных объектов(Б), но фактически (А) уже никому не нужны, а с них, в свою очередь еще есть кучи и кучи ссылок на другие объекты, которые занимают туеву хучу памяти, и не могут быть освобождены сборщиком мусора, потому что на них есть ссылки с других объектов.
Сложно? Сейчас будет еще сложнее :) Плюс к упомянутым выше специфичным для ruby утечкам памяти из-за ссылок на объекты, в ruby также могут быть (и есть!) и классические утечки памяти. В основном в различных экстеншнах, обычно подключаемых через gem’ы. Но и в самом интерпретаторе ruby вероятно где-то еще остались необнаруженные утечки.. только и количество и вероятность их обнаружения на пару-тройку порядков ниже, чем в сторонних gem’ах. Потому что в разработке ruby явно участвует больше и людей и тестеров, чем в разработке любого gem’а.
1. Как узнать что память течет?
Банально :) Смотрим периодически на наш ruby-процесс через ps
или top
, и если размер памяти процесса постоянно увеличивается, и вскоре начинает занимать всю физическую память + своп – то серьезные утечки памяти скорее всего есть.
Но это способ эмпирический, а вот более практический – нужно ограничить процессу память каким-нибудь значительным объемом (например 512Мб)
1 |
Process.setrlimit Process::RLIMIT_AS, 512*1024*1024, 512*1024*1024 |
и погонять процесс. Если он через какое-то время (минуты, часы, дни, месяцы, … :) выпадает с криком [FATAL] failed to allocate memory
– значит утечки есть.
Этот способ не сработает, или сработает неправильно, если работа процесса связана с обработкой больших объемов данных (сопоставимых с 512Мб в данном случае).
2. Как бороться?
Для того чтобы бороться надо сначала узнать с кем конкретно бороться :) А мы пока узнали только тот факт что память течет. Далее нужно определить виновника. Для этого есть как минимум три ортогональных способа:
- valgrind – для поиска утечек в самом ruby и экстеншнах. для нормальной работы требует специально-фигурно-выпиленный бинарник ruby. патчи тут. К текущей версии ruby (ruby-1.8.7-p249) подходят с трудом.
- bleak_house – инструмент для отслеживания аллокейшнов ruby-объектов. Подходит для Rails.
- метод эмпирической дихотомии – универсальный способ :) – во-первых, надо определить что память течет (п.1), во-вторых, берем большой топор и начинаем вырезать или комментировать куски кода.. после каждой ампутации заново проверям осталась ли утечка. если осталась – продолжаем ампутацию, если исчезла – смотрим внутрь отрезанного куска, вплоть до локализации “текущего” метода экстеншна либо присвоения глобальной переменной (
$global_var
) или переменной класса (@@class_var).
P.S. Все утечки актуальны только во время исполнения приложения. Как только приложение завершается, вся память всегда возвращается системе.
rails3: link_to + image_tag
было: (rails 2.x)
1 2 3 |
<%= link_to image_tag('rainbow.png'), '/' -%> <%= link_to "#{image_tag('rainbow.png')}Главная", '/' -%> |
стало: (rails 3.x)
1 2 3 |
<%= link_to image_tag('rainbow.png'), '/' -%> <%= link_to "#{image_tag('rainbow.png')}Главная".html_safe, '/' -%> |
Первый вариант (просто image_tag
) не изменился, а вот второй вариант (image_tag
внутри строки) теперь требует явного указания html_safe
.
rails3: link_to_function
1 |
ActionView::Template::Error (undefined method `link_to_function' for #<Class>) |
теперь link_to_function
находится в плагине prototype_legacy_helper:
1 |
./script/rails plugin install git://github.com/rails/prototype_legacy_helper.git |
возможно, ребята придумали чем-то заменить, а потом и задепрекейтить, но никаких постов на эту тему я в нете не нашел.
Rails 3.x "Crazy Loading" is awesome!
1 2 3 4 5 6 7 8 |
red_items = Item.where(:colour => 'red') red_items.find(1) item = red_items.new item.colour #=> 'red' red_items.exists? #=> true red_items.update_all :colour => 'black' red_items.exists? #=> false |
// actually it’s “Lazy Loading” and stuff. Read more: Active Record Query Interface 3.0.
Rails streaming VS mongrel, thin, ebb and Passenger
С некоторых пор моим любимым веб-сервером для руби приложений является thin
.
Но сегодня он меня конкретно разочаровал. Как, впрочем и mongrel
.
Как нетрудно догадаться из заголовка, дело касается streaming
(про стриминг в рельсах читать тут, начиная с Streaming data and/or controlling the page generation)
((и почему они не автогенерят id для хидеров?? можно было бы ссылку сразу куда надо поставить..)
пример там приведен такой:
1 2 3 4 5 6 7 |
# Streams about 180 MB of generated data to the browser. render :text => proc { |response, output| 10_000_000.times do |i| output.write("This is line #{i}\n") output.flush end } |
так вот, что thin
, что mongrel
, оба тупо забивают на этот стриминг, и пытаются всосать в себя всё что им рельсы отдают, а потом выплюнуть юзеру единым куском..
thin
, например, делает так:
1 2 |
terminate called after throwing an instance of 'std::runtime_error' what(): no allocation for outbound data |
а mongrel
так:
1 2 |
Error calling Dispatcher.dispatch #<NoMemoryError: failed to allocate memory> /usr/lib64/ruby/gems/1.8/gems/actionpack-2.3.5/lib/action_controller/cgi_process.rb:58:in `write' |
Еще хотел быстренько попробовать ebb, но не обнаружил в дистрибутиве внятных инструкций по его установке и настройке.. ладно, запустил через Ebb.start_server("/path/to/rails/app")
, но толку от этого оказалось мало – на порту он поднялся, но ни на один запрос отвечать не захотел.. тупо висел и думал о чем-то там своем..
And the winner is…
Passenger. Хоть я его раньше и не использовал, и довольно таки скептически к нему относился, но он успешно зарекомендовал себя в продакшене, легко поставился (потянув, естественно за собой апача, которого я тоже недолюбливаю..) (хмм.. хотя он есть и для nginx, это несколько меняет дело, на досуге поковыряю)
Так вот, Passenger легко отдал псевдофайлик размером 2 гига с локалхоста на локалхост со скоростью около 12 мегабайт в секунду. При этом не показав никакого значительного увеличения потребления памяти!
PS:
Linux zz 2.6.31-gentoo-r6-zz #1 SMP PREEMPT Tue Dec 22 01:38:46 YEKT 2009 x86_64 Intel(R) Core(TM)2 Duo CPU E8400 @ 3.00GHz GenuineIntel GNU/Linux
Rails 2.3.5
mongrel 1.1.5
thin 1.2.5
passenger 2.2.9
uninitialized constant Test::Unit::TestResult::TestResultFailureSupport
после очередного апдейта гемов возникла сабжевая бага.
лечится просто и эоегантно :)
1 |
sudo gem uninstall test-unit |
как сказано вот тут, проблема связана с постепенным внедрением ruby 1.9, а для 1.8.x пока этот гем в принципе нужен не особо.
новый gem: Яндекс.Метрика
Установка
Добавьте в config/environment.rb:
1 |
config.gem "yandex_metrika", :lib => "yandex/metrika", :source => "http://gemcutter.org" |
и выполните команду:
1 |
rake gems:install |
Описание
Быстрая интеграция Яндекс.Метрики в ваше Rails-приложение.
По умолчанию код метрики автоматически вставляется в каждую страницу перед
закрывающим тэгом </body>.
Но сначала нужно корректно сконфигурировать плагин, иначе он будет ругаться.
Конфигурация
Для этого добавьте следующий код в config/environment.rb:
1 2 3 |
if defined?Yandex::Metrika Yandex::Metrika.counter_id = '123456' end |
А для избежания замусоривания environment.rb всякими плагинами -
можно добавить этот конфиг в config/initializers/yandex_metrika.rb
Вместо ‘123456’ нужно вставить ваш личный COUNTER_ID, который можно вытащить
из javascript-кода, предоставляемого Яндексом: “new Ya.Metrika(123456)”,
тут 123456 и есть искомый код.
По умолчанию код метрики вставляется в страницы только при использовании
production окружения. Для активации кода и в development нужно сделать так:
1 |
Yandex::Metrika.environments = %w'production development' |
Если есть необходимость для каких-то страниц выключить код Яндекс.Метрики – то
добавть следующий код в соответствующий класс контроллера:
1 |
skip_after_filter :add_yandex_metrika_code |
discovered the bug in Rails sqlite3 adapter
example migrations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class M1 < ActiveRecord::Migration def self.up create_table 'users' do |t| t.string 'name' t.decimal 'v1', :precision => 10, :scale => 3 end end def self.down drop_table 'users' end end class M2 < ActiveRecord::Migration def self.up change_column :users, :v1, :decimal, :precision => 12, :scale => 5 end def self.down end end |
expected resulting schema:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
ActiveRecord::Schema.define(:version => 20090706090649) do create_table "users", :force => true do |t| t.string "name" t.decimal "v1", :precision => 12, :scale => 5 end end</code></pre> *actual resulting schema:*<pre><code>ActiveRecord::Schema.define(:version => 20090706090649) do create_table "users", :force => true do |t| t.string "name" t.decimal "v1" end end |
PATCH:
1 2 3 4 5 6 7 8 9 10 11 12 |
diff -ru activerecord-2.3.2-orig/lib/active_record/connection_adapters/sqlite_adapter.rb activerecord-2.3.2/lib/active_record/connection_adapters/sqlite_adapter.rb --- activerecord-2.3.2-orig/lib/active_record/connection_adapters/sqlite_adapter.rb 2009-07-06 15:43:43.000000000 +0600 +++ activerecord-2.3.2/lib/active_record/connection_adapters/sqlite_adapter.rb 2009-07-06 15:20:07.000000000 +0600 @@ -285,6 +285,8 @@ self.limit = options[:limit] if options.include?(:limit) self.default = options[:default] if include_default self.null = options[:null] if options.include?(:null) + self.precision = options[:precision] if options.include?(:precision) + self.scale = options[:scale] if options.include?(:scale) end end end |
Особенности кэширования классов в Rails
Задача
Вот есть например у нас некий самописный класс для взаимодействия со сторонним API:
1 2 3 4 5 6 7 |
class SomeAPIClient def initialize params = {} @host = params[:host] || 'some.api.host' @port = params[:port] || 12345 end ... end |
И кладем мы его в #{RAILS_ROOT}/lib/, чтобы его можно было легко юзать из классов рельсового приложения.
Дальше мы задумываемся о том, что из нашего Rails-приложения, мы будем всегда обращаться к одним и тем же хосту и порту апи-сервера. И надо где-то их вконфигурить. А приложение-то у нас opensource, и хостится на github-e, поэтому хост и порт (и возможно какие-то другие параметры типа api-key) у нас для разных юзеров приложения могут быть разными.
Отсюда вопрос: Как бы это сконфигурить так, чтобы и либу не особо корежить, и чтоб наиболее Rails-way было?..
Мне вот лично пришла в голову следующая мысль:
1 2 3 4 5 6 7 8 9 |
class SomeAPIClient cattr_accessor :default_port, :default_host def initialize params = {} @host = params[:host] || default_host || 'some.api.host' @port = params[:port] || default_port || 12345 end ... end |
Проблема
Всё вроде бы красиво, свои значения для default_host & default_port мы назначаем в #{RAILS_ROOT}/config/initializers/my_api_client.rb, который исключаем из git-репозитария путём помещения в .gitignore.
Но! Но не зря в топике написано про кэширование классов.. в development окружении первый запрос к API выполняется отлично, а вот уже на втором и всех следующих – вступает в действие config.cache_classes = false, рельсы перечитывают декларацию нашего апи, при этом удаляя старый класс из памяти через remove_const. Ну а initializers естественно не перечитывают.
В production всё ОК, потому что там обычно config.cache_classes = true.
Решение
Копаясь во внутренностях Rails обнаружил вот это:
( actionpack-2.3.2/lib/action_controller/dispatcher.rb )
1 2 3 4 5 6 7 8 9 10 11 |
# Add a preparation callback. Preparation callbacks are run before every # request in development mode, and before the first request in production # mode. # # An optional identifier may be supplied for the callback. If provided, # to_prepare may be called again with the same identifier to replace the # existing callback. Passing an identifier is a suggested practice if the # code adding a preparation block may be reloaded. def to_prepare(identifier = nil, &block) ... |
Таким образом, чтобы решение стало работоспособным надо обернуть нашу инициализацию в do_prepare:
( #{RAILS_ROOT}/config/initializers/my_api_client.rb )
1 2 3 4 |
ActionController::Dispatcher.to_prepare(:my_api) do SomeAPIClient.default_host = 'my.api.host' SomeAPIClient.default_port = '23456' end |
И в development mode этот код будет срабатывать перед обработкой каждого запроса, а в production – только перед обработкой самого первого запроса.
Done.
rspec views & params
Пытаюсь тестить спеку вьюшки(view). Причем внутри нее такая логика:
1 |
<%= verbose_search_results(user) if params[:verbose] %>
|
И я не нашел более вменяемого способа передать в нее params, чем следующий:
1 2 3 4 |
it "should do what I said" @controller.stub!(:params).and_return({:verbose => true}) render 'admins/users/list' end |
И тут есть белая сторона медали, а есть черная.
Белая – при этом стандартные параметры 'action'
и 'controller'
также нормально передаются, т.е. мой and_return
действует недеструктивно.
Черная – params
, переданный внутри такой спеки, становится невосприимчив к ключам-символам!
т.е. if params[:verbose]
работать НЕ будет (хотя на живых рельсах работает прекрасно).
А вот if params['verbose']
будет работать и там и там.
PS: может быть, конечно, есть более вменяемый способ передать params, но пока я его не обнаружил.
UPD: как оказалось rspec инициализирует параметры 'action'
и 'controller'
уже на готовом хэше params
, т.е. чтобы всё работало также как и на живых рельсах, надо делать так:
1 2 3 4 |
it "should do what I said" @controller.stub!(:params).and_return(HashWithIndifferentAccess.new({:verbose => true})) render 'admins/users/list' end |
т.е. явно инициализировать params
как HashWithIndifferentAccess
, а не просто хэш.
IMHO как-то это всё-таки кривовато, и должен быть более прямой и очевидный способ..