Здесь есть японский перевод этого поста!
Что такое символ 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=
- https://www.ruby-lang.org/en/news/2013/02/22/json-dos-cve-2013-0269/
- https://groups.google.com/forum/#!topic/ruby-security-ann/o0Dsdk2WrQ0
- http://ronin-ruby.github.io/blog/2013/01/09/rails-pocs.html
Список можно продолжать и продолжать, но вы понимаете, в чем дело. Создание символов из пользовательского ввода опасно; только если символы не являются сборщиком мусора, что происходит до 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 при создании метода он сохраняет уникальный идентификатор в таблице методов.
Позже, когда вы вызываете метод, 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, производительности и символах, следуйте за ним за всем этим джазом.