데이터베이스는 많은 애플리케이션의 핵심이며 데이터베이스에 문제가 있으면 심각한 성능 문제가 발생할 수 있습니다.
ActiveRecord 및 Mongoid와 같은 ORM은 구현을 추상화하고 코드를 더 빠르게 전달하는 데 도움이 되지만 때로는 내부에서 실행 중인 쿼리를 확인하는 것을 잊어버립니다.
bullet gem은 잘 알려진 데이터베이스 관련 문제를 식별하는 데 도움이 됩니다.
- "N+1 쿼리":애플리케이션이 목록의 각 항목을 로드하기 위해 쿼리를 실행할 때
- "Unused Eager Loading":일반적으로 N+1 쿼리를 피하기 위해 애플리케이션이 데이터를 로드하지만 사용하지 않을 때
- "카운터 캐시 누락":애플리케이션이 관련 항목 수를 얻기 위해 카운트 쿼리를 실행해야 하는 경우
이 게시물에서 보여줄 내용:
bullet
를 구성하는 방법 Ruby 프로젝트의 gem,- 앞서 언급한 각 문제의 예
bullet
방법 각각을 감지하고- 각 문제를 해결하는 방법 및
bullet
통합 방법 AppSignal과 함께 합니다.
이 게시물을 위해 만든 프로젝트의 몇 가지 예를 사용하겠습니다.
Ruby 프로젝트에서 Bullet을 구성하는 방법
먼저 Gemfile
에 gem을 추가합니다. .
주어진 모든 환경에 추가할 수 있고 활성화 또는 비활성화하고 각각에 대해 다른 접근 방식을 사용할 수 있습니다.
gem 'bullet'
다음으로 구성해야 합니다.
Rails 프로젝트에 있는 경우 다음 명령을 실행하여 구성 코드를 자동으로 생성할 수 있습니다.
bundle exec rails g bullet:install
Rails가 아닌 프로젝트에 있는 경우 예를 들어 spec_helper.rb
에 다음 코드를 추가하여 수동으로 추가할 수 있습니다. 애플리케이션 코드를 로드한 후:
Bullet.enable = true
Bullet.bullet_logger = true
Bullet.raise = true
그리고 응용 프로그램의 코드를 로드한 후 기본 파일에 다음 코드를 추가합니다.
Bullet.enable = true
이 게시물에서 구성에 대한 자세한 내용을 공유하겠습니다. 모두 보고 싶으시면 Bullet의 README 페이지로 가세요.
테스트에서 글머리 기호 사용
이전에 제안된 구성으로 Bullet은 테스트에서 실행된 잘못된 쿼리를 감지하고 예외를 발생시킵니다.
이제 몇 가지 예를 살펴보겠습니다.
N+1 쿼리 감지
주어진 index
다음과 같이 조치하십시오.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
다음과 같은 보기:
# app/views/posts/index.html.erb
<h1>Posts</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Comments</th>
</tr>
</thead>
<tbody>
<% @posts.each do |post| %>
<tr>
<td><%= post.name %></td>
<td><%= post.comments.map(&:name) %></td>
</tr>
<% end %>
</tbody>
</table>
bullet
예를 들어 다음과 같은 요청 사양을 사용하여 보기와 컨트롤러에서 코드를 실행하는 통합 테스트를 실행할 때 "N+1"을 감지하는 오류가 발생합니다.
# spec/requests/posts_request_spec.rb
require 'rails_helper'
RSpec.describe "Posts", type: :request do
describe "GET /index" do
it 'lists all posts' do
post1 = Post.create!
post2 = Post.create!
get '/posts'
expect(response.status).to eq(200)
end
end
end
이 경우 다음 예외가 발생합니다.
Failures:
1) Posts GET /index lists all posts
Failure/Error: get '/posts'
Bullet::Notification::UnoptimizedQueryError:
user: fabioperrella
GET /posts
USE eager loading detected
Post => [:comments]
Add to your query: .includes([:comments])
Call stack
/Users/fabioperrella/projects/bullet-test/app/views/posts/index.html.erb:17:in `map'
...
# ./spec/requests/posts_controller_spec.rb:9:in `block (3 levels) in <top (required)>'
이는 뷰가 post.comments.map(&:name)
에 있는 각 주석 이름을 로드하기 위해 하나의 쿼리를 실행하기 때문에 발생합니다. :
Processing by PostsController#index as HTML
Post Load (0.4ms) SELECT "posts".* FROM "posts"
↳ app/views/posts/index.html.erb:14
Comment Load (0.0ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]]
↳ app/views/posts/index.html.erb:17:in `map'
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 2]]
이 문제를 해결하려면 오류 메시지의 지침을 따르고 .includes([:comments])
를 추가하면 됩니다. 쿼리:
-@posts = Post.all
+@posts = Post.all.includes([:comments])
이것은 ActiveRecord가 단 하나의 쿼리로 모든 댓글을 로드하도록 지시합니다.
Processing by PostsController#index as HTML
Post Load (0.2ms) SELECT "posts".* FROM "posts"
↳ app/views/posts/index.html.erb:14
Comment Load (0.0ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (?, ?) [["post_id", 1], ["post_id", 2]]
↳ app/views/posts/index.html.erb:14
그러나 bullet
컨트롤러 테스트는 기본적으로 뷰를 렌더링하지 않으므로 다음과 같은 컨트롤러 테스트에서 예외가 발생하지 않으므로 N+1 쿼리가 트리거되지 않습니다.
참고:컨트롤러 테스트는 Rails 5부터 권장되지 않습니다.
# spec/controllers/posts_controller_spec.rb
require 'rails_helper'
RSpec.describe PostsController do
describe 'GET index' do
it 'lists all posts' do
post1 = Post.create!
post2 = Post.create!
get :index
expect(response.status).to eq(200)
end
end
end
Bullet이 "N+1"을 감지하지 못하는 테스트의 또 다른 예는 보기 테스트입니다. 이 경우 데이터베이스에서 N+1 쿼리를 실행하지 않기 때문입니다.
# spec/views/posts/index.html.erb_spec.rb
require 'rails_helper'
describe "posts/index.html.erb" do
it 'lists all posts' do
post1 = Post.create!(name: 'post1')
post2 = Post.create!(name: 'post2')
assign(:posts, [post1, post2])
render
expect(rendered).to include('post1')
expect(rendered).to include('post2')
end
end
테스트에서 N+1을 더 많이 감지할 수 있는 팁
올바른 HTTP 상태를 반환하는지 테스트한 다음 bullet
를 반환하는지 테스트하기 위해 각 컨트롤러 작업에 대해 최소 1개의 요청 사양을 만드는 것이 좋습니다. 이 보기를 렌더링할 때 쿼리를 관찰합니다.
사용하지 않는 Eager 로딩 감지
다음 basic_index
가 주어졌을 때 액션:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def basic_index
@posts = Post.all.includes(:comments)
end
end
그리고 다음 basic_index
보기:
# app/views/posts/basic_index.html.erb
<h1>Posts</h1>
<table>
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
<% @posts.each do |post| %>
<tr>
<td><%= post.name %></td>
</tr>
<% end %>
</tbody>
</table>
다음 테스트를 실행할 때:
# spec/requests/posts_request_spec.rb
require 'rails_helper'
RSpec.describe "Posts", type: :request do
describe "GET /basic_index" do
it 'lists all posts' do
post1 = Post.create!
post2 = Post.create!
get '/posts/basic_index'
expect(response.status).to eq(200)
end
end
end
글머리 기호는 다음 오류를 발생시킵니다.
1) Posts GET /basic_index lists all posts
Failure/Error: get '/posts/basic_index'
Bullet::Notification::UnoptimizedQueryError:
user: fabioperrella
GET /posts/basic_index
AVOID eager loading detected
Post => [:comments]
Remove from your query: .includes([:comments])
Call stack
/Users/fabioperrella/projects/bullet-test/spec/requests/posts_request_spec.rb:20:in `block (3 levels) in <top (required)>'
이것은 이 보기에 대한 댓글 목록을 로드할 필요가 없기 때문에 발생합니다.
문제를 해결하려면 위의 오류 지침에 따라 .includes([:comments])
쿼리를 제거하면 됩니다. :
-@posts = Post.all.includes(:comments)
+@posts = Post.all
render_views
없이 컨트롤러 테스트만 실행하면 동일한 오류가 발생하지 않는다는 것은 말할 가치가 있습니다. , 이전에 표시된 대로.
카운터 캐시 누락 감지
다음과 같은 컨트롤러가 제공됩니다.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index_with_counter
@posts = Post.all
end
end
다음과 같은 보기:
# app/views/posts/index_with_counter.html.erb
<h1>Posts</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Number of comments</th>
</tr>
</thead>
<tbody>
<% @posts.each do |post| %>
<tr>
<td><%= post.name %></td>
<td><%= post.comments.size %></td>
</tr>
<% end %>
</tbody>
</table>
다음 요청 사양을 실행하는 경우:
describe "GET /index_with_counter" do
it 'lists all posts' do
post1 = Post.create!
post2 = Post.create!
get '/posts/index_with_counter'
expect(response.status).to eq(200)
end
end
bullet
다음 오류가 발생합니다:
1) Posts GET /index_with_counter lists all posts
Failure/Error: get '/posts/index_with_counter'
Bullet::Notification::UnoptimizedQueryError:
user: fabioperrella
GET /posts/index_with_counter
Need Counter Cache
Post => [:comments]
# ./spec/requests/posts_request_spec.rb:31:in `block (3 levels) in <top (required)>'
이것은 이 보기가 post.comments.size
의 댓글 수를 계산하기 위해 1개의 쿼리를 실행하기 때문에 발생합니다. 각 게시물에 대해.
Processing by PostsController#index_with_counter as HTML
↳ app/views/posts/index_with_counter.html.erb:14
Post Load (0.4ms) SELECT "posts".* FROM "posts"
↳ app/views/posts/index_with_counter.html.erb:14
(0.4ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]]
↳ app/views/posts/index_with_counter.html.erb:17
(0.1ms) SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 2]]
이 문제를 해결하기 위해 특히 프로덕션 데이터베이스에 데이터가 있는 경우 약간 복잡할 수 있는 카운터 캐시를 만들 수 있습니다.
카운터 캐시는 테이블에 추가할 수 있는 열로, 연결된 모델을 삽입 및 삭제할 때 ActiveRecord가 자동으로 업데이트합니다. 이 게시물에 자세한 내용이 있습니다. 카운터 캐시를 만들고 동기화하는 방법을 알아보려면 이 문서를 읽는 것이 좋습니다.
개발 시 Bullet 사용
예를 들어 테스트 범위가 낮은 경우 테스트에서 이전에 언급한 문제를 감지하지 못할 수 있으므로 bullet
를 활성화할 수 있습니다. 다른 접근 방식을 사용하는 다른 환경에서.
개발 환경에서 다음 구성을 활성화할 수 있습니다.
Bullet.alert = true
그러면 브라우저에 다음과 같은 경고가 표시됩니다.
Bullet.add_footer = true
오류가 있는 페이지에 바닥글을 추가합니다.
브라우저 콘솔에 오류가 기록되도록 설정할 수도 있습니다.
Bullet.console = true
다음과 같은 오류가 추가됩니다.
Appsignal과 함께 스테이징에서 Bullet 사용
스테이징에서 환경에서는 이러한 오류 메시지가 최종 사용자에게 표시되는 것을 원하지 않지만 응용 프로그램에 앞에서 언급한 문제 중 하나가 발생하기 시작하는지 아는 것이 좋습니다.
동시에 bullet
애플리케이션의 성능을 저하시키고 메모리 소비를 증가시킬 수 있으므로 스테이징에서 일시적으로만 활성화하는 것이 좋습니다. 하지만 프로덕션에서는 활성화하지 마세요. .
스테이징 가정 환경이 프로덕션과 동일한 구성 파일을 사용하고 있습니다. 둘 사이의 차이를 줄이는 좋은 방법인 환경에서 환경 변수를 사용하여 bullet
를 활성화하거나 비활성화할 수 있습니다. 다음과 같이:
# config/environments/production.rb
config.after_initialize do
Bullet.enabled = ENV.fetch('BULLET_ENABLED', false)
Bullet.appsignal = true
end
Bullet이 준비 환경에서 발견한 문제에 대한 알림을 받으려면 AppSignal을 사용하여 해당 알림을 오류로 보고할 수 있습니다. appsignal
이 있어야 합니다. 프로젝트에 설치 및 구성한 gem. Ruby gem 문서에서 자세한 내용을 볼 수 있습니다.
그런 다음 bullet
에 의해 문제가 감지되면 , 다음과 같은 오류 인시던트를 생성합니다.
이 오류는 bullet
에서 추출된 uniform_notifier gem에 의해 발생합니다. .
유감스럽게도 오류 메시지에는 충분한 정보가 표시되지 않지만 이를 개선하기 위해 Pull Request를 보냈습니다!
결론
bullet
gem은 애플리케이션의 성능을 저하시키는 문제를 감지하는 데 도움이 되는 훌륭한 도구입니다.
앞서 언급한 대로 테스트 범위를 잘 유지하여 프로덕션으로 이동하기 전에 이러한 문제를 감지할 가능성을 높이십시오.
추가 팁으로 데이터베이스와 관련된 성능 문제로부터 더욱 보호받고 싶다면 적절한 인덱스를 사용하지 않는 쿼리를 감지하는 데 도움이 되는 wt-activerecord-index-spy gem을 살펴보십시오.
추신 Ruby Magic 게시물이 언론에 공개되는 즉시 읽고 싶다면 Ruby Magic 뉴스레터를 구독하고 게시물을 놓치지 마세요!