31st Aug, 2010

抢答开源

抢答本身是抢座的表单模块,但是由于Google Form在国内无法正常使用,所有我们将其剥离了出来,作为一个单独的产品运营,经过这段时间的运营,我们收到了许多用户反馈,其中大部分是功能需求,但是由于我们的人力有限,这些功能一直没有做,所以我们决定将抢答的代码开源出来,希望能够借助社区的力量让这个产品更加的完善,也希望能够对需要此功能的朋友有所帮助。

源码地址: http://github.com/yzhang/thundersurvey
许可证: GPLv2

抢答是基于Rails 3和MongoDB,因此你需要首先安装mongodb,然后执行下面的命令即可:

$ git clone http://github.com/yzhang/thundersurvey
$ bundle install
$ sudo mongod
$ rails s

如果有问题或者建议,欢迎和我联系。

最近需要给抢座增加图表功能,于是对Rails的图表方案做了些调查,整理成这篇贴,希望对有同样需求的人有所帮助。

Google Chart

Google Chart的优点是接口简单,容易上手,但是缺点也很明显,需要依赖Google的服务,在有些地方加载可能会存在速度问题,并且有被GFW的风险。

Google Chart的Rails插件很多,个人比较喜欢mattetti的googlecharts:

Google Visualization

Google Visualization是Google Chart的升级版,Rails插件:http://github.com/mwarnock/rails-google-visualization-plugin

Gruff

Gruff是基于RMagick的Ruby绘图工具,功能强大,可扩展性和可定制性都非常好,不过由于Gruff需要在服务端实时生成图片,因此不适合实时展示数据。

Gruff还有一个JS封装bluff:http://bluff.jcoglan.com/

Flot

Flot是一个jQuery插件,可以基于JS生成实时图表,如果你使用jQuery,那么Flot是个不错的选择。Flot有一个很好用的Rails插件插件Flotilla: http://github.com/joshuamiller/flotilla

GNUPlot

GNUPlot最初是为学术目的使用的数据可视化工具,但是现在也支持生成非交互式的2d和3d图形,如果你需要生成非常复杂的图形,那么可以试试gnuplot,GNUPlot有一个叫rgplot的rubygem可以使用:http://rgplot.rubyforge.org

Ziya

Ziya是一个Rails图表插件,可以生成非常漂亮和复杂的Flash图表,如果你不介意Flash,那么Ziya是个不错的选择。

Scruffy

Scruffy是一个Rubygem,它的目标是帮你生成高质量的SVG和PNG图片,同Gruff一样,是一个服务端解决方案,不过Scruffy目前似乎已经停止开发了,首页的图片在Firefox和Chrome下均无法显示。

Highcharts

感谢dxiao同学留言,Highchars是一个纯JS的图表库,但是实现的效果却和Flash不相上下,相当绚丽,兼容性也很不错,并且有一个Rails的插件可以用,值得一试。

Flash

除了上面介绍的Ziya,Flash图表解决方案还有很多,下面列举几个对Rails支持比较好的:

参考:http://stackoverflow.com/questions/87561/what-is-your-preferred-way-to-produce-charts-in-a-ruby-on-rails-web-application

mongodb的map/reduce是个十分强大的功能,mongo_mapper虽然已经接近于1.0,但还是少一些比较有用的功能,比如对一个字段求和,取最大,最小值等,不过你可以很容易的通过mongodb的map/reduce为mongo_mapper扩展这些功能,甚至是更复杂的,下面以sum为例:


class Model
  def self.sum(field)
    m = "function () {emit('sum', this.#{field.to_s})}"
    r = "function(k, vals) { var sum = 0; for(var i in vals){sum += vals[i];}; return sum;}"
    res = self.collection.map_reduce(m, r)
    return res.find().next_document['value'].to_i
  end
end

最近多背一公斤的网站由于前段时间加了太多功能,但是没有考虑性能问题,导致速度下降,内存占用剧增,apache频频重启,需要进行优化,于是花了点时间学习了下rails的优化,整理成这篇文章,希望对遇到同样问题的人有用。

基本原则

大部分讲优化的文章都会在开头讲到优化的基本原则:

1. 不要过早优化,这里必须要提到Donald Knuth的名言:过早的优化是万恶之源,Premature optimization is the root of all evil,在你的应用还没有遇到性能问题之前,不要为性能担忧,也无需为优化浪费时间,只有当性能成为问题时才进行优化,这是第一原则。

2. 基准测试是必须的,没有基准测试,优化的效果就无从衡量,所有优化的第一步都应该是基准测试。

3. 要适可而止,不要追求极致。性能的优化实际上是个没有尽头的事情,没有任何一个应用可以在性能方面达到完美,只要你愿意,总是能找到可以优化的地方,但是这里要进行权衡,如果你的团队花了1个月的时间进行优化,而性能仅仅提高了10%,那么不如添置一台服务器,一台服务器的成本肯定要远远小于你的团队一个月的工资,另外,也没有必要为了一些可有可无的优化而牺牲代码的可读性,什么是可有可无的优化?比如直接写a标签肯定比link_to要快,调用 helper肯定也不如将所有代码写在view中快,但这样的优化得不偿失,没有意义。

看过了这些原则,如果你还认为你的应用需要优化,那就接着往下看吧!

request-log-analytizer

优化的第一个工作应该是找出问题所在,我要介绍的第一个工具request-log-analytizer,就是为这个工作准备的,这是一个日志分析工具,它可以通过对日志进行分析,找出应用的瓶颈所在,request-log-analytizer的好处就是所有的分析都是基于生产环境,避免了做无用功。

request-log-analytizer的使用非常简单:


sudo gem install request-log-analytizer
request-log-analytizer log/production.log

现在你就可以在命令行看到日志分析结果了,为了方便查看,request-log-analytizer也支持以HTML格式输出结果:

request-log-analytizer -f log.html –output html log/production.log

用浏览器打开log.html,可以看到最上面是摘要,告诉你日志中总共有多少个请求,以及请求的时间跨度,下面一个部分是每个时间段的平均请求数,然后就是被请求次数最多的20个URL的请求信息,接下来是按照请求方法和响应分类的请求信息。

在这些信息的后面,就是我们关心的性能信息了,这里有6块,总处理时间最长的请求,平均处理时间最长的请求,总View渲染时间最长的请求,平均View渲染时间最长的请求,总数据库查询时间最长的请求以及平均数据库查询时间最长的请求,通过这6个部分,我们就可以了解到应用的性能瓶颈,在这6块后面,request-log-analytizer还列出了平均处理时间超过1秒的请求,这些应该是优化的重点,如下图,我们可以看出,1公斤的网站需要优化的请求是如此之多。

有了目标,下面就让我们来挨个处理它们。

Benchmark

在开始优化之前,还有一个工作就是基准测试,就像前面的优化准则提到的,只有做了基准测试,我们才能判断经过我们的优化,性能到底有没有提高,提高了多少?

Rails 从2.2开始内置了Benchmarker和Profiler工具,如果你新建一个Rails应用,在script/performance目录下就可以看到它们,你也可以通过performance_test这个generator来生成新的性能测试用例,关于Rails的性能测试,请参考Rails的官方指南(http://guides.rubyonrails.org/performance_testing.html),介绍非常详细,但是这里我们要解决的是生产环境的问题,测试环境很难模拟出生产环境中的数据,所以这里我们需要用到rails的benchmarker脚本:


$ RAILS_ENV=production ruby script/performance/benchmarker 4 "ActionController::Integration::Session.new.get_via_redirect('/activities')"
user system total real
#1 2.720000 0.040000 2.760000 ( 4.861908)

这条命令需要说明一下,首先我们设置benchmarker运行在production环境,第一个参数4是告诉benchmarker脚本的运行次数,第二个参数就是我们的测试,ActionController::Integration::Session是Rails提供的集成测试框架,可以用来模拟用户请求,这里我们看到,未优化前,处理4个/activities请求花去了2.76秒的时间,现在我们就有了一个判断的标准,后面我们就可以通过这条命令来检验我们的优化提高了多少性能。

不过不幸的是,Rails自从2.3.4版本开始,引入了一个Bug,导致这条命令不能在Mac系统运行,如果你使用的是mac系统,可以将Rails降回2.3.4以下版本,或者直接在console运行:


Benchmark.bm do |x|
  x.report { 4.times { app.get(“/activities”) }}
end

除了benchmarker,rails还内置了一个profiler脚本,是对ruby-prof的封装,你也可以用这个工具对你的action进行profiler测试,看看到底是那个函数调用浪费了时间:


$ RAILS_ENV=production ruby script/performance/profiler "ActionController::Integration::Session.new.get_via_redirect('/activities')"

query_reviewer

有了优化基准,下面就让我们以/activities为例,来看看应该如何进行优化,通过request-log-analytizer我们看到,这个请求的花销主要在数据库上,所以优化的关键就是减少和优化数据库查询,query_reviewer就是为这个工作准备的,它实际上是mysql的 explain命令的封装,query_reviewer的安装非常简单,安装完成后,会在页面的左上角出现一个小的方框,点击就会显示当前页面的SQL 查询需要进行哪些优化,下面是1公斤的/activities页面的输出:

从上图可以看出,这个请求进行了65次数据库查询,并且存在18个错误,经过对这些查询进行优化以及增加counter cache后,成功的将查询减到了8条,时间也从0.534减到了0.012,速度提高了44倍:

bullet

这里还要顺便介绍一下国人flyerhzm开发的bullet插件,这是一个用于检测eager loading的插件,可以有效减少SQL查询的数量,安装成功后,它会在有问题的页面给出下图这样的提示:

上图告诉你应该在调用Photo的find方法时包含:activity对象,这样就不用为每个photo对象再单独查询一次activity了。

好了,经过上面的优化,让我们再来运行benchmarker检验一下效果吧:


$ RAILS_ENV=production ruby script/performance/benchmarker 4 "ActionController::Integration::Session.new.get_via_redirect('/activities')"
user system total real
#1 0.680000 0.000000 0.680000 ( 0.696291)

可以看到,经过这番优化,对/activities页面的请求从2.76秒降到了0.68秒,提高了超过2秒,单个请求提高了0.5秒,任务完成。

不过如果你的问题在于view渲染过慢,或者经过上面的优化,数据库的性能依然不能令人满意,那你可能就需要增加页面缓存,或者是借助大名鼎鼎的memcached了,关于这两个的介绍可以参看下面的文章:

Rails缓存:http://railsenvy.com/2007/2/28/rails-caching-tutorial
Rails Memcached:http://nubyonrails.com/articles/memcached-basics-for-rails

logrotate是Linux系统的日志打包程序,通过cron运行,要使用logrotate打包你的rails日志,只需要在/etc/logrotate.d/目录下为你的rails应用创建一个配置文件即可,配置文件的语法如下:


/path/to/your/app/log/production.log {
  compress
  weekly
  rotate 10
  create 0600 user group
  missingok
  # Use bzip2 for compress.
  compresscmd /usr/bin/bzip2
  uncompresscmd /usr/bin/bunzip2
  compressoptions -9
  compressext .bz2
  postrotate
    cd /path/to/your/app && touch tmp/restart.txt
  endscript
}

weekly表示每周打包一次,你也可以根据需要改为:daily, monthly, yearly,或者通过size来设置当日志超过某个尺寸后进行打包,rotate用于指定要保存的日志文件个数,最后的postrotate用于指定日志打包后需要执行的命令,如果你使用passenger,上面的例子告诉passenger重启rails应用以使用新的日志文件,更多的参数可以参看logrotate的帮助:http://linux.die.net/man/8/logrotate

在保存后最好通过下面的命令手动测试一下你的配置文件,以确保没有错误。


$ sudo /usr/sbin/logrotate /etc/logrotate.d/yourapp -v
rotating pattern: /path/to/your/app/log/production.log weekly (10 rotations)
empty log files are rotated, old logs are removed
considering log /path/to/your/app/log/production.log
  log does not need rotating
not running postrotate script, since no logs were rotated

如果你的配置正确,会看到类似上面的输出,否则会看到错误提示。

Rails 2.3引入的防止跨站攻击的功能在某些情况下会导致用户正常使用时也产生InvalidAuthenticityToken异常,比如某个表单需要登录后才能提交,但是用户在登录后,过了很长一段时间才提交,此时Session已经失效,就会导致这个异常,有些人会直接忽略验证AuthenticityToken的filter,但是这样有安全隐患,比较好的处理方法就是在ApplicationController中增加一个全局Handler:


class ApplicationController < ActionController::Base &nsp; rescue_from ActionController::InvalidAuthenticityToken, :with => :bad_token
   def bad_token
    flash[:notice] = "Your session has expired."
    respond_to do |accepts|
      accepts.html do
        store_location
        redirect_to(:controller => '/sessions', :action => 'new') and return false
       end
       accepts.js do
        store_location
        render :update do |page|
          page.redirect_to(:controller => '/sessions', :action => 'new') and return false
        end
      end
    end
  end
end

来自:http://gist.github.com/94801

10th Apr, 2010

ar_mailer Rails 3 插件

ar_mailer是个很不错的邮件发送插件,工作原理是将邮件发送分为两部分,首先将邮件保存到数据库,然后在调用后台程序实现异步发送,可惜的是目前还不支持Rails 3,于是花了一个下午,将它移植到了Rails 3,这就是:ar_mailer_rails3

安装

sudo gem install ar_mailer_rails

使用

script/rails generate ar_mailer_rails3 Email

你也可以将Email替换为其它名字,邮件将会被保存到这个Model中。

Generator会自动生成一个initializer:


# config/initializers/ar_mailer.rb
ActionMailer::Base.add_delivery_method :active_record,
    ArMailerRails3::ActiveRecord, :email_class => Email
ActionMailer::Base.delivery_method = :active_record

如果你不希望所有Mailer都使用ActiveRecord发送,也可以在Mailer中单独定义:


class MyMailer < ActionMailer::Base
   self.delivery_method = :active_record
end

命令行的使用方式和ar_mailer是一样的,只不过为了和ar_mailer 2保持兼容,命令行的程序变成了ar_mailer_rails3:

$ ar_sendmail_rails3 -h

详细的使用方法可参看这里

7th Apr, 2010

豆瓣API Rails 插件

目前只支持Rails 3 beta,地址:http://github.com/yzhang/douban

安装

$ gem install douban

使用

1. 修改Gemfile:


gem 'douban', :require => 'oauth'
require 'douban'

2. 运行generator

$ script/rails generate douban

3. 修改config/douban.yml, 并在ApplicationController中包含DoubanHelper:


class ApplicationController
  include DoubanHelper
end

4. 然后在你的application layout中加入:


<% if douban_authorized? %>
  <%= link_to '注销豆瓣登录', douban_logout_path %>
<% else %>
  <%= link_to "使用豆瓣帐号登录", douban_login_path %>
<% end %>

还可以使用下面的Helper:

  • douban_auth_or_login_required, 方便和RESTFUL_AUTH一起使用
  • douban_auth_required, 可以作为before_filter使用

5. 要访问用户资源,可以调用:


douban.get('/people/yzhang')
douban.post('/reviews', review)

6. 要验证用户是否已授权,可以调用:


douban.authorized?
=> true # 如果已经获得access token

更新: Mail 2.2 已解决此问题

如果你升级到了Rails 3 Beta,并且使用了ActionMailer,那么你会发现邮件标题出现有规律的乱码,每个8的倍数的地方都出现了奇怪的字符,这是ActionMailer和Mail gem配合出现了问题,解决的办法很简单,修改ActionMailer::Base#quote_fields,将这个函数改成如下:


def quote_fields!(headers, charset) #:nodoc:
  m = @_message
  m.subject ||= headers[:subject] if headers[:subject]
  m.to ||= headers[:to] if headers[:to]
  m.from ||= headers[:from] if headers[:from]
  m.cc ||= headers[:cc] if headers[:cc]
  m.bcc ||= headers[:bcc] if headers[:bcc]
  m.reply_to ||= headers[:reply_to] if headers[:reply_to]
end

给Rails Core提了一个Patch,但是因为考虑不周,没有通过,所以官方的解决方案恐怕要等到Mail 2.2或者Rails 3 Beta1。

26th Feb, 2010

升级到Rails 3 Beta

安装


gem install tzinfo builder memcache-client rack rack-test rack-mount erubis mail text-format thor bundler i18n
gem install rails --pre

这里会遇到一个rack-mount依赖问题,可以通过手动安装0.4.0版的rack-mount解决:

sudo gem install rack-mount -v=0.4.0

升级应用

rails_upgrade(http://github.com/rails/rails_upgrade)是Rails官方的Rails3升级辅助插件,它提供了几个Rake任务帮助你轻松完成升级。

1. 执行 rails:upgrade:backup 备份会被Rails 3覆盖的文件,比如:application_controller.rb会被重命名为application_controller.rb.rails2
2. 执行 rake rails:upgrade:routes 生成Rails 3格式的routes.rb,以及rake rails:upgrade:gems生成新的Gemfile
3. 在你的程序目录执行: rails . 注意不要覆盖routes.rb和Gemfile

接下来要做的就是将备份的rails2文件合并到新生成的文件中,再次启动应用,如果幸运的话,你的应用现在已经运行在Rails 3 beta上了,注意检查Log中的Deprecated Warning,并将其替换为Rails 3的方式,关于Rails 3的新特性请参看Release Notes:http://guides.rails.info/3_0_release_notes.html

ActionView

Rails 3重写了View Helper,并作废了以前的link_to_function, link_to_remote, remote_form_for等AJAX Helper,而改为为link_to, form_for增加:remote => true来实现。

link_to_function(text, function) => link_to(text, ‘#’, function)
link_to_remote(text, {:url => url}) => link_to(text, url, :remote => true)
remote_form_for(object) => form_for(object, :remote => true)

Passenger

Passenger从2.2.9版本开始支持Rails 3,如果你的版本小于2.2.9,请先升级,另外需要注意的是,Rails 3会在你的应用目录下创建一个config.ru文件,如果需要以development模式运行你的应用,需要做点改变:

1. 在你的apache配置中加入: RackEnv development
2. 删除config.ru,这样passenger会继续使用RailsEnv

jQuery

Rails 3默认是使用Prototype的,如果要使用jQuery,jquery-ujs(http://github.com/rails/jquery-ujs.git)项目提供了jQuery版本的rails.js,下载并替换默认的rails.js,然后在 application.html.erb的head部分增加:


<meta name="csrf-token" content="<%= form_authenticity_token %>" />
<meta name="csrf-param" content="authenticity_token" />

Paperclip

Paperclip目前还不支持Rails 3,不过Github上已经有了一个Rails 3的分支,我们可以直接使用这个分支:


git submodule add -b rails3 git://github.com/thoughtbot/paperclip.git vendor/plugins/paperclip
git submodule init

但是这个分支目前工作也不正常,还需要打一个补丁:


# in lib/paperclip/attachment.rb at line 293
def callback which #:nodoc:
  # replace this line...
  # instance.run_callbacks(which, @queued_for_write){|result,obj| result == false }
  # with this:
  instance.run_callbacks(which, @queued_for_write)
end

此处参考:http://jameswilding.net/2010/02/07/paperclip-on-rails-3-beta/