Статьи

Symbol GC в Ruby 2.2

Здесь есть японский перевод этого поста!

Fotolia_62448068_Subscription_Monthly_M

Что такое символ GC и почему это должно вас волновать? Ruby 2.2 был только что выпущен и, помимо инкрементального GC, еще одной важной особенностью является Symbol GC . Если вы были в окружении Ruby, вы слышали термин «символ DoS». Атака отказа в обслуживании символов происходит, когда система создает так много символов, что ей не хватает памяти. Это потому, что до Ruby 2.2 символы жили вечно. Например в Ruby 2.1:

# Ruby 2.1 before = Symbol.all_symbols.size 100_000.times do |i| "sym#{i}".to_sym end GC.start after = Symbol.all_symbols.size puts after - before # => 100001 

Здесь мы создаем 100 000 символов, и они все еще существуют, хотя мы запустили GC и никакие переменные не ссылаются на эти объекты. Это может легко стать проблемой, если вы to_sym некоторый код, который принимает пользовательский параметр и вызывает to_sym :

 def show step = params[:step].to_sym end 

В этом случае кто-то может сделать много запросов на example.com/step= и, поскольку ваше приложение никогда не очищает символы, вашей программе в конечном итоге не хватит памяти и произойдет сбой. Это может звучать как сфабрикованный пример, но он был похож на код, который я на самом деле зафиксировал в своем драгоценном камне Wicked (не волнуйтесь, это исправлено). Это также не единичный случай:

Список можно продолжать и продолжать, но вы понимаете, в чем дело. Создание символов из пользовательского ввода опасно; только если символы не являются сборщиком мусора, что происходит до Ruby 2.2.

Symbol GC в Ruby 2.2

Начиная с Ruby 2.2, теперь символы собирают мусор.

 # Ruby 2.2 before = Symbol.all_symbols.size 100_000.times do |i| "sym#{i}".to_sym end GC.start after = Symbol.all_symbols.size puts after - before # => 1 

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

WAT?

#not_all_symbols

До Ruby 2.2 мы не могли собирать символы, потому что они использовались внутренне интерпретатором Ruby. По сути, каждый символ имеет уникальный идентификатор объекта. Например :foo.object_id всегда должен быть одинаковым в течение всего времени выполнения программы. Это связано с тем, как работает rb_intern .

В C-Ruby при создании метода он сохраняет уникальный идентификатор в таблице методов.

Слайд из выступления Нари о Symbol GC

Позже, когда вы вызываете метод, Ruby ищет символ имени метода, а затем получает идентификатор этого символа. Идентификатор символа используется для указания на статическую память функции в C. Затем вызывается функция в C, и именно так Ruby выполняет методы.

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

Чтобы обойти эту проблему, Нарихиро Накамура представил идею «Бессмертного символа» в Мире C и «Смертного символа» в мире Рубинов.

По сути, все символы, создаваемые динамически во время работы Ruby (с помощью to_sym и т. Д.), Можно собирать мусором, поскольку они не используются за кулисами внутри интерпретатора Ruby. Однако символы, созданные в результате создания нового метода, или символы, которые статически находятся внутри кода, не будут собираться мусором. Например :foo и def foo; end def foo; end оба не будут собирать мусор, однако "foo".to_sym будет иметь право на сборку мусора.

Есть подходы с этим подходом, все еще возможно иметь DoS, если вы случайно создаете методы, основанные на пользовательском вводе.

 define_method(params[:step].to_sym) do # ... end 

Поскольку define_method вызывает rb_intern за кулисами, даже если мы передаем динамически определенный (то есть to_sym ) символ, он будет преобразован в бессмертный символ, чтобы его можно было использовать для поиска метода. Надеюсь, вы все равно этого не сделаете, но все же хорошо отметить опасные моменты в Ruby.

Переменные также используют символы за сценой.

 before = Symbol.all_symbols.size eval %Q{ BAR = nil FOO = nil } GC.start after = Symbol.all_symbols.size puts after - before # => 2 

Несмотря на то, что переменная равна nil , она использует закулисный символ, который никогда не будет собирать мусор. Кроме того, чтобы избежать случайного определения методов на основе пользовательского ввода, также следите за созданием переменных на основе пользовательского ввода:

 self.instance_variable_set( "@step_#{ params[:step] }".to_sym, nil ) 

Чтобы быть по-настоящему безопасным, вы должны периодически проверять Symbol.all_symbols.size после запуска GC.start чтобы убедиться, что таблица символов не растет. В будущем мы надеемся, что некоторые хорошие стандарты в отношении того, что можно и что нельзя делать с символами, становятся более общими знаниями. Если вы найдете другую действительно распространенную ошибку, свяжитесь со мной в твиттере, и я постараюсь постоянно обновлять этот раздел.

Спасибо @ nari3 за просмотр этого раздела и предоставление обратной связи. Для получения дополнительной информации о внутреннем оборудовании и его реализации прочитайте слайд Нари или прослушайте презентацию на Ruby Kaigi .

Я чувствую потребность в скорости

В дополнение к безопасности, основной причиной, по которой вы должны заботиться об этой функции, является скорость. Есть тонна кода, написанного вокруг превращения символов в строки, чтобы избежать случайного выделения символов из пользовательского ввода. Обычно, когда вы соединяете слова «ton» и «code», результаты не быстрые.

Наиболее распространенным примером избегания выделения символов является HashWithIndifferentAccess Rail (ActiveSupport). Поскольку я писал о подклассах Hash, например, о том , что Hashie медленный , вы, возможно, не удивитесь, обнаружив, что такое поведение в Rails приводит к огромным потерям производительности.

 require 'benchmark/ips' require 'active_support' require 'active_support/hash_with_indifferent_access' hash = { foo: "bar" } INDIFFERENT = HashWithIndifferentAccess.new(hash) REGULAR = hash Benchmark.ips do |x| x.report("indifferent-string") { INDIFFERENT["foo"] } x.report("indifferent-symbol") { INDIFFERENT[:foo] } x.report("regular-symbol") { REGULAR[:foo] } end 

Когда мы запустим это:

 Calculating ------------------------------------- indifferent-string 115.962ki/100ms indifferent-symbol 82.702ki/100ms regular-symbol 150.856ki/100ms ------------------------------------------------- indifferent-string 4.144M (± 4.4%) i/s - 20.757M indifferent-symbol 1.578M (± 3.7%) i/s - 7.939M regular-symbol 8.685M (± 2.4%) i/s - 43.447M 

Мы видим, что хеш безразличного доступа со строкой составляет примерно половину скорости обычного хеша с символьными клавишами. Мы также видим, что использование символа для доступа к значению в хэше индифферентного доступа в 5 раз медленнее, чем использование обычного хэша с символьными ключами. Я писал о том, что производительность строкового ключа в Ruby 2.2 значительно улучшается , однако доступ к хешу с символом по-прежнему является самым быстрым и, как некоторые могут поспорить, наиболее эстетически приятным способом доступа к хешу. Теперь в Ruby 2.2 мы могли использовать символьные ключи в параметрах Rails. Если бы мы сделали это переключение, нам не пришлось бы беспокоиться о безопасности, и нам не пришлось бы нести накладные расходы по налогу HashWithIndifferentAccess .

Примечание. Прежде чем вносить какие-либо значительные изменения в производительность, вы должны выполнить сравнительный анализ на уровне приложения, особенно когда это требует устаревания API. Никогда не отправляйте исправление производительности с обоснованием того, что «какой-то блог сказал, что это быстрее», даже если этот блог мой. Всегда проверяйте претензии в каждом конкретном случае.

резюмировать

Symbol GC защищает ваш приклад от DoS-атак и позволяет вам гибко использовать символы в любом месте. В сочетании с множеством других функциональных возможностей Ruby 2.2, включая инкрементный сборщик мусора и дедупликацию строк с помощью хэш-ключей , нет причин не обновлять сразу. Установить локально:

 $ ruby-install ruby 2.2.0 

Запустите в производстве (если вы используете Heroku ):

 $ echo "ruby '2.2.0'" >> Gemfile 

Не ждите, будущее Руби сейчас!


@schneems пишет о Ruby, производительности и символах, следуйте за ним за всем этим джазом.