[译] 在 Ruby 2.2 中的 Symbol GC

什么是 Symbol GC

什么是Symbol GC呢?为什么我们需要在乎它呢?Ruby 2.2 刚刚发布了一个新的功能来对应 不断增长的GC,这是2.2里面比较大的功能。如果你熟悉 Ruby 的话,一定听说过 “symbol DoS” 的问题。一共 symbol 拒绝式攻击,当一共系统创建太多的 symbol 会导致内存不足的问题。 这是因为 Ruby 2.2 之前的版本, symbol是永远存在的。例如:

在 Ruby 2.1 中:

1
2
3
4
5
6
7
8
9
10
# 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

在这里我们建了100000个 symbols,即使我们运行了 GC, symbol的数量并未发生减少。如果写了一些代码, 将用户的输入参数调用了to_sym方法的话:

1
2
3
def show
  step = params[:step].to_sym
end

这种情况下,有人可以通过发送很多example.com/step=的request请求,因为系统没有对symbol做垃圾回收, 你的程序将会导致内存不足从而引起程序崩溃。像这种根据用户的收入新建symbol是非常危险的,如果symbol没有 被回收的话,但这个问题只发生在 2.2 版本之前。

在Ruby 2.2 中的 Symbol GC

Ruby 2.2 中的 Symbol 可以垃圾回收了。

1
2
3
4
5
6
7
8
9
# 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

因为我们创建的symbol没有被其他对象引用,他们可以被安全的回收。这样可以避免我们意外创建太多 symbol 导致 程序崩溃的问题。然而 Ruby 并没有回收所有的 symbol。

不是所有的 symbol

在 Ruby 2.2以前,我们不能收集symbol是因为Ruby解释器内部使用了。每个symbol都有一个唯一的object ID。例如 ::foo.object_id的值一直是一样,不管你在哪里使用,这是rb_intern工作方式导致的。

在 C-Ruby,当你创建了一个方法将会是存储一个唯一的ID到一个方法表里面。

如果你调用这个方法,Ruby会寻找方法名的symbol,然后得到这个symbol的ID。这个symbol的ID 被用来指向在C方法的静态内存。C方法然后被调用,这就是Ruby怎么调用方法的过程。

如果我们垃圾回收symbol,而这个symbol正好指向一个方法的话,这个方法将不再被能调用,这是不好的。

围绕这个问题,Narihiro Nakamura 介绍了在C世界里面的"永生的 Symbol" 和 Ruby世界 “终将死亡的 Symbol” 的2个概念。

一般来说,当调用Ruby运行to_sym方法,所有的 Symbol 会被动态创建,这些 Symbol 可以被垃圾回收的。 因为这些 Symbol 没有被使用在 Ruby 内部的解释器里面。然而,symbol被用来创建新方法或者内部使用的一些 symbol将不会被回收。例如::foodef foo; end都不会被垃圾回收,然后"foo".to_sym将 有资格被垃圾回收。

知道这种实现方式的话,那还是有可能被Dos如果你根据用户输入动态定义方法的话。

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

因为define_method调用rb_intern,因为我们创建了动态的方法,导致都会转化为永生的symbol用来 方法的查找。但是一般情况下,我们会很少这么使用。

变量也需要使用symbols。

1
2
3
4
5
6
7
8
9
before = Symbol.all_symbols.size
eval %Q{
  BAR = nil
  FOO = nil
}
GC.start
after = Symbol.all_symbols.size
puts after - before
# => 2

即使变量的值是nil,内部还是使用了symbol,这个symbol永远都不会被垃圾回收。 所以除了避免根据用户输入创建方法之外,也要注意避免根据用户输入定义变量的代码出现。

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

为了保证安全,你需要周期性的看下Symbol.all_symbols.size的值(在运行完GC.start之后)来确保 symbol表没有持续增长。未来希望有一些标准告诉我们哪些使用symbol的方式是不安全的,成为一个常识。

需要在乎速度

除了安全之外,最大的原因你应该在乎的是速度。有很多代码将symbol转化成了string,为了避免根据用户输入 动态生成symbol的问题。

使用最多的应该是Rails的ActiveSupport里的HashWithIndifferentAccess类。我使用了benchmark测试了下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

结果

1
2
3
4
5
6
7
8
Calculating -------------------------------------
indifferent-string   115.962k i/100ms
indifferent-symbol    82.702k i/100ms
regular-symbol   150.856k i/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

你会发现使用HashWithIndifferentAccess通过string作为key访问的速度只有正常Hash访问速度的一半, 而使用symbol方式访问的速度比Hash访问的慢5倍。所以通过访问一个hash通过symbol访问速度是最快的, 所以目前Ruby 2.2的话,最好还是使用symbol作为parameters的key。所以没有必要 使用HashWithIndifferentAccess了。

总结

Symbol GC可以让你避免Dos攻击,可以让你更加灵活使用symbol,所以还不赶快升级Ruby到2.2。

评论