在优化你的应用之前,我们首先需要明确以下几点:
- 不先进行性能测试就盲目的优化是非常愚蠢的
- 如果你的应用是因为设计不合理而导致性能低下,那么我建议你最好花点时间重构你的代码,而不是进行局部的优化,这只会使问题越来越多。
- 在优化之前,最好先为自己树立一个目标,这样可以防止因为过度优化而浪费时间,达到预期的目标后就该适可而止
- 没有必要对每一个页面都进行优化,只需要关注那些最经常被访问的页面就可以了,
- 在开发期间,进行持续的性能测量,这样有助于你在优化时定位性能瓶颈。
- 延迟,响应一个请求需要多少时间
- 吞吐量,每秒最多可以处理多少个请求
- 系统利用率,在大量请求需要处理的时候,你的系统在满负荷运转吗?
- 资源开销,在每个请求上所花费的开销
- Rails日志文件(debug_level >= Logger::DEBUG)
- Rails日志分析工具(需要将日志输出到syslog)
- Rails基准脚本(script/benchmarker)
- 数据库提供的性能分析器
- Apache Bench(ab或者ab2)
- httperf
- railsbench
除了基准测试工具,你也可以选择单纯的性能测试工具:
- Ruby profiler
- Zen profiler
- rubyprof
- Rails profiler script
- Ruby Performance Validator(商业软件,仅支持windows)
工具已经搞定,下面就让我们开始我们的优化之旅吧!
根据我的经验,Rails性能问题一般集中在以下几个方面:
- 很慢的helper方法
- 负责的路由
- 过多的联合(associations)
- 过多访问数据库
- 缓慢的session存取
不过,数据库的性能基本可以不用考虑,因为连接数据库的主要开销事实上在于建立ActiveRecord对象。
这一讲就到这里,下一讲我们将针对以上几个问题给出具体的优化方案。
=================================================================================
在开始之前,有一点需要说明,优化策略事实上大部分情况下都不具备通用性,因为软硬件差异,用户使用习惯等等原因,可能会造成同一条优化策略在不同系统中得到完全不同的效果,当然这里讲的都是一些具有普遍适用性的策略,但我还是建议你在应用这些策略时进行一下对比测试,就像第一讲开头所说,不要盲目优化。
Session优化
如果你的系统需要为每个访问者保存单独的Session信息(比如购物网站),那么session的存取速度将是影响系统性能的关键因素,目前可用的session存取策略有:
Rails默认支持一下集中Cache方式:
使用Components会对ActionController的性能造成较大的影响,我的建议是没有特别的理由,不要使用components,因为调用render_component会引发一个新的请求处理循环,大部分情况下,component都可以使用helper或者partials代替。
ActionView
对于每一个请求,Rails都会创建一个controller和view实例,并会将controller的action中创建的实例变量通过instance_variable_get和instance_variable_set传递给view,因此不要在action中创建view中用不到的实例变量
helper优化
首先是pluralize,可以看一下pluralize的实现,如果不给出最后一个参数,它会创建一个Inflector实例,因此不要写pluralize(n, ‘post’),应该写成pluralize(n, ‘post’, ‘posts’)
其次是link_to与url_for,由于需要查找路由策略,因此link_to与url_for可以说是最慢的helper方法,没有特别的需要,不要使用这两个函数。
<a href="/recipe/edit/<%=#{recipe.id}%>" class="edit_link">
look here for something interesting
</a>
会比下面这段快许多:
<%= link to ”look here for something interesting” ,
{ :controller => ”recipe”, :action => edit, :id => @recipe.id },
{ :class => ” edit link ” } %>
ActiveRecord
访问AR对象的关联对象相对而言会比较慢,可以使用:include提前获取关联对象
class Article
belongs to :author
end
Article . find ( :all , :include => :author)
或者使用piggy backing指定要获取的关联对象的某些字段,关于piggy backing的介绍请参看 这篇文章
class Article
piggy back :author name, :from => :author, :attributes => [:name]
end
article = Article . find ( :all , :piggy => :author)
puts article .author name
另外需要注意的是,从数据库中获取的字段值一般来说都是String类型,因此每次访问可能都需要进行类型转换,如果你在一个请求处理过程中需要进行多次转换,那么最好对转换后的值进行缓存。
还有,根据我对一个应用的分析,大约有30%的时间花在了字符处理上,另外30%时间花在了垃圾收集,还有10%用于URL识别,因此在数据库中缓存格式化后的字段可以大大减小字符处理的开销。
这一讲就先到这里,更多的优化技巧,请关注后续文章。
skyover at 2007-7-02 10:50:30
Session优化
如果你的系统需要为每个访问者保存单独的Session信息(比如购物网站),那么session的存取速度将是影响系统性能的关键因素,目前可用的session存取策略有:
- 内存,快,相当快!但是如果你的应用挂了,或者由于其它什么原因需要重启,那么所有的session信息都会丢失,并且这种方式仅仅只能在单APP Server的应用中使用
- 文件系统,很容易使用,每个session对应一个文件,并且可以通过NFS或者NAS轻松进行容量扩展,但是速度较慢
- 数据库/ActiveRecordStore,使用简单(Rails的默认策略),但是很慢
- 数据库/SQLSessionStore,与上面一种方式类似,但是使用原始SQL取代了ActiveRecord,性能有一定提升,关于SQLSessionStore与ActiveRecordStore的对比可以参看这篇文章
- memcached,比SQLSessionStore稍微快一些,可扩展性较好,但是较难获取统计信息,关于memcached与SQLSessionStore的对比,请参看这篇文章
- DrbStore,在memcached不支持的一些平台上,可以选择DrbStore,但是性能比memcached要差一些,并且不支持session自动清除。
Rails默认支持一下集中Cache方式:
- Pages,很快,整个页面都被保存在文件系统,Web Server可以直接绕过APP Server完成请求应答,但是存在一些固有缺陷,比如无法应付需要用户登录的应用
- Actions,第二快,缓存controller的action执行结果,同时由于可以调用到controller的过滤器,因此可以很好的防止未授权用户访问。
- Fragment,缓存请求结果的一部分,也可以感知用户是否登录
- 内存,最快的方式,如果你的程序只需要在一个APP Server上运行,那么这无疑是最好的方式
- 文件系统,一般快,但可以使用正则表达式来刷新过期页面
- DrbStore,同文件系统相比,刷新过期页面更为快一些
- memcached,比DrbStore更快且易于扩展,但不支持使用正则刷新过期页面
使用Components会对ActionController的性能造成较大的影响,我的建议是没有特别的理由,不要使用components,因为调用render_component会引发一个新的请求处理循环,大部分情况下,component都可以使用helper或者partials代替。
ActionView
对于每一个请求,Rails都会创建一个controller和view实例,并会将controller的action中创建的实例变量通过instance_variable_get和instance_variable_set传递给view,因此不要在action中创建view中用不到的实例变量
helper优化
首先是pluralize,可以看一下pluralize的实现,如果不给出最后一个参数,它会创建一个Inflector实例,因此不要写pluralize(n, ‘post’),应该写成pluralize(n, ‘post’, ‘posts’)
其次是link_to与url_for,由于需要查找路由策略,因此link_to与url_for可以说是最慢的helper方法,没有特别的需要,不要使用这两个函数。
<a href="/recipe/edit/<%=#{recipe.id}%>" class="edit_link">
look here for something interesting
</a>
会比下面这段快许多:
<%= link to ”look here for something interesting” ,
{ :controller => ”recipe”, :action => edit, :id => @recipe.id },
{ :class => ” edit link ” } %>
ActiveRecord
访问AR对象的关联对象相对而言会比较慢,可以使用:include提前获取关联对象
class Article
belongs to :author
end
Article . find ( :all , :include => :author)
或者使用piggy backing指定要获取的关联对象的某些字段,关于piggy backing的介绍请参看 这篇文章
class Article
piggy back :author name, :from => :author, :attributes => [:name]
end
article = Article . find ( :all , :piggy => :author)
puts article .author name
另外需要注意的是,从数据库中获取的字段值一般来说都是String类型,因此每次访问可能都需要进行类型转换,如果你在一个请求处理过程中需要进行多次转换,那么最好对转换后的值进行缓存。
还有,根据我对一个应用的分析,大约有30%的时间花在了字符处理上,另外30%时间花在了垃圾收集,还有10%用于URL识别,因此在数据库中缓存格式化后的字段可以大大减小字符处理的开销。
这一讲就先到这里,更多的优化技巧,请关注后续文章。
上一讲,我们的优化策略主要是针对Rails框架进行,这一讲,我们将精力集中到Ruby语言本身。
首先,Ruby语言中的各种元素由于算法的不同,访问时间也各不相等,比如局部变量采用数组索引,在解析时进行顶问,因此访问代价总是O(1),而实例变量和和方法调用由于使用Hash访问,因此只能保持理论上的O(1)访问,也就是没有冲突的情况下,同时调用方法时如果不能在子类找到这个方法,则还需要沿继承树向上回溯查找。
因此,应该尽量避免不必要的多态继承,同时应该尽量使用局部变量,比如下面这段代码的效率就不如修改后的高:
def submit to remote(name, value, options = {})
options[ :with ] ||= ’Form.serialize( this .form)’
options[:html ] ||= {}
options[:html ][ :type ] = ’button’
options[:html ][ :onclick ] = ”#{remote function(options)}; return false ; ”
options[:html ][ :name] = name
options[:html ][ :value] = value
tag(”input” , options[:html ], false )
end
修改后:
def submit to remote(name, value, options = {})
options[ :with ] ||= ’Form.serialize( this .form)’
html = (options[:html ] ||= {})
html[:type ] = ’button’
html[ :onclick ] = ”#{remote function(options)}; return false ; ”
html[:name] = name
html[:value] = value
tag(”input” , html, false )
end
其次,对于经常用到的数据,应该进行缓存,避免每次用到时再进行计算,比如:
def capital_letters
( ”A” .. ”Z” ). to a
end
写成下面这样会更好:
def capital letters
@capital letters ||= ( ”A” .. ”Z” ). to a
end
当然对于上面这种情况,如果所有类需要的数据都相同,那么完全可以将它定义成class级变量:
@@capital letters = (”A” .. ”Z” ). to a
def capital letters
@@capital letters
end
当然,除了效率也要注意优美,下面这段代码就不够优美:
def actions
unless @actions
# do something complicated and costly to determine action’s value
@actions = expr
end
@actions
end
改成这样会更好一些:
def actions
@actions ||=
begin
# do something complicated and costly to determine action’s value
expr
end
end
另外,使用常量对效率也有一定提升。
def validate_find_options (options)
options.assert valid keys( :conditions , :include , :joins , :limit , :offset ,
;:order , :select , :readonly, :group, :from )
end
上面这段代码进行如下修改会更好一些:
VALID FIND OPTIONS = [
;:conditions , :include , :joins , :limit ,
;:offset , :order , :select , :readonly, :group, :from ]
def validate find options (options)
options.assert valid keys(VALID FIND OPTIONS)
end
同时,应该尽可能的使用局部变量。
sql << ” GROUP BY #{options[:group]} ” if options[:group]
上面这种方式明显不如以下两种:
if opts = options[:group]
sql << ” GROUP BY #{opts} ”
end
opts = options[:group] and sql << ” GROUP BY #{opts} ”
当然,能够写成这样是最好的:
sql << ” GROUP BY #{opts} ” if opts = options[:group]
但是语法不支持。
还有一些小技巧:
logger.debug ”args: #{hash.keys.sort.join ( ’ ’ )}” if logger
这段代码的问题在于,不管logger.level是否为DEBUG,hash.keys.sort.join(’ ’) 都会被执行,因此,应该写成这样:
logger.debug ”args: #{hash.keys.sort.join ( ’ ’ )}” if logger && logger.debug?
还有就是关于ObjectSpace.each_object的,在production模式最好不要使用这个方法。
ObjectSpace.each object(Class) {|c| f(c) }
事实上跟下面的代码是相等的:
ObjectSpace.each object {|o| o.is a?(Class) && f(o) }
它会对堆上的每一个对象都进行检查,这会对性能造成极大损耗。
好了,关于Ruby语言的优化技巧就这么多了,下一讲我们将对Ruby的垃圾回收(GC)机制进行分析,并给出相应的优化策略。
skyover at 2007-7-02 10:51:09
首先,Ruby语言中的各种元素由于算法的不同,访问时间也各不相等,比如局部变量采用数组索引,在解析时进行顶问,因此访问代价总是O(1),而实例变量和和方法调用由于使用Hash访问,因此只能保持理论上的O(1)访问,也就是没有冲突的情况下,同时调用方法时如果不能在子类找到这个方法,则还需要沿继承树向上回溯查找。
因此,应该尽量避免不必要的多态继承,同时应该尽量使用局部变量,比如下面这段代码的效率就不如修改后的高:
def submit to remote(name, value, options = {})
options[ :with ] ||= ’Form.serialize( this .form)’
options[:html ] ||= {}
options[:html ][ :type ] = ’button’
options[:html ][ :onclick ] = ”#{remote function(options)}; return false ; ”
options[:html ][ :name] = name
options[:html ][ :value] = value
tag(”input” , options[:html ], false )
end
修改后:
def submit to remote(name, value, options = {})
options[ :with ] ||= ’Form.serialize( this .form)’
html = (options[:html ] ||= {})
html[:type ] = ’button’
html[ :onclick ] = ”#{remote function(options)}; return false ; ”
html[:name] = name
html[:value] = value
tag(”input” , html, false )
end
其次,对于经常用到的数据,应该进行缓存,避免每次用到时再进行计算,比如:
def capital_letters
( ”A” .. ”Z” ). to a
end
写成下面这样会更好:
def capital letters
@capital letters ||= ( ”A” .. ”Z” ). to a
end
当然对于上面这种情况,如果所有类需要的数据都相同,那么完全可以将它定义成class级变量:
@@capital letters = (”A” .. ”Z” ). to a
def capital letters
@@capital letters
end
当然,除了效率也要注意优美,下面这段代码就不够优美:
def actions
unless @actions
# do something complicated and costly to determine action’s value
@actions = expr
end
@actions
end
改成这样会更好一些:
def actions
@actions ||=
begin
# do something complicated and costly to determine action’s value
expr
end
end
另外,使用常量对效率也有一定提升。
def validate_find_options (options)
options.assert valid keys( :conditions , :include , :joins , :limit , :offset ,
;:order , :select , :readonly, :group, :from )
end
上面这段代码进行如下修改会更好一些:
VALID FIND OPTIONS = [
;:conditions , :include , :joins , :limit ,
;:offset , :order , :select , :readonly, :group, :from ]
def validate find options (options)
options.assert valid keys(VALID FIND OPTIONS)
end
同时,应该尽可能的使用局部变量。
sql << ” GROUP BY #{options[:group]} ” if options[:group]
上面这种方式明显不如以下两种:
if opts = options[:group]
sql << ” GROUP BY #{opts} ”
end
opts = options[:group] and sql << ” GROUP BY #{opts} ”
当然,能够写成这样是最好的:
sql << ” GROUP BY #{opts} ” if opts = options[:group]
但是语法不支持。
还有一些小技巧:
logger.debug ”args: #{hash.keys.sort.join ( ’ ’ )}” if logger
这段代码的问题在于,不管logger.level是否为DEBUG,hash.keys.sort.join(’ ’) 都会被执行,因此,应该写成这样:
logger.debug ”args: #{hash.keys.sort.join ( ’ ’ )}” if logger && logger.debug?
还有就是关于ObjectSpace.each_object的,在production模式最好不要使用这个方法。
ObjectSpace.each object(Class) {|c| f(c) }
事实上跟下面的代码是相等的:
ObjectSpace.each object {|o| o.is a?(Class) && f(o) }
它会对堆上的每一个对象都进行检查,这会对性能造成极大损耗。
好了,关于Ruby语言的优化技巧就这么多了,下一讲我们将对Ruby的垃圾回收(GC)机制进行分析,并给出相应的优化策略。
上一讲我们讲解了如何通过优化Ruby代码来提升我们的Rails应用性能,这一讲,让我们更深入一些,先看看Ruby的内存管理和垃圾回收机制。
首先,由于Ruby最初的设计目标是成为像Perl那样的批处理语言,因此它的内存管理机制并没有针对Rails这样的需要长期运行的服务端程序进行最优化,有些地方甚至是背道而驰:
并且,Ruby的清除算法依赖于堆的大小,而不是当前非垃圾区的大小,但是堆的增长存在一定限制,只有当进行GC后,当前的freelist< FREE_MIN,堆才会增加,gc.c中定义的增加值为4096,这对于Rails来说明显太小了,堆应该至少能够容纳20万个对象。
要提高Ruby GC的性能,可以在Rails dispatcher中添加如下语句:
# excerpt from dispatch. fcgi
RailsFCGIHandler.process! nil, 50
这句话将禁止Ruby GC运行,在处理50个请求后再启用GC,但是这个方法存在一个问题,它没法区分小请求和大请求,这有可能会导致:
RUBY_HEAP_MIN_SLOTS = 600000
RUBY_GC_MALLOC_LIMIT = 60000000
RUBY_HEAP_FREE_MIN = 100000
如果你进行基准测试的话,就会发现性能提高不少。
最后,我们再讲讲模板优化,对于许多在编译时就知道结果的helper方法,完全没有必要在每次处理请求时都进行解析,比如:
<%= end_form_tag %> ===> </form>
这纯粹就是浪费时间,还有我们前面提到的link_to,因此,如果我们可以在敲代码时确定这个helper的输出,那么最好直接写出结果。
另外,还可以使用Ryan Davis的ParseTree和ruby2ruby来获取ActionView的render方法的AST,并进行模板终极优化:
好了,到这里,这一系列的文章就结束了,附录中还有一些性能测试的数据和配置信息,就不一一列举了,感兴趣的可以 下载原文查看。
首先,由于Ruby最初的设计目标是成为像Perl那样的批处理语言,因此它的内存管理机制并没有针对Rails这样的需要长期运行的服务端程序进行最优化,有些地方甚至是背道而驰:
- Rails的内存管理策略是尽量减少内存占用
- 标记和清除算法十分简单
- 使用malloc来分配连续的内存块(Ruby heap)
- 复杂的数据结构
- C扩展十分容易编写,但是当前的C接口很难实现generational GC(Ruby2有可能)
并且,Ruby的清除算法依赖于堆的大小,而不是当前非垃圾区的大小,但是堆的增长存在一定限制,只有当进行GC后,当前的freelist< FREE_MIN,堆才会增加,gc.c中定义的增加值为4096,这对于Rails来说明显太小了,堆应该至少能够容纳20万个对象。
要提高Ruby GC的性能,可以在Rails dispatcher中添加如下语句:
# excerpt from dispatch. fcgi
RailsFCGIHandler.process! nil, 50
这句话将禁止Ruby GC运行,在处理50个请求后再启用GC,但是这个方法存在一个问题,它没法区分小请求和大请求,这有可能会导致:
- 堆变的过大
- 小页面的性能会受损
- Ruby will still deallocate heap blocks if empty after GC(这句不是很理解,上原文)
- RUBY_HEAP_MIN_SLOTS, 初始堆大小,默认10000
- RUBY HEAP FREE MIN,GC后可用的heap slot的最小值,默认4096
- RUBY GC MALLOC LIMIT,允许不触发GC而分配的C数据结构的最大值(字节为单位),默认8,000,000
RUBY_HEAP_MIN_SLOTS = 600000
RUBY_GC_MALLOC_LIMIT = 60000000
RUBY_HEAP_FREE_MIN = 100000
如果你进行基准测试的话,就会发现性能提高不少。
最后,我们再讲讲模板优化,对于许多在编译时就知道结果的helper方法,完全没有必要在每次处理请求时都进行解析,比如:
<%= end_form_tag %> ===> </form>
这纯粹就是浪费时间,还有我们前面提到的link_to,因此,如果我们可以在敲代码时确定这个helper的输出,那么最好直接写出结果。
另外,还可以使用Ryan Davis的ParseTree和ruby2ruby来获取ActionView的render方法的AST,并进行模板终极优化:
- 展开所有helper方法
- 去除不会调用到的代码
- 去除不会用到的变量(以及partials)
- 合并hash
- 替换常量
- 替换结果已确定的方法调用
- 替换符号
好了,到这里,这一系列的文章就结束了,附录中还有一些性能测试的数据和配置信息,就不一一列举了,感兴趣的可以 下载原文查看。