Rails 常见性能问题一览
作者 Stefan Kaes译者 Jason Lai 发布于 2007年3月26日 上午6时48分
Ruby 主题 Web框架, 性能和扩展性, Ruby on Rails
在最近几个月里,我从性能问题的角度,分析了不少 Rails 应用程序(里面有一些牵涉到我的咨询业务,另外一些则是开源应用)。这些应用程序面向的多个领域之间存在着诸多差异,导致每项性能调优任务都颇具挑战性。然而,它们之间还是存在不少共性,使得我们得以提炼出不少出现问题的地方,正是它们导致了很多应用程序难以达到高性能。它们包括:
- 选用了缓慢的 Session 容器
- 本可在启动时一次性完成的操作被放在每次请求中执行
- 请求处理过程中重复相同运算
- 频繁从数据库加载过多数据(尤其在使用关联进行连接的情况下)
- 过分依赖于低效 helper 方法
除此之外,Rails 框架自身依然存在某些问题区(problem areas),它们的性能是我所希望在今后改善的。里面有一部分可以在应用程序级别解决,另外一部分则无能为力。下面是我比较感兴趣的问题:
- Route 识别和 Route生成
- ActiveRecord 对象的构造
- SQL 查询的构造
我会在下文中罗列出针对以上问题的部分编码技巧。
一些忠告
遵循本文的建议,有可能改善你的应用程序的性能,但也可能行不通。性能调优是件费脑筋的事情,尤其在实现语言结构的性能特征没有被明确规范的时候,更是如此(在 Ruby 里面就是这种情况)。
我强烈建议,在进行任何变更之前,先评测一下你的应用程序原先的性能,之后每次变更后再次进行评测。我编写了一个包,叫做 railsbench,这是一个很不错的性能回归测试工具。它简单易用,只需几分钟你就可以得到性能的基准数据。不过比较遗憾,它无法告诉你,在你的应用程序里时间到底花费在哪里(译注:此外,你的应用程序所调用的 Rails 框架的代码也是有时间开销的)。
假如你手头有台运行 Windows 的机器(或者一台带双启动的 Intel Mac),我建议你去试试 Software Verification Ltd.(SVL)的 Ruby Performance Validator(RPVL)。对于我深及 Rails 框架内核的 Rails 性能调优工作,特别是在我所建议的在已有热点视图基础上的调用图功能被 SVL 实现之后,我发现它真是功勋卓著。据我所知,它是目前市面上唯一的 Ruby 应用程序性能分析工具。Railsbench 内建了对 RPVL 的支持,这样也使得在 RPVL 之下运行被 Railsbench 定义的基准变得易如反掌。
选用 Session 容器
Rails 提供了几个内建的 Session 容器。在所有我分析过的应用程序里,要么使用了将 Session 信息储存在你文件系统上独立文件的 PStore,要么用了和数据库打交道的 ActiveRecordStore。这两个方案都不甚理想,特别是拖累了缓存 Action 的页面(action cached pages)。这里提供两个好用得多的备选方案供大家参考:SQLSessionStore 和 MemCacheStore。
SQLSessionStore 通过以下手段避免了 ActiveRecordStore 相关的额外性能开支:
- 避免使用事务(对于 SQLSessionStore 的正确操作,它们并不是必要的)
- 撤销向数据库更新“created_at”和“updated_at”的操作
如果使用 MySQL,你应当保证使用 MyISAM 表来存储 Session 数据,因为它要比 InnoDB 表快很多,并且它不会强制你使用事务处理。前不久我又为 SQLSessionStore 增加了 Postgres 支持,不过,用于 Session 存储 Postgres 看起来要比 MySQL 慢得多。因此,如果你打算使用基于数据库的 Session 存储,我推荐为 Session 表安装 MySQL 数据库(我想不出一个基于 session id 的需要连接的更好用例(use case))。
MemCacheStore 要比 SQLSessionStore快得更多。我的测评结果显示,对于缓存了 Action 的页面它能够带来了 30% 的速度提升 。你得先安装 Eric Hodel 的 memcache client ,并在 environment.rb 中做相应配置之后才能正常使用。注意:Ruby-Memcache 还是不要去试的好(实在、实在慢得让人难以忍受)。
在我自己的项目中,我更倾向于使用基于数据库的 Session 存储,原因是可以使用 Rails 命令行或者数据库软件包提供的管理工具进行简单得管理。对于 MemCacheStore 你就得自己为它编写脚本了。另一方面,对于高访问量网站来说,内存缓存方式的扩展性更好一些,并且它随支持 Session 超时(session expiry)的 Rails 一起提供。
在请求过程中缓存运算结果
假如你在处理一次请求过程中不止一次需要某些相同的数据,但由于你的数据以某种形式依赖于请求参数,而不能使用类级别缓存的时候,那么请将数据缓存起来,以避免重复计算。
要采取这种模式很简单:
module M
def get_data_for(request)
@cached_data_for_request ||=
begin
expensive computation depending on request returning data
end
end
end
你的代码可以是简单的 "A".."Z".to_a ,或者是一个数据库查询,例如取得一个指定的用户等。
在启动或者首次访问时执行与请求无关的运算
这个性能忠告太简单了,我都有点儿犹豫是不是还该把它放在本文中谈论——然而,我还是发现很多我分析过的应用程序并没有采取这项有效的优化技术。
事实上,这个技术再简单不过了:如果你的应用程序里面存在哪些数据,在整个生命周期里都不会发生改变,或者数据改变之少到发生变更之后进行一次服务器重启就可以解决问题的话,那么,请把这些数据缓存在你的应用程序中某个类适当的类变量中。常见的范例如下:
class C
@@cached_data = nil
def self.cached_data
@@cached_data ||= expensive computation returning data
end
...
end
实际的例子有:
- 应用程序的配置数据(如果你把应用程序设计成其他人可以安装的方式)
- 数据库中恒定不变的(静态的)数据(否则使用缓存)
- 使用 ObjectSpace.each 检测已安装的 Ruby 类/模块
优化查询
Rails 提供了功能强大的领域特定语言(Domain Specific Language),用来定义模型类之间反映数据表关系的关联。哎,可惜目前的实现仍然没有对性能进行优化。依赖于内建生成的访问器会严重地影响性能。p>
首当其冲的问题就是常被大家称作“1 + N”查询的问题:如果你从类 Article(数据表“articles”)加载 N 个对象,而类 Article 到类 Author(数据表“authors”)之间存在多对一的关系,使用生成访问器方法访问一个特定的 Article 将会触发 N 个额外的数据库查询。这自然而然就给数据库带来了额外负担,不过对于 Rails 应用服务器性能来说,更重要的是,要提交的 SQL 查询语句会为已经访问过的对象被重新进行构造。
要解决这个额外开销,你可以像下面一样在你的查询参数中添加一个 :include => :author:
Articles.find(:all, :conditions => ..., :include => :author
这样一来,通过提交单独的 SQL 语句以及立即构造 Author 对象的方式,上述额外开销都能得以避免。这项技术常常被称为“贪婪关联查找(find with eager associations)”,并且它对于其它关系类型同样适用(比如一对一、一对多或者多对多关联)。p>
尽管如此,我们还可以使用一项叫做“携带加载(piggy backing)”的技术来进一步优化多对一关系:来自原数据表的属性连同连接表的属性均由带表连接的 ActiveRecord 对象。因此,一条带连接的查询就可以从数据库全数抓取所需的信息。假设你的视图要显示的只是关联到文章信息的作者名称,你可以把上面的查询替换成:
Articles.find(:all, :conditions => ...,
:joins => "LEFT JOIN authors ON articles.author_id=authors.id",
:select => "articles.*, authors.name AS author_name")
此外,假如你的视图显示的是可用的文章字段的一个子集,譬如“title”、“author_id”和“created_at”,那么你得把上述代码改成:
Articles.find(:all, :conditions => ...,
:joins => "LEFT JOIN authors ON articles.author_id=authors.id",
:select => "articles.id, articles.title, articles.created_at, articles.author_id, authors.name AS author_name")
一般说来,仅加载部分对象可以在一定程度上提升查询的速度,尤其在你的模型对象拥有大量字段的时候更是如此。为了使用这项技术达到全面提速,你还得在模型类中定义一个方法,用来访问查询附带的所有属性:
class Articles
...
def author_name
@attributes['author_name'] ||= author.name
end
end
当你编写视图代码时,使用这个模式,你就可以不必去了解原来的查询是否存在连接了。
假如你的数据库支持视图,你可以定义一个仅包含必要信息的视图,这样就不必手动编写复杂的查询代码了。这么做的另外一条好处是,可以为你从连接表内加载的字段获得正确的数据类型转换。到目前为止,Rails 尚未提供这些功能,你必须手动为它们编写代码。
给“潮流前沿的伙计们”的一点补充:翻来覆去捣腾这些相同的模式真是让我颇为厌倦,所以我编写了一些扩展代码用来自动完成大部分这类任务。请到我的博客上查看其预发行版。
避免使用笨拙迟缓的 helper 方法
在 Rails 内核中,有许多 helper 方法运行缓慢。一般而言,所有取 URL hash 作参数的 helper 方法都会调用 routing 模块,来生成引用前基础控制器 action 的最短 URL。这就意味着 route 文件中的几个 routes 应该进行检查,这个过程通常煞费时间。哪怕只是使用像下面一个简单的 route 文件:
ActionController::Routing::Routes.draw do |map|
map.connect '', :controller => "welcome"
map.connect ':controller/service.wsdl', :action => 'wsdl'
map.connect ':controller/:action/:id'
end
你将会发现写成
link_to "Look here for joke #{h @joke.title}",
{ :controller => "jokes", :action => "show", :id => @joke },
{ :class => "joke_link" }
和直接编写这样精简的HTML之间显著的性能差异:
<a href="/jokes/show/<%= @joke.id %>"
class="joke_link">Look here for joke <%= h @joke.title %></a>
对于显示大量链接的页面,我所测得的速度提升比例最高能达到 200%(前提是其它部分已经进行了优化)。
为使模板代码可读性更强,并避免不必要的重复,我通常在 application.rb 内加入生成链接的 helper 方法:
def fast_link(text, link, html_options='')
%(<a href="#{request.relative_url_root}/#{link}"> hmtl_options>#{text})
end
def joke_link(text, link)
fast_link(text, "joke/#{link}", 'class="joke_link"')
end
将上述范例改成:
joke_link "Look here for joke #{h @joke.title}", "show/<!---->"
用这种方式来解决 route 生成缓慢的问题显然过于繁琐,一般只有性能要求很高的页面才应当使用。如果你不必马上进行性能调优的话(啊呃,读起来挺像我每天收到的垃圾邮件的感觉),不妨关注我那即将问世的模板优化工具的第一个发行版本,它将在大多数情况下自动帮你完成所有此类工作。
未来 Rails 性能调优的话题
正如上文所说,route 识别和 route 生成的性能需要有些东西来协助解决。我的模板优化工具将解决 route 生成问题。上周又有一个新的 route 实现被整合进 Rails 开发分支中。我进行了一些评测,结果显示,在 route 识别方面,性能似乎得到了一些改善。但就 route 生成而言,结果则比较参差不齐。新的实现是否能改善到一直都比先前版本的速度快,尚且有待观望。
从数据库加载大量 ActiveRecord 对象相对来说是比较慢的,当然程度并不高,因为实际的数据库连接传输开销比较大。不过,由于行数据被表示成用字符串作为键值索引的 hash,在 Rails 内部构造 Ruby 对象的开销相当昂贵。转而使用基于数组的数据行类(array based row class)可以改善这个问题。然而,要做得恰如其分的话,就得牵扯到 ActiveRecord 实现的核心部分,因而我们只能寄希望它在 Rails 2.0 中推出。
最后一点,目前 SQL 查询的构造方式,使得生成查询语句的运算比从数据库获取实际数据的代价更为高昂。要大幅度改善这个现状,我的看法是,可以使绝大多数 SQL 查询只生成一次,然后代入实际参数值进行扩充。当前唯一能解决这个问题的方法只有手动编写你的查询代码。
注:以上关于追求更优性能的可行方案,是我个人的观点,并不代表任何官方“核心团队”对待这些问题的看法。
结语
上文列举的问题并不想给你一种 Rails 可能运行缓慢(乃至于我觉得它太过迟缓)的心里暗示。恰恰相反,我坚信 Rails 是一个卓越的 Web 应用开发框架,对于稳健而又快速的 Web 应用的开发大有裨益,并且带来开发效率的改善。正如所有框架一样,它提供了方便的使用方法,可以大幅度地提升你的开发速度,而且在绝大多数场合下适用于你绝大部分的需求。不过,有些时候,你必须尽可能在每一秒内腾出一些额外的请求,或者你在硬件资源上面受到一定限制,了解如何进行性能调优是大有帮助的。一旦你碰上性能问题,希望本文帮助你理清某些范围的要点,助你一臂之力。
关于作者
Stefan Kaes 是 Rails 性能方面的权威博客RailsExperess 和于 2007 年初发行的《Performance Rails》一书的作者。Stefan 的书将跻身于 Addison-Wesley 新系列丛书 Professional Ruby 的首发阵容。此系列丛书将于 2006 年底面世,包括 Hal Fulton 的《The Ruby Way》第二版,以及丛书编辑 Obie Fernandez 所著的旗舰大作《Professional Ruby on Rails》。该系列将是一套精辟入里的学习工具型丛书,从专业的高度帮助读者最大限度从 Ruby 和 Rails 中汲取精华。
分享到:
相关推荐
Ruby On Rails 框架自它提出之日起就受到广泛关注,在“不要重复自己”,“约定优于配置”等思想的指导下,Rails 带给 Web 开发者的是极高的开发效率。 ActiveRecord 的灵活让你再也不用配置繁琐的 Hibernate 即可...
如果应用设计上存在固有的性能问题,往往需要进行昂贵的重新设计。因此,事先规划好性能目标能够帮助开发者在性能改进工作上节约成本。 在进行Rails性能优化时,合理的做法是先设置一个性能目标,然后通过测量、...
7. **缓存问题**:Rails的缓存机制可以显著提升性能,但有时也会带来困扰。例如,未清除的缓存可能导致旧数据被展示。了解`Rails.cache`的各种方法,并在更新数据后正确清理或更新缓存。 8. **依赖注入问题**:...
Ruby on Rails(简称Rails),作为一个流行的Web开发框架,虽然提供了丰富的功能和快速的开发体验,但也可能会面临性能瓶颈的问题。本文将深入探讨几种优化Rails应用性能的方法,特别是针对内存管理和垃圾回收...
Ruby on Rails:Rails性能优化与缓存策略.docx
-Rails应用的性能基准:例如,Rails应用在处理请求时各个阶段的耗时,包括路由解析、模型加载、视图渲染等,这些数据对于定位性能瓶颈非常有帮助。 - 服务器资源的监控与优化:高可用的配置不仅仅是服务器的数量和...
- 常见的路由类型: 默认路由、命名路由、约束路由等。 #### 七、练习作业1-建立Group-CRUD与RESTful - **CRUD操作**: - Create (创建): 创建新的Group对象。 - Read (读取): 显示Group的信息。 - Update (更新...
5. **Rails安全**:学习如何防止常见的Web攻击,如SQL注入、跨站脚本(XSS)和跨站请求伪造(CSRF)。了解授权库如Pundit和CanCanCan,以及如何安全地处理用户输入和密码存储。 6. **部署与维护**:了解如何在各种...
对于想要深入了解Rails框架并构建高性能Web应用的开发者来说,这些知识是必不可少的。 #### 10. Rails Plugins(Rails插件) Rails插件是扩展框架功能的有效方式。本章节将介绍如何安装、使用和开发自己的Rails...
描述中提到的博文链接指向了一个ITEYE博客文章,尽管具体内容未提供,但通常这样的博客可能会包含Rails的使用技巧、最佳实践、新版本更新或者特定问题的解决方案。 标签 "源码" 暗示了可能涉及Rails的源代码分析或...
此外,书中还介绍了一些Rails中的高级概念,如Strong Parameters(强参数),它在Rails 4.0版本中引入,用于解决之前版本中的参数篡改问题,从而帮助开发者安全地处理外部提交的数据。 通过一系列的教学内容,包括...
Rails Recipes涵盖了Rails的众多方面,包括但不限于模型、视图、控制器、路由、数据库迁移、安全、性能优化、测试和部署等。 书中所提到的“隐藏的宝石”,意味着即使是经验丰富的Rails开发者也可能还没有发现或...
在开发Web应用时,Rails框架和MySQL数据库的集成是一个常见的选择。然而,有时在尝试连接Rails应用到MySQL数据库时,可能会遇到一些问题。本篇文章将深入探讨这些常见问题及其解决方案。 首先,Rails与MySQL的连接...
标题 "Rails相关电子书...此外,书中可能还涵盖了Rails的安全实践、性能优化以及与其他技术(如JavaScript库)的集成等内容。对于希望深入理解Rails框架或想要提升Web开发技能的开发者来说,这本书是一个宝贵的资源。
Ruby on Rails,通常简称为Rails,是一个基于Ruby编程语言的开源Web应用框架,遵循MVC(Model-View-Controller)架构模式。这个“Rails项目源代码”是一个使用Rails构建的图片分享网站的完整源代码,它揭示了如何...
通过提供直观的可视化界面,Skylight使得复杂的性能问题变得易于理解和解决。 在Rails开发过程中,性能优化是提升用户体验的关键环节。Ruby-Skylight 的核心功能包括: 1. **实时性能监控**:一旦集成到Rails应用...
- **Rails中的REST实现**:Rails默认遵循REST原则,通过资源路由和标准的控制器动作支持常见的CRUD操作。 - **资源和表示**:讨论如何将数据建模为资源,并如何通过不同的HTTP方法处理这些资源的不同状态。 **4. ...
Rails API的目的是为了提高性能,并减小API服务器与传统的Rails Web应用程序之间的差异。 在Rails API中,主要的亮点包括: 1. **轻量级结构**:Rails API剥离了Web展示层的组件,如Action View和Asset Pipeline,...