एक डेटाबेस कई अनुप्रयोगों का दिल है, और इसके साथ समस्या होने पर गंभीर प्रदर्शन समस्याएं हो सकती हैं।
ActiveRecord और Mongoid जैसे ORM हमें अमूर्त कार्यान्वयन में मदद करते हैं और कोड को तेजी से वितरित करते हैं, लेकिन कभी-कभी, हम यह जांचना भूल जाते हैं कि हुड के नीचे कौन सी क्वेरी चल रही हैं।
बुलेट रत्न हमें कुछ प्रसिद्ध डेटाबेस-संबंधी समस्याओं की पहचान करने में मदद करता है:
- "N+1 प्रश्न":जब एप्लिकेशन सूची के प्रत्येक आइटम को लोड करने के लिए एक क्वेरी चलाता है
- "अप्रयुक्त उत्सुक लोड हो रहा है":जब एप्लिकेशन डेटा लोड करता है, आमतौर पर N+1 प्रश्नों से बचने के लिए, लेकिन इसका उपयोग नहीं करता है
- "मिसिंग काउंटर कैश":जब एप्लिकेशन को संबंधित वस्तुओं की संख्या प्राप्त करने के लिए गणना प्रश्नों को निष्पादित करने की आवश्यकता होती है
इस पोस्ट में, मैं दिखाने जा रहा हूँ:
bullet
को कैसे कॉन्फ़िगर करें? एक रूबी परियोजना में मणि,- पहले बताई गई हर समस्या के उदाहरण
- कैसे
bullet
प्रत्येक का पता लगाता है, - हर समस्या को कैसे ठीक करें, और
- कैसे एकीकृत करें
bullet
ऐपसिग्नल के साथ।
मैं इस पोस्ट के लिए बनाए गए प्रोजेक्ट के कुछ उदाहरणों का उपयोग करूंगा।
बुलेट को रूबी प्रोजेक्ट में कैसे कॉन्फ़िगर करें
सबसे पहले, रत्न को Gemfile
. में जोड़ें ।
हम इसे दिए गए सभी परिवेशों में जोड़ सकते हैं, हम इसे सक्षम या अक्षम कर सकते हैं और प्रत्येक पर एक अलग दृष्टिकोण का उपयोग कर सकते हैं:
gem 'bullet'
इसके बाद, इसे कॉन्फ़िगर करना आवश्यक है।
यदि आप रेल परियोजना में हैं, तो आप कॉन्फ़िगरेशन कोड स्वचालित रूप से उत्पन्न करने के लिए निम्न आदेश चला सकते हैं:
bundle exec rails g bullet:install
यदि आप एक गैर रेल परियोजना में हैं, तो आप इसे मैन्युअल रूप से जोड़ सकते हैं, उदाहरण के लिए, spec_helper.rb
में निम्न कोड जोड़कर एप्लिकेशन का कोड लोड करने के बाद:
Bullet.enable = true
Bullet.bullet_logger = true
Bullet.raise = true
और एप्लिकेशन के कोड को लोड करने के बाद मुख्य फाइल में निम्नलिखित कोड जोड़ना:
Bullet.enable = true
मैं इस पोस्ट में कॉन्फ़िगरेशन पर अधिक विवरण साझा करने जा रहा हूं। अगर आप उन सभी को देखना चाहते हैं, तो बुलेट के रीडमी पेज पर जाएं।
परीक्षा में बुलेट का उपयोग करना
पहले सुझाए गए कॉन्फ़िगरेशन के साथ, 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 को सभी टिप्पणियों को केवल 1 क्वेरी के साथ लोड करने का निर्देश देगा।
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 क्वेरी ट्रिगर नहीं की जाएगी।
नोट:रेल 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 का पता लगाने की अधिक संभावना के लिए एक युक्ति
मैं प्रत्येक नियंत्रक कार्रवाई के लिए कम से कम 1 अनुरोध युक्ति बनाने की अनुशंसा करता हूं, बस यह जांचने के लिए कि क्या यह सही HTTP स्थिति देता है, फिर bullet
इन विचारों को प्रस्तुत करते समय प्रश्नों को देखेगा।
अप्रयुक्त उत्सुक लोडिंग का पता लगाना
निम्नलिखित को देखते हुए 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.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
. की आवश्यकता होगी मणि आपके प्रोजेक्ट में स्थापित और कॉन्फ़िगर किया गया है। आप रूबी जेम डॉक्स में अधिक विवरण देख सकते हैं।
फिर, यदि bullet
. द्वारा किसी समस्या का पता लगाया जाता है , यह इस तरह एक त्रुटि घटना पैदा करेगा:
यह त्रुटि यूनिफ़ॉर्म_नोटिफ़ायर रत्न द्वारा उठाई गई है जिसे bullet
. से निकाला गया था ।
दुर्भाग्य से, त्रुटि संदेश पर्याप्त जानकारी नहीं दिखाता है, लेकिन मैंने इसे सुधारने के लिए एक पुल अनुरोध भेजा है!
निष्कर्ष
bullet
मणि एक बेहतरीन टूल है जो हमें उन समस्याओं का पता लगाने में मदद कर सकता है जो अनुप्रयोगों में प्रदर्शन को खराब कर देंगी।
जैसा कि पहले उल्लेख किया गया है, अच्छा परीक्षण कवरेज रखने की कोशिश करें, ताकि उत्पादन में जाने से पहले इन समस्याओं का पता लगाने की अधिक संभावना हो।
एक अतिरिक्त युक्ति के रूप में, यदि आप डेटाबेस से संबंधित प्रदर्शन समस्याओं से और भी अधिक सुरक्षित रहना चाहते हैं, तो wt-activerecord-index-spy gem पर एक नज़र डालें, जो उन प्रश्नों का पता लगाने में मदद करता है जो उचित अनुक्रमणिका का उपयोग नहीं कर रहे हैं।
पी.एस. यदि आप रूबी मैजिक की पोस्ट प्रेस से बाहर होते ही पढ़ना चाहते हैं, तो हमारे रूबी मैजिक न्यूजलेटर की सदस्यता लें और एक भी पोस्ट मिस न करें!