एक डेटाबेस कई एप्लिकेशन का दिल होता है, और इसके साथ समस्याएं गंभीर प्रदर्शन समस्याओं का कारण बन सकती हैं।
ActiveRecord और Mongoid जैसे ORMs हमें इम्प्लीमेंटेशन को अमूर्त करने और तेजी से कोड डिलीवर करने में मदद करते हैं, लेकिन कभी-कभी हम यह जांचना भूल जाते हैं कि हुड के नीचे कौन से क्वेरी चल रहे हैं।
bullet जेम हमें कुछ प्रसिद्ध डेटाबेस-संबंधी समस्याओं की पहचान करने में मदद करता है:
- "N+1 Queries": जब एप्लिकेशन एक सूची के प्रत्येक आइटम को लोड करने के लिए एक क्वेरी चलाता है
- "Unused Eager Loading": जब एप्लिकेशन डेटा लोड करता है, आमतौर पर N+1 क्वेरी से बचने के लिए, लेकिन इसका उपयोग नहीं करता
- "Missing Counter Cache": जब एप्लिकेशन को संबंधित आइटम्स की संख्या प्राप्त करने के लिए काउंट क्वेरी निष्पादित करने की आवश्यकता होती है
इस पोस्ट में, मैं दिखाऊंगा:
- रूबी प्रोजेक्ट में
bulletजेम को कैसे कॉन्फ़िगर करें - ऊपर उल्लिखित प्रत्येक समस्या के उदाहरण
bulletप्रत्येक का पता कैसे लगाता है- प्रत्येक समस्या को कैसे ठीक करें
bulletको AppSignal के साथ कैसे इंटीग्रेट करें
मैं इस पोस्ट के लिए बनाए गए एक प्रोजेक्ट के कुछ उदाहरणों का उपयोग करूंगा।
रूबी प्रोजेक्ट में 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 के README पेज पर जाएं।
टेस्ट्स में Bullet का उपयोग करना
पहले सुझाए गए कॉन्फ़िगरेशन के साथ, 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 Posts
Name Comments <% @posts.each do |post| %> <%= post.name %> <%= post.comments.map(&:name) %> <% end %>
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 '
ऐसा इसलिए होता है क्योंकि व्यू 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 Posts
Name <% @posts.each do |post| %> <%= post.name %> <% end %>
जब हम निम्न टेस्ट चलाते हैं:
# 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
Bullet निम्न त्रुटि उठाएगा:
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 '
ऐसा इसलिए होता है क्योंकि इस व्यू के लिए कमेंट्स की सूची लोड करना आवश्यक नहीं है।
समस्या को ठीक करने के लिए, हम बस ऊपर की त्रुटि में दिए निर्देश का पालन कर सकते हैं और क्वेरी .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 Posts
Name Number of comments <% @posts.each do |post| %> <%= post.name %> <%= post.comments.size %> <% end %>
यदि हम निम्न रिक्वेस्ट स्पेक चलाते हैं:
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 '
ऐसा इसलिए होता है क्योंकि यह व्यू 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 जेम इंस्टॉल और कॉन्फ़िगर होना चाहिए। आप रूबी जेम डॉक्स में अधिक विवरण देख सकते हैं।
फिर, यदि bullet द्वारा कोई समस्या पता लगाई जाती है, तो यह इस तरह की एक त्रुटि घटना बनाएगा:
यह त्रुटि uniform_notifier जेम द्वारा उठाई गई है जिसे bullet से निकाला गया था।
दुर्भाग्य से, त्रुटि संदेश पर्याप्त जानकारी नहीं दिखाता, लेकिन मैंने इसमें सुधार के लिए एक पुल रिक्वेस्ट भेजी है!
निष्कर्ष
bullet जेम एक बेहतरीन टूल है जो हमें उन समस्याओं का पता लगाने में मदद कर सकता है जो एप्लिकेशन में प्रदर्शन को कम करेंगी।
पहले उल्लिखित अनुसार अच्छे टेस्ट कवरेज को बनाए रखने का प्रयास करें, ताकि प्रोडक्शन में जाने से पहले इन समस्याओं का पता लगाने की अधिक संभावना हो।
एक अतिरिक्त टिप के रूप में, यदि आप डेटाबेस से संबंधित प्रदर्शन समस्याओं के खिलाफ और भी अधिक सुरक्षित होना चाहते हैं, तो wt-activerecord-index-spy जेम पर एक नज़र डालें, जो उन क्वेरीज़ का पता लगाने में मदद करता है जो उचित इंडेक्स का उपयोग नहीं कर रहे हैं।
P.S. यदि आप रूबी मैजिक पोस्ट को प्रेस से बाहर आते ही पढ़ना चाहते हैं, तो हमारे रूबी मैजिक न्यूज़लेटर की सदस्यता लें और कभी भी एक भी पोस्ट मिस न करें!