ruby on rails
Ruby语言因其灵活性而经常被引用。 正如Dick Sites所说,您可以“编写程序来编写程序”。 Ruby on Rails扩展了核心Ruby语言,但是Ruby本身使扩展性成为可能。 Ruby on Rails利用该语言的灵活性,可以轻松地编写高度结构化的程序,而无需太多样板代码或额外的代码:您无需进行任何额外工作即可获得大量标准行为。 尽管这种自由行为并不总是完美的,但是您无需进行大量工作即可在应用程序中获得许多良好的体系结构。
例如,Ruby on Rails基于模型-视图-控制器(MVC)模式,这意味着大多数Rails应用程序被清晰地分为三部分。 该模型包含管理应用程序数据所需的行为。 通常,在Ruby on Rails应用程序中,模型与数据库表之间存在1:1的关系。 ActiveRecord
是Ruby on Rails默认使用的对象关系映射(ORM),用于管理模型与数据库的交互,这意味着一般的Ruby on Rails程序几乎没有SQL编码。 第二部分,视图,由创建发送给用户的输出的代码组成。 它通常由HTML,JavaScript等组成。最后一部分是控制器,它将来自用户的输入转换为对正确模型的调用,然后使用适当的视图呈现响应。
支持Rails的人经常引用MVC范例以及Ruby和Rails的其他优点,以提高其易用性,声称更少的程序员可以在更短的时间内产生更多的功能。 当然,这意味着每花一美元的软件开发就能获得更多的商业价值,因此Ruby on Rails的开发已变得越来越受欢迎。
但是,最初的开发成本并不是全部。 还有其他持续成本,例如维护成本和运行该应用程序的硬件成本。 Ruby on Rails开发人员经常使用测试和其他敏捷开发技术来降低维护成本,但是可以很轻松地将注意力转移到大量数据的高效运行Rails应用程序上。 尽管Rails使访问数据库变得容易,但它并不总是那么有效。
为什么Rails应用程序运行缓慢?
Rails应用程序运行缓慢的原因可能很少。 首先很简单:Rails为您做出假设以加快开发速度。 通常,这些假设是正确且有用的。 但是,它们并不总是对性能有利,并且可能导致资源(尤其是数据库资源)的低效使用。
例如,默认情况下, ActiveRecord
使用等效于SELECT *
SQL语句SELECT *
查询中的所有字段。 在具有大量列的情况下,尤其是在某些列是较大的VARCHAR
或BLOB
字段的情况下,就内存使用和性能而言,此行为可能是一个严重的问题。
另一个重大挑战是N +1问题,本文将对其进行详细研究。 本质上,这导致执行许多小查询,而不是一个大查询。 ActiveRecord
无法知道例如正在为一组父记录中的每个父记录请求一个子记录,因此它将为每个父记录产生一个子记录查询。 由于每个查询的开销,此行为可能会导致严重的性能问题。
其他挑战与Ruby on Rails开发人员的开发习惯和态度更紧密相关。 因为ActiveRecord
使这么多任务变得如此容易,所以Rails开发人员经常可以以“ SQL不好”的态度,即使在更有意义的情况下也可以避开SQL。 创建和处理大量ActiveRecord
对象可能会很慢,因此在某些情况下,直接编写不实例化任何对象SQL查询会更快。
由于Ruby on Rails通常用于减少开发团队的规模,并且由于Ruby on Rails开发人员经常执行在生产中部署和维护其应用程序所需的一些系统管理任务,因此对环境的有限了解可能会引起问题。 操作系统和数据库设置可能未正确设置。 尽管不是最佳选择,但例如,在Ruby on Rails部署中,MySQL my.cnf设置通常保留为默认设置。 此外,可能没有足够的监视和基准测试工具来长期了解性能。 当然,这并不是对Ruby on Rails开发人员的批评。 这仅仅是非专业化的结果; 在某些情况下,Rails开发人员可能是这两个领域的专家。
最后一个问题是Ruby on Rails鼓励程序员在本地环境中进行开发。 这样做有很多好处,例如减少开发延迟和增加分发,但是这确实意味着您可以使用有限的数据集,因为工作站的大小较小。 它们的开发方式与代码部署位置之间的差异可能是一个大问题。 您可能在卸载的本地服务器上以较小的数据量工作了很长时间,并且性能良好,只是发现应用程序在拥挤的服务器上具有较大的数据量时,会遇到严重的性能问题。
当然,Rails应用程序存在性能问题的原因还有很多。 找出Rails应用程序潜在性能问题的最佳方法是查看诊断工具,这些工具可以为您提供准确,可重复的测量。
检测性能问题
最好的工具之一是Rails开发日志,它位于log / development.log文件中的每台开发计算机上。 它具有各种可用的总指标:响应请求所花费的总时间,在数据库中花费的时间百分比,在生成视图时花费的时间百分比等。可使用各种工具为您分析日志文件,例如development-log-analyzer
。
在生产期间,您可以通过检查mysql_slow_log
来找到有价值的信息。 完整的细节不在本讨论的范围之内,但是您可以在“ 相关主题”部分中找到更多信息。
其中一个最强大和最有用的工具是query_reviewer
插件(参见相关主题 )。 该插件向您显示页面上正在执行多少个查询以及页面花费了多长时间。 并且它会自动分析ActiveRecord
生成SQL代码是否存在潜在问题。 例如,它找到不使用MySQL索引的,因此,如果您忘记了索引一个重要的列,是造成你的性能问题的查询,你可以很容易地找到它(请参阅相关的主题为有关MySQL索引的更多信息)。 插件在弹出的<div>
中显示所有这些信息,仅在开发模式下可见。
最后,不要忘记使用Firebug, yslow
,Ping和tracert
来检测性能问题可能来自网络还是资产加载问题。
接下来,让我们处理一些特定的Rails性能问题及其解决方案。
N +1查询问题
N +1查询问题是Rails应用程序最大的问题之一。 例如,清单1中的代码产生多少查询? 这段代码是一个假设的帖子表中所有帖子的简单循环,显示了帖子的类别和正文。
清单1.未优化的Post.all代码
<%@posts = Post.all(@posts).each do |p|%>
<h1><%=p.category.name%></h1>
<p><%=p.body%></p>
<%end%>
答:该代码生成一个查询,然后在@posts
每行生成一个查询。 由于每个查询的开销,这可能是一个巨大的挑战。 罪魁祸首是对p.category.name
的调用。 该调用仅适用于该特定的post对象,不适用于整个@posts
数组。 幸运的是,您可以使用快速加载来解决此问题。
预先加载意味着Rails将自动执行必要的查询以加载任何指定子对象的对象。 Rails将使用JOIN
SQL语句或执行多个查询的策略。 但是,假设您指定了将要使用的所有子代,则永远不会导致N +1的情况,在这种情况下,循环的每次迭代都会产生一个附加查询。 清单2是清单1中代码的一个版本,它使用紧急加载来避免N +1问题。
清单2.急于加载的优化的Post.all代码
<%@posts = Post.find(:all, :include=>[:category]
@posts.each do |p|%>
<h1><%=p.category.name%></h1>
<p><%=p.body%></p>
<%end%>
该代码最多生成两个查询,无论您在posts表中有多少行。
当然,并非所有情况都如此简单。 处理更复杂的N +1查询情况需要做更多的工作。 值得付出努力吗? 让我们进行一些快速测试。
测试N +1
使用清单3中的脚本,您可以发现查询有多慢(或快)。 清单3演示了如何在独立脚本中使用ActiveRecord
建立数据库连接,定义表以及加载数据。 然后,您可以使用Ruby的内置基准测试库来查看哪种方法更快,效率更高。
清单3.急于加载的基准脚本
require 'rubygems'
require 'faker'
require 'active_record'
require 'benchmark'
# This call creates a connection to our database.
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "127.0.0.1",
:username => "root", # Note that while this is the default setting for MySQL,
:password => "", # a properly secured system will have a different MySQL
# username and password, and if so, you'll need to
# change these settings.
:database => "test")
# First, set up our database...
class Category < ActiveRecord::Base
end
unless Category.table_exists?
ActiveRecord::Schema.define do
create_table :categories do |t|
t.column :name, :string
end
end
end
Category.create(:name=>'Sara Campbell\'s Stuff')
Category.create(:name=>'Jake Moran\'s Possessions')
Category.create(:name=>'Josh\'s Items')
number_of_categories = Category.count
class Item < ActiveRecord::Base
belongs_to :category
end
# If the table doesn't exist, we'll create it.
unless Item.table_exists?
ActiveRecord::Schema.define do
create_table :items do |t|
t.column :name, :string
t.column :category_id, :integer
end
end
end
puts "Loading data..."
item_count = Item.count
item_table_size = 10000
if item_count < item_table_size
(item_table_size - item_count).times do
Item.create!(:name=>Faker.name,
:category_id=>(1+rand(number_of_categories.to_i)))
end
end
puts "Running tests..."
Benchmark.bm do |x|
[100,1000,10000].each do |size|
x.report "size:#{size}, with n+1 problem" do
@items=Item.find(:all, :limit=>size)
@items.each do |i|
i.category
end
end
x.report "size:#{size}, with :include" do
@items=Item.find(:all, :include=>:category, :limit=>size)
@items.each do |i|
i.category
end
end
end
end
该脚本使用:include
子句测试有无加载的循环100、1,000和10,000个对象的速度。 要运行此脚本,您可能需要用适合您的本地环境的参数替换脚本顶部附近的适当的数据库连接参数。 您还需要创建一个名为test的MySQL数据库。 最后,您需要ActiveRecord
和faker
gem,可以通过运行gem install activerecord faker
。
在我的机器上运行脚本产生的结果如清单4所示。
清单4.急于加载的基准脚本输出
-- create_table(:categories)
-> 0.1327s
-- create_table(:items)
-> 0.1215s
Loading data...
Running tests...
user system total real
size:100, with n+1 problem 0.030000 0.000000 0.030000 ( 0.045996)
size:100, with :include 0.010000 0.000000 0.010000 ( 0.009164)
size:1000, with n+1 problem 0.260000 0.040000 0.300000 ( 0.346721)
size:1000, with :include 0.060000 0.010000 0.070000 ( 0.076739)
size:10000, with n+1 problem 3.110000 0.380000 3.490000 ( 3.935518)
size:10000, with :include 0.470000 0.080000 0.550000 ( 0.573861)
在所有情况下,使用:include
的测试都更快-分别是5.02、4.52和6.86倍。 当然,确切的结果取决于您的特定情况,但是急切的加载显然可以显着提高性能。
嵌套的渴望加载
如果要引用嵌套关系-关系的关系怎么办? 清单5演示了一个共同的情况下这样的事情可能会发生:通过所有帖子循环并显示图像的作者,其中Author
有belongs_to
有关系Image
。
清单5.嵌套的渴望加载用例
@posts = Post.all
@posts.each do |p|
<h1><%=p.category.name%></h1>
<%=image_tag p.author.image.public_filename %>
<p><%=p.body%>
<%end%>
该代码与以前存在同样的N +1问题,但是该修补程序的语法并不立即明显,因为您正在使用关系的关系。 那么,您如何渴望加载嵌套关系呢?
正确的答案是对:include
子句使用哈希语法。 清单6提供了一个使用哈希值嵌套嵌套的紧急加载的示例。
清单6.嵌套的紧急加载解决方案
@posts = Post.find(:all, :include=>{ :category=>[],
:author=>{ :image=>[]}} )
@posts.each do |p|
<h1><%=p.category.name%></h1>
<%=image_tag p.author.image.public_filename %>
<p><%=p.body%>
<%end%>
如您所见,您可以嵌套哈希和数组文字。 请注意,在这种情况下,哈希和数组之间的唯一区别是哈希可以具有嵌套的子项,而数组则不能。 否则,它们是等效的。
间接渴望加载
并非所有的N +1问题实例都一样容易理解。 例如,清单7产生多少个查询?
清单7.间接渴望加载示例用例
<%@user = User.find(5)
@user.posts.each do |p|%>
<%=render :partial=>'posts/summary', :locals=>:post=>p
%> <%end%>
当然,确定查询数量需要了解posts/summary
部分。 您可以在清单8中看到部分。
清单8.间接地预先加载部分内容:posts / _summary.html.erb
<h1><%=post.user.name%></h1>
不幸的是,答案是清单7和清单8在post
每一行中生成了一个额外的查询,查询用户名-即使post
对象是由ActiveRecord
从内存中的User
对象自动生成的。 简而言之,Rails到目前为止尚未将儿童记录与父母相关联。
解决方法是使用自我引用的预先加载。 本质上,由于Rails会重新加载由父记录生成的子记录,因此您需要像加载父记录一样是一个完全独立的关系来急于加载它们。 看起来像清单9中的代码。
清单9.间接的预先加载解决方案
<%@user = User.find(5, :include=>{:posts=>[:user]})
...snip...
尽管违反直觉,但该技术的工作原理与上述技术非常相似。 不幸的是,使用这种技术很容易过度嵌套,特别是在层次结构复杂的情况下。 简单的用例就可以了,如清单9所示,但是大量的嵌套可能会引起问题。 在某些情况下,Ruby对象的过度加载实际上比处理N +1问题要慢-尤其是在每个对象都没有遍历整个树的情况下。 在那种情况下,针对N +1问题的其他解决方案可能更合适。
一种方法是使用缓存技术。 Rails V2.1具有内置的简单缓存访问权限。使用Rails.cache.read
, Rails.cache.write
和相关方法,可以轻松创建自己的简单缓存机制,并且后端可以是简单的内存后端,基于文件的后端或内存缓存服务器。 您可以在“ 相关主题”部分中找到有关Rails内置缓存支持的更多信息。 不过,您无需创建自己的缓存解决方案; 您可以使用预制的Rails插件,例如Nick Kallen的cache money
插件。 该插件提供直写式缓存,并且基于Twitter上使用的代码。 请参阅相关主题以获取更多信息。
当然,并非所有的Rails问题都与查询数量有关。
Rails分组和聚合计算
您可能会遇到的一个问题涉及在Ruby中进行应由数据库完成的工作。 这证明了Ruby有多么强大。 很难想象人们会在没有明显动机的情况下自愿地用C重新实现其数据库代码的一部分,但是很容易对Rails中的ActiveRecord
对象组进行类似的计算。 不幸的是,Ruby总是比您的数据库代码慢。 不要使用纯Ruby方法执行计算,如清单10所示。
清单10.执行分组计算的错误方法
all_ages = Person.find(:all).group_by(&:age).keys.uniq
oldest_age = Person.find(:all).max
相反,Rails为您提供了一系列分组和聚合功能。 如清单11所示使用它们。
清单11.执行分组计算的正确方法
all_ages = Person.find(:all, :group=>[:age])
oldest_age = Person.calcuate(:max, :age)
ActiveRecord::Base#find
有许多选项可用于模拟SQL。 您可以在Rails文档中找到更多信息。 请注意, calculate
方法可与数据库支持的任何有效聚合函数一起使用,例如:min
, :sum
和:avg
。 此外, calculate
可以采用许多参数,例如:conditions
。 有关详细信息,请查看Rails文档。
但是,并非您在SQL中可以做的所有事情都可以在Rails中完成。 如果内置功能还不够,请使用自定义SQL。
使用Rails自定义SQL
假设您有一张表格,其中列出了过去一年中所涉及的人员,他们的职业,年龄以及所发生的事故数量。 您可以使用定制SQL语句来检索信息,如清单12所示。
清单12.带ActiveRecord
自定义SQL示例
sql = "SELECT profession,
AVG(age) as average_age,
AVG(accident_count)
FROM persons
GROUP
BY profession"
Person.find_by_sql(sql).each do |row|
puts "#{row.profession}, " <<
"avg. age: #{row.average_age}, " <<
"avg. accidents: #{row.average_accident_count}"
end
该脚本将产生类似于清单13的结果。
清单13.带有ActiveRecord
输出的自定义SQL
Programmer, avg. age: 18.010, avg. accidents: 9
System Administrator, avg. age: 22.720, avg. accidents: 8
当然,这是一个简单的例子。 但是,您可以想象如何将示例扩展到任何复杂SQL语句。 您还可以使用ActiveRecord::Base.connection.execute
方法运行其他类型SQL语句,例如ALTER TABLE
语句,如清单14所示。
清单14.带有ActiveRecord的自定义非查找器SQL
ActiveRecord::Base.connection.execute "ALTER TABLE some_table CHANGE COLUMN..."
大多数模式操作,例如添加和删除列,都可以使用Rails的内置方法来完成。 但是,如果需要,可以使用执行任意SQL代码的功能。
结论
像所有框架一样,Ruby on Rails可能会在没有适当注意和注意的情况下遭受一些性能问题的困扰。 幸运的是,监视和纠正这些挑战的正确技术相对简单易学,甚至可以通过一些耐心和对性能问题根源的了解来解决复杂的问题。
翻译自: https://www.ibm.com/developerworks/opensource/library/os-railsn1/index.html
ruby on rails