加载中

Working with Rails for some time you start nitpicking on how to improve it. This is a first in the series of articles regarding on how to improve (even marginally) Rails's performance.

I'll focus on a bunch of gems that speed up, in some cases considerably, small parts of Rails, like the html escaping, the String#blank? and JSON utils.

Benchmarking methodology

Methodology is a strong word for just running a couple of times in the console wrk but I'm not searching for the holy grail here, just to get a raw idea.

I switched from the old apache ab to wrk redirect:

wrk is a modern HTTP benchmarking tool capable of generating significant
 load when run on a single multi-core CPU.

wrk -t10 -c10 -d10s http://localhost:3000

This runs a benchmark for 10 seconds, using 10 threads, and keeping 50 HTTP connections open i.e. this should suffice. Just remember to benchmark on your actual app to see the real improvements.

使用Rails一段时间之后,你可能就会开始吹毛求疵的想要提高它性能。这是一系列文章中第一次考虑如何提高(即使微不足道的)Rails的性能。

我将会关注在一些gem的提速上面,在某些情况下,可能是一小部分的Rails,如html转义,String.blank?和JSON工具类。

基准原则

原则,对于仅仅在控制台wrk运行几次来讲,是一个与其过强的词语,但是我这里不是来寻找“圣杯”的,而是提供一些初始的想法。

我将从旧的apache ab切换到wrk

wrk是现代的 HTTP 基准工具,当在一个单一的多核 CPU 上运行时,能够产生巨大的负载。
wrk -t10 -c10 -d10s http://localhost:3000

这条指令运行基准问题10s,使用10个线程,并且保持打开50个HTTP链接,也就是说,这样就足够了。记得将这些基准测试在你实际的应用中跑一下,看一下实际上的性能提高有多少。

The escape_utils gem

Just faster all html escaping via the lovely escape_utils redirect gem. In order to use it in Rails one needs to add an initializer that patches things up:

begin
  require 'escape_utils/html/rack' # to patch Rack::Utils
  require 'escape_utils/html/erb' # to patch ERB::Util
  require 'escape_utils/html/cgi' # to patch CGI
  require 'escape_utils/html/haml' # to patch Haml::Helpers
rescue LoadError
  Rails.logger.info 'Escape_utils is not in the gemfile'
end

The logic to test it:

def escape_utils
  @escape_me = <<-HTML
    <body class="application articles_show">
      <!-- Responsive navigation
      ==================================================== -->
      <div class="container">
        <nav id="nav">
      <ul>
        <li><a href="/"><i class="ss-standard ss-home"></i>home</a></li>
        <li><a href="/home/about"><i class="ss-standard ss-info"></i>about</a></li>
        <li><a href="/contact"><i class="ss-standard ss-ellipsischat"></i>contact</a></li>
        <li><a href="/home/projects"><i class="ss-standard ss-fork"></i>projects</a></li>
        <li><a href="/tags"><i class="ss-standard ss-tag"></i>tags</a></li>
        <li><a href="/articles?query=code"><i class="ss-standard ss-search"></i>search</a></li>
      </ul>
    </nav>
    <a href="#" class="ss-standard ss-list" id="nav-toggle" aria-hidden="true"></a>
  HTML

  render inline: "Hello  world <%= @escape_me %>"
end

With standard Rails:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    35.40ms    3.55ms  64.70ms   91.98%
    Req/Sec   142.19     11.68   164.00     83.12%
  2837 requests in 10.00s, 4.92MB read
Requests/sec:    283.61
Transfer/sec:    503.34KB

With the escape_utils gem:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    34.06ms    3.89ms  63.92ms   89.10%
    Req/Sec   148.65     13.36   180.00     75.94%
  2960 requests in 10.00s, 5.46MB read
Requests/sec:    295.98
Transfer/sec:    558.72KB

escape_utils gem

通过可爱的escape_utils gem可以加快HTML的转义。为了使其能够在Rails中使用,需要添加一个初始值设定来解决:

begin
  require 'escape_utils/html/rack' # to patch Rack::Utils
  require 'escape_utils/html/erb' # to patch ERB::Util
  require 'escape_utils/html/cgi' # to patch CGI
  require 'escape_utils/html/haml' # to patch Haml::Helpers
rescue LoadError
  Rails.logger.info 'Escape_utils is not in the gemfile'
end

对该逻辑进行测试的用例:

def escape_utils
  @escape_me = <<-HTML
    <body class="application articles_show">
      <!-- Responsive navigation
      ==================================================== -->
      <div class="container">
        <nav id="nav">
      <ul>
        <li><a href="/"><i class="ss-standard ss-home"></i>home</a></li>
        <li><a href="/home/about"><i class="ss-standard ss-info"></i>about</a></li>
        <li><a href="/contact"><i class="ss-standard ss-ellipsischat"></i>contact</a></li>
        <li><a href="/home/projects"><i class="ss-standard ss-fork"></i>projects</a></li>
        <li><a href="/tags"><i class="ss-standard ss-tag"></i>tags</a></li>
        <li><a href="/articles?query=code"><i class="ss-standard ss-search"></i>search</a></li>
      </ul>
    </nav>
    <a href="#" class="ss-standard ss-list" id="nav-toggle" aria-hidden="true"></a>
  HTML

  render inline: "Hello  world <%= @escape_me %>"
end

使用标准Rails:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    35.40ms    3.55ms  64.70ms   91.98%
    Req/Sec   142.19     11.68   164.00     83.12%
  2837 requests in 10.00s, 4.92MB read
Requests/sec:    283.61
Transfer/sec:    503.34KB

使用escape_utils gem:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    34.06ms    3.89ms  63.92ms   89.10%
    Req/Sec   148.65     13.36   180.00     75.94%
  2960 requests in 10.00s, 5.46MB read
Requests/sec:    295.98
Transfer/sec:    558.72KB

The fast_blank gem

Living under the impression that the blank? method is too slow? say no more and just try the fast_blank redirect gem!

Just add gem 'fast_blank' to your Gemfile and this should speed up quite nicely the String#blank? method as described in this article redirect. For testing I just added this code:

fast_blank is a simple extension which provides a fast implementation of active support's string#blank? function

 def fast_blank_test
    n = 1000

    strings = [
      "",
      "\r\n\r\n  ",
      "this is a test",
      "   this is a longer test",
      "   this is a longer test
      this is a longer test
      this is a longer test
      this is a longer test
      this is a longer test"
    ]

    Benchmark.bmbm  do |x|
      strings.each do |s|
        x.report("Fast Blank #{s.length}    :") do
          n.times { s.blank? }
        end
      end
    end

    render nothing: true
  end

With standard Rails:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.40s   207.72ms   1.58s    92.68%
    Req/Sec     3.10      2.11     6.00     53.66%
  69 requests in 10.01s, 33.08KB read
Requests/sec:      6.90
Transfer/sec:      3.31KB

With the fast_blank gem:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.33s   179.56ms   1.41s    93.33%
    Req/Sec     3.07      0.80     4.00     40.00%
  72 requests in 10.00s, 34.52KB read
Requests/sec:      7.20
Transfer/sec:      3.45KB

fast_blank gem

是否在印象里,blank?方法太慢?不用多说,试一下fast_blank gem!

仅需要在你的Gemfile中添加gem 'fast_blank',这应该就可以非常漂亮的提高像这篇文章中提到的String.black?方法的速度。为了测试,我仅添加下俩代码:

fast_blank是一个简单的扩展,提供了一个支持String.blank?功能的快速实现。
 def fast_blank_test
    n = 1000

    strings = [
      "",
      "\r\n\r\n  ",
      "this is a test",
      "   this is a longer test",
      "   this is a longer test
      this is a longer test
      this is a longer test
      this is a longer test
      this is a longer test"
    ]

    Benchmark.bmbm  do |x|
      strings.each do |s|
        x.report("Fast Blank #{s.length}    :") do
          n.times { s.blank? }
        end
      end
    end

    render nothing: true
  end

使用标准Rails:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.40s   207.72ms   1.58s    92.68%
    Req/Sec     3.10      2.11     6.00     53.66%
  69 requests in 10.01s, 33.08KB read
Requests/sec:      6.90
Transfer/sec:      3.31KB

使用fast_blank gem:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.33s   179.56ms   1.41s    93.33%
    Req/Sec     3.07      0.80     4.00     40.00%
  72 requests in 10.00s, 34.52KB read
Requests/sec:      7.20
Transfer/sec:      3.45KB

The oj gem

# oj gem
gem 'oj'
gem 'oj_mimic_json' # we need this for Rails 4.1.x

The test logic is simple, just serialize all articles into JSON:

class SidechannelsController < ApplicationController
  def oj
    render json: Article.all
  end
end

With standard Rails serializers:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   108.37ms    5.12ms 134.90ms   83.33%
    Req/Sec    45.76      3.60    55.00     57.69%
  922 requests in 10.00s, 57.41MB read
Requests/sec:     92.17
Transfer/sec:      5.74MB

With oj gem:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    78.06ms    4.43ms  92.83ms   81.31%
    Req/Sec    63.64      5.33    71.00     64.49%
  1277 requests in 10.00s, 79.83MB read
Requests/sec:    127.65
Transfer/sec:      7.98MB

Using jemalloc

OK, this is not really a gem, if you want to dig into it then do check out my gist redirect. On initial testing it won't yield much performance gains, at least for my use case.

note: it will be included by default in Ruby at some point.

update: do try the jemalloc redirect gem by kzk:

gem install jemalloc

je -v rails s

Dig into your Rails app

Fear not and use MiniProfiler redirect with the awesome FlameGraphs redirect  by Sam Saffron.

oj gem

# oj gem
gem 'oj'
gem 'oj_mimic_json' # we need this for Rails 4.1.x

这个测试用例非常简单,仅仅将所有的article序列化为JSON格式:

class SidechannelsController < ApplicationController
  def oj
    render json: Article.all
  end
end

使用标准Rails序列化器:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   108.37ms    5.12ms 134.90ms   83.33%
    Req/Sec    45.76      3.60    55.00     57.69%
  922 requests in 10.00s, 57.41MB read
Requests/sec:     92.17
Transfer/sec:      5.74MB

使用oj gem:

Running 10s test @ http://localhost:3000/sidechannels/bench
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    78.06ms    4.43ms  92.83ms   81.31%
    Req/Sec    63.64      5.33    71.00     64.49%
  1277 requests in 10.00s, 79.83MB read
Requests/sec:    127.65
Transfer/sec:      7.98MB

使用jemalloc

好吧,这其实不是一个真正的gem,如果你想深入探究它,可以看我的这篇文章。在初始测试时,jemalloc并没有产生太多性能的提升,至少对我使用的测试用例是这样的。

提示:某些情况下,可能会默认包含在Ruby中。

更新:请一定尝试一下kzk的jemalloc gem

gem install jemalloc

je -v rails s

深入探究你的Rails应用

不要担心,去用一下Sam Saffron的带有非常棒的FlameGraphsMiniProfiler吧!

Conclusion

Depending on what your app is doing you might want to add to your Gemfile some of these gems, I usually add them all just for good measure (you might want to check your RAM usage and have a full test suite before doing this though).

The oj gem is just great for a Rails based JSON API where you can drop the views and just serialize using representers or your pattern of choice.

结语

鉴于你的应用要做什么,你可能想为你的Gemfile添加上述的一些gem。通常我会把他们都添加上,当然是出于一个好的估量(你可能会想检查你的RAM利用率,然后在添加之前,进行一个完整的测试)。

oj gem基于JSON API,对Rails来说是非常不错的,使用oj gem,你可以删除视图并仅使用代言人或者你选择的模式进行序列化。

返回顶部
顶部