Giảm số lượng các câu lệnh queries bằng gem Bullet

I. Bullet

1. Giới thiệu

  • Gem Bullet được thiết kể để giúp các application tăng hiệu năng bằng cách giảm các câu lệnh queries của app đó. Bullet sẽ xem xét các câu lệnh queries từ lúc bạn develop sản phẩm và thông báo cho bạn khi bạn sử đã bị N+1 queries, khi bạn sử dụng bộ nhớ cache.
  • Bạn nên sử dụng Bullet trong môi trường development hoặc các custom mode khác như staging, profile, etc.
  • Bullet gem hiện tại đang support cho activerecord >= 3.0 và mongoid >= 2.4.1.
  • Nếu bạn sử dụng activerecord 2.x thì hãy sử dụng bullet <= 4.5.0.

2. Install

  • Thêm vào Gemfile: gem 'bullet', group: :development
  • Hoặc install bằng terminal: gem install bullet

3. Configuration

  • Bullet sẽ không làm gì cả nếu bạn ko khai báo cho nó 1 cách rõ ràng trong config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.growl = true
  Bullet.xmpp = { :account  => '[email protected]',
                  :password => 'bullets_password_for_jabber',
                  :receiver => '[email protected]',
                  :show_online_status => true }
  Bullet.rails_logger = true
  Bullet.honeybadger = true
  Bullet.bugsnag = true
  Bullet.airbrake = true
  Bullet.rollbar = true
  Bullet.add_footer = true
  Bullet.stacktrace_includes = [ 'your_gem', 'your_middleware' ]
  Bullet.stacktrace_excludes = [ 'their_gem', 'their_middleware' ]
  Bullet.slack = { webhook_url: 'http://some.slack.url', foo: 'bar' }
end

Những dòng code dưới đây sẽ enable tất cả 7 các thông báo của Bullet

  • Bullet.enable: enable Bullet gem, nếu không thì không làm gì cả
  • Bullet.alert: popup ra JavaScript alert trên trình duyệt
  • Bullet.bullet_logger: log Bullet's log ra file (Rails.root/log/bullet.log)
  • Bullet.rails_logger: thêm các warnings trực tiếp vào rails console log
  • Bullet.honybadger: thêm notifications vào Honeybatger
  • Bullet.bugsnag: thêm notifications vào bugsnag
  • Bullet.airbrake: thêm notifications vào airbrake
  • Bullet.rollbar: thêm notifications vào rollbar
  • Bullet.console: log warnings vào console của trình duyệt
  • Bullet.growl: popup Growl warnings nếu hệ thống có cài đặt Growl
  • Bullet.xmpp: gửi XMPP/Jabber notifications tới người nhận. Chú ý rằng code mặc định sẽ không handle các contacts đang được add, bởi vậy sẽ cần phải làm cả 2 accounts được xác nhận trước khi bạn nhận bất kì thông báo nào. Nếu bạn set cho show_online_status: false thì bạn vẫn nhận được thông báo nhưng Bullet account sẽ không hiện online status nữa
  • Bulelt.raise raise error, nếu bạn không tối ưu hóa thì sẽ dễ làm cho hệ thống sai về specs
  • Bullet.add_footer: thêm thông tim chi tiết vào phía trái bên dưới góc của page
  • Bullet.stacktrace_includes: thêm đường dẫn với substrings vào stack trace, thậm chí nếu chúng không ở main app
  • Bullet.stacktrace_excludes: bỏ qua đường dẫn với substrings vào stack trace, thậm chí nếu chúng không ở main app
  • Bullet.slack: thêm notifications vào slack

Nhưng Bullet cũng cho phép bạn disable các chức năng detectors

# Each of these settings defaults to true

# Detect N+1 queries
Bullet.n_plus_one_query_enable     = false

# Detect eager-loaded associations which are not used
Bullet.unused_eager_loading_enable = false

# Detect unnecessary COUNT queries which could be avoided
# with a counter_cache
Bullet.counter_cache_enable        = false

4. Whitelist

  • Nhưng thỉnh thoảng Bullet có thể thông báo cho bạn về vấn đề queries cái mà bạn không quan tâm fix, hoặc những cái đến từ ngoài code của bạn. Bạn có thể thêm vào whitelist để bỏ qua nó:
  • Ví dụ:
Bullet.add_whitelist :type => :n_plus_one_query, :class_name => "Post", :association => :comments
Bullet.add_whitelist :type => :unused_eager_loading, :class_name => "Post", :association => :comments
Bullet.add_whitelist :type => :counter_cache, :class_name => "Country", :association => :cities
  • Nếu bạn muốn bỏ qua bullet trong các controllers thì bạn có thể làm như thế này:
class ApplicationController < ActionController::Base
  around_action :skip_bullet

  def skip_bullet
    Bullet.enable = false
    yield
  ensure
    Bullet.enable = true
  end
end

5. Log

Bullet's log sẽ trông như thế này

  • N+1 Query
# log/bullet.log
2009-08-25 20:40:17[INFO] N+1 Query: PATH_INFO: /posts;    model: Post => associations: [comments]·
Add to your finder: :include => [:comments]
2009-08-25 20:40:17[INFO] N+1 Query: method call stack:·
/Users/richard/Downloads/test/app/views/posts/index.html.erb:11:in `_run_erb_app47views47posts47index46html46erb'
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `each'
/Users/richard/Downloads/test/app/views/posts/index.html.erb:8:in `_run_erb_app47views47posts47index46html46erb'
/Users/richard/Downloads/test/app/controllers/posts_controller.rb:7:in `index'

Hai dòng đầu tiên là những thông báo về N+1 queries. Những dòng tiếp theo là nói tới chỗ nào trong code của bạn của lỗi đó, từ đó bạn có thể tìm nó và sửa

  • Unused eager loading:
2009-08-25 20:53:56[INFO] Unused eager loadings: PATH_INFO: /posts;    model: Post => associations: [comments]·
Remove from your finder: :include => [:comments]

Important

Nếu bạn không thấy Bullet hoạt động thì hãy tắt cache trình duyệt mà các bạn đang sử dụng.

II. Demo

1. Tạo 1 application test

$ rails new test_bullet
$ cd test_bullet
$ rails g scaffold post name:string
$ rails g scaffold comment name:string post_id:integer
$ bundle exec rake db:migrate

2. Change app/model/post.rb and app/model/comment.rb

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

3. Bật rails c để thêm dữ liệu

post1 = Post.create(:name => 'first')
post2 = Post.create(:name => 'second')
post1.comments.create(:name => 'first')
post1.comments.create(:name => 'second')
post2.comments.create(:name => 'third')
post2.comments.create(:name => 'fourth')

4. Thay đổi app/views/posts/index.html.erb để làm xuất hiện N+1 query

<% @posts.each do |post| %>
  <tr>
    <td><%= post.name %></td>
    <td><%= post.comments.map(&:name) %></td> # Ta thêm dòng này
    <td><%= link_to 'Show', post %></td>
    <td><%= link_to 'Edit', edit_post_path(post) %></td>
    <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>

5. Ta add gem bullet vào Gemfile

gem 'bullet' sau đó chạy bundle install

6. Ta config Bullet trong config/environments/development.rb

config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  # Bullet.growl = true
  Bullet.rails_logger = true
  Bullet.add_footer = true
end

7. Chạy server

rails s

8. Vào trình duyệt rồi đi tới http://localhost:3000/posts, ta sẽ thấy được 1 popup

  • JavaScript alert

test_bullet_posts.png

  • Log server:

log_server.png

9. Sửa N+1 query

Ta sửa file app/controllers/posts_controller.rb:

def index
  @posts = Post.includes(:comments)

  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @posts }
  end
end

10. Tải lại trang http://localhost:3000/posts. Ta không thấy bất kỳ 1 popup nào được hiện lên

  • Log server:

log_server_2.png

=> Vậy là N+1 query đã được sửa.

11. Mô phỏng unused eager loading

  • Thay đổi các file
# app/controllers/posts_controller.rb
def index
  @posts = Post.includes(:comments)

  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @posts }
  end
end

# app/views/posts/index.html.erb
<% @posts.each do |post| %>
  <tr>
    <td><%= post.name %></td>
    <td><%= link_to 'Show', post %></td>
    <td><%= link_to 'Edit', edit_post_path(post) %></td>
    <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>

12. Tải lại trang http://localhost:3000/posts

  • Ta thấy xuất hiện 1 popup

pop_up_bullet_2.png

  • Sửa:
# app/controllers/posts_controller.rb
def index
  @posts = Post.all

  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @posts }
  end
end

III. Tài liệu tham khảo

https://github.com/flyerhzm/bullet