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. Как бороться?

Для того чтобы бороться надо сначала узнать с кем конкретно бороться :) А мы пока узнали только тот факт что память течет. Далее нужно определить виновника. Для этого есть как минимум три ортогональных способа:

  1. valgrind – для поиска утечек в самом ruby и экстеншнах. для нормальной работы требует специально-фигурно-выпиленный бинарник ruby. патчи тут. К текущей версии ruby (ruby-1.8.7-p249) подходят с трудом.
  2. bleak_house – инструмент для отслеживания аллокейшнов ruby-объектов. Подходит для Rails.
  3. метод эмпирической дихотомии – универсальный способ :) – во-первых, надо определить что память течет (п.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

Phuby on Phails: PHP и Ruby в одном флаконе! жесть!

код и видео
видео в на порядок выразительнее кода :)

// thanks to Rail0rz

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

submitted to Rails’ lighthouse

  • Posted on July 06, 2009
  • Tagged rails, bug

Особенности кэширования классов в 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 как-то это всё-таки кривовато, и должен быть более прямой и очевидный способ..