rack-mini-profiler 这个 gem,可以永远显示网页的加载时间。(2300✨)开发环境和产品环境都可以用。(生成非常详细的报告)
- development环境,直接使用gem 'rack-mini-profiler'
- production环境,
- 1. gem 'rack-mini-profiler', require: false ,让
- 2. 然后运行bundle exec rails g rack_profiler:install, 这样在开发环境下也可以使用。
以下服务可以收集网站实际营运的数据,找出哪些部分是效能不好的地方加以改善:
后端效能提速的方向
对后端来说,一个方向是提供 Rails 和 Ruby 代码的效能,一个方向是提供数据库方面的效能。
主要是query查询SQL的效能提升空间大。
1. 避免N + 1的query
includes():让你存取Post模型的关联对象user的属性,却不会产生额外的查询语句。用于简单的join的执行速度的改进。
可以嵌套,可以指定多个关系,可以附加where(具体看API)
对关联,可以使用:
def index
@posts = Post.includes(:user).page(params[:page])
end
SELECT "posts".* FROM "posts" LIMIT ? [["LIMIT", 2]]
SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 5)
def show
@post = Post.find(params[:id])
@comments = @post.comments.includes(:user)
end
多个关联,和加嵌套:
def index
@posts = Post.includes(:user, :liked_users, {:comments => :user}).page(params[:page])
end
{:comments => :user}是因为post关联comments,下一层comments关联了user。
<td><%= post.comments.map{ |c| c.user.display_name }%></td>
加上条件查询:
可以在include()后面直接加上where()。
使用scope(name, body, &block),预先设置查询的rails语法. 就是一个类方法。返回一个ActiveRecord::Relation。
可以把返回结果当array,用Enumerable的方法。
scope可以连接scope或类方法。
例子:
如果希望在index页面只显示公开的comments,即comment的status属性的值是public,
同时不希望产生N+1query,
改MVC模型:
则在Models/post.rb中添加:
has_many :comments, -> { where( status: "public")}, class_name: "Comment"
在Controllers/posts_controller.rb中的index方法中修改:
{:comment => :user}改为{:visible_comment => :user}
在views/posts/index.html.erb中修改:
post.comments为post.visble_comments
Bullet在开发时协助侦测 N+1 queries
gem https://github.com/flyerhzm/bullet (5500✨)
注意:在配置时,注释掉不需要的gem的功能。然后打开页面,如果遇到N+1 query就会弹出窗口。
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.alert = true #弹出提示框。
Bullet.bullet_logger = true
Bullet.console = true
Bullet.rails_logger = true #在terminal显示提升
Bullet.add_footer = true #在当前页角显示提升
end
end
activerecord内存的优化:
1.不要用all,可以使用分页的gem。 will_paginate 或 kaminari
2. 真要捞全部数据,使用批处理方法 find_each ,find_in_batches。
例子: 在数据库内新增一个字段,并赋值。
db/migrate/2017XXXXXXXXXX_add_date_to_posts.rb
class AddDateToPosts < ActiveRecord::Migration[5.1]
def change
+ add_column :posts, :date, :date
+
+ Post.find_each do |post|
+ post.date = post.created_at.to_date
+ post.save( :validate => false )
+ end
end
end
3. Preload技术,使用复杂的语法糖(复杂的Sql)一次性从数据库捞出所有想要的数据,存入了内存。
对不同的需求,使用不同的调用方法从内存中获取记录。
def show
@post = Post.find(params[:id])
if current_user
all_comments = @post.comments.where("status = ? OR (status = ? AND user_id = ?)","public", "private", current_user.id).includes(:user)
@comments = all_comments.select{ |x| x.status == "public" }
@my_comments = all_comments.select{ |x| x.status == "private" }
else
@comments = @post.comments.visible.includes(:user)
end
end
对应的查询语法:
SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? AND (status = 'private' OR (status = 'public' AND user_id = 1)) [["post_id", 1]]
4. count和size: size可以返回已经加载的collection的数量,如果没有加载则调用count的sql方法。所以在collection中都用size,
总结:sql优化,是优化已经实现的功能。不可能有先见之明。
5. pluck()选择字段,在只需要记录的部分字段的情况下使用的。可以节省内存。直接返回一个数组。
Post.all.map{|x| x.id } 是先捞出输出生成ActiveRecord,然后再生成数组
Post.pluck(:id)直接捞需要的数据生成数组,不会生成ActiveRecord。
pluck()和select()用有区别,select()有2个用法:
1。select()返回的是activerecord对象集合。
⚠️比较好的写法是: Post.select(:id, :column_name, ...)这样会带上id。否则返回的activerecord对象不带id索引.
2。select{|x| 条件 } :这是另一种用法,返回数组对象集合。
数据库的sql优化:
使用复杂的sql语法,进行如报表之类的计算。因为在数据库中的计算,比调出数据再用Ruby计算的速度快很多。
因此, 如COUNT, MAX, MIN, SUM, AVG等都直接用数据库计算比较好。
对应的Rails语法糖:count(), maximum(), minimum(), sum(), average().
例子:
SQL:
SELECT posts.*, COUNT(subscriptions.id) as subscriptions_count FROM "posts" INNER JOIN "subscriptions" ON "subscriptions"."post_id" = "posts"."id" GROUP BY posts.id
Rails语法糖:
Post.joins(:subscriptions).group("posts.id").select("posts.*, count(subscriptions.id) as subscriptions_count")
group()和select()可以换顺序,不影响SQL。
解释:group by 用于集合计算的函数并分组显示结果。
使用计数缓存counter_cache
比如以前没有使用counter_cache,现在已经有了一堆相关的记录。可以:
1. rails g migration AddXxxToXxxs xxx:type,然后在db/migrate/XXX_add_xxx中添加:
2. add_column :users, :posts_count, :integer, default: 0, null: false
User.pluck(:id).each {|id| User.reset_counters(id, :posts)}
⚠️:reset_counters(id, *counters, touch: nil)
3. 在posts.rb中加上 belongs_to :user, counter_cache: true
4. 在user的posts数量发生变化时,可以更新它使用update_counters(id, counters, touch: nil)
User.update_counters(user.id, posts_count: 1 )
⚠️:id, 想要更新一个counter的对象的id或者,一数组ids
⚠️:counters是hash, 要更新的字段的名字做key,字段的值是value.
⚠️: 如果设置选项touch:true代表更新时间戳updated_at。
Rails 内建的 Counter Cache 功能比较简单,如果你需要更多功能,请参考 https://github.com/magnusvk/counter_culture 这个 gem。
小结论:什么时候用逆规范化做优化?
如果不常显示该数据, 可以用纯 SQL 的方式来解决。但是如果需要经常显示该数据,就可以考虑用逆规范化的方式,将数据缓存下来。这样效能可以更好。但是缺点就是需要维护该数据的正确性,要写的 Ruby 代码也比较多。
考量:读取的频率 v.s. 更新缓存数据的成本。
比如:点赞数,关注数。无需时时更新。可以用逆规范化。
改进render partial的效能(比较小)
<% @posts.each do |post| %>
<%= render partial: "post", locals: {post: post}%>
<% end %>
改为用a collection of partials:
<%= render partial: "post", collection: @posts, as: :post%>
数据库索引 (加快查询速度,但会占内存)
add_index(table_name, column_name, options={}) ,各种例子的用法见API
以下需要加上index:
- 外部键(Foreign key)
- 会被排序的字段(被放在order方法中)
- 会被查询的字段(被放在where方法中)
- 会被group的字段(被放在group方法中)
- order("id desc")等同于created_at desc,效能更好。这是因为id是integer格式,而created_at是datetime格式。因此⚠️index对不同的数据格式的效率也不同。
效率上Boolean > integer > String > Date > Datetime
内存缓存
超高流量的网站会需要用到缓存来进一步提升后端效能。本教程没有提及如何做缓存,有兴趣的同学请直接看老师的Rails 实战圣经。
Rails的各类内存缓存,比如俄罗斯套娃机制,可以看guide。博客里也有相应文章。