ActiveRecord เป็นคุณสมบัติมหัศจรรย์ที่สุดของ Ruby on Rails ปกติแล้วเราไม่จำเป็นต้องกังวลเกี่ยวกับการทำงานภายใน แต่เมื่อดำเนินการ ต่อไปนี้คือวิธีที่ AppSignal จะช่วยให้เราทราบว่าเกิดอะไรขึ้นภายใต้ประทุน
ActiveRecord คืออะไร
ในการพูดคุยเกี่ยวกับ ActiveRecord เราต้องคิดถึงเฟรมเวิร์กก่อน โดยเฉพาะเกี่ยวกับเฟรมเวิร์ก MVC MVC ย่อมาจาก Model-View-Controller และเป็นรูปแบบการออกแบบซอฟต์แวร์ยอดนิยมสำหรับแอปพลิเคชันกราฟิกและเว็บ
เฟรมเวิร์ก MVC ประกอบด้วย:
- รุ่น :จัดการตรรกะทางธุรกิจและความคงอยู่ของข้อมูล
- ดู :ขับเคลื่อนเลเยอร์การนำเสนอและวาดอินเทอร์เฟซผู้ใช้
- ตัวควบคุม :ผูกทุกอย่างเข้าด้วยกัน
ActiveRecord คือ รุ่น คอมโพเนนต์ในเฟรมเวิร์ก Ruby in Rails มันแนะนำชั้นนามธรรมระหว่างโค้ดและข้อมูล ดังนั้นเราจึงไม่ต้องเขียนโค้ด SQL ด้วยตัวเอง แต่ละรุ่นถูกแมปกับตารางเดียวและมีวิธีการต่างๆ ในการดำเนินการ CRUD (สร้าง อ่าน อัปเดต และลบ)
การตรวจสอบ ActiveRecord ด้วย AppSignal
นามธรรมรู้สึกมหัศจรรย์ — ช่วยให้เราเพิกเฉยต่อรายละเอียดที่เราไม่จำเป็นต้องรู้และมุ่งความสนใจไปที่งานที่ทำอยู่ แต่เมื่อสิ่งต่าง ๆ ไม่ทำงานตามที่คาดไว้ ความซับซ้อนที่พวกเขาเพิ่มเข้าไปอาจทำให้ระบุสาเหตุที่แท้จริงได้ยากขึ้น AppSignal สามารถให้รายละเอียดเกี่ยวกับสิ่งที่เกิดขึ้นจริงใน Rails ได้
กราฟเวลาตอบสนอง
มาดูปัญหากันก่อน การแก้ไขปัญหาประสิทธิภาพการทำงานเป็นกระบวนการที่ทำซ้ำได้ เมื่อคุณมีแอปพลิเคชัน Rails รายงานไปยัง AppSignal แล้ว ให้ไปที่ เหตุการณ์> ประสิทธิภาพ .
กราฟเวลาตอบสนองจะแสดงเปอร์เซ็นต์ไทล์เวลาตอบสนองสำหรับแต่ละเนมสเปซ เหตุการณ์ ActiveRecord ถูกกำหนดโดยอัตโนมัติให้กับเนมสเปซที่คำขอหรืองานพื้นหลังที่ดำเนินการค้นหา
ถัดไป ดูกราฟกลุ่มเหตุการณ์ มันแสดงให้เห็นว่าใช้เวลาเท่าใดตามหมวดหมู่ ตรวจสอบระยะเวลาที่เกี่ยวข้องโดย active_record
. ตรวจสอบการใช้งานในเนมสเปซทั้งหมดของคุณ
กราฟจะบอกเราทันทีว่าเราควรมุ่งเน้นที่ใดในการเพิ่มประสิทธิภาพโค้ด
ขณะที่คุณอยู่ในแดชบอร์ดกราฟประสิทธิภาพ ให้ตรวจสอบเวลาตอบสนองและปริมาณงานเพื่อให้แน่ใจว่าไม่มีกิจกรรมที่สูงกว่าปกติในแอปพลิเคชันของคุณ
แดชบอร์ดการสืบค้นข้อมูลช้า
ตอนนี้เราได้ระบุแล้วว่าปัญหาเกิดจากข้อมูลผูกมัด เรามาดูกันว่าเราจะขยายภาพเพื่อหาสาเหตุที่แท้จริงได้หรือไม่
เปิด ปรับปรุง> แบบสอบถามช้า แผงควบคุม. หน้านี้แสดงรายการแบบสอบถาม SQL ที่จัดอันดับตามผลกระทบต่อเวลาโดยรวม ข้อความค้นหาที่สร้างจาก ActiveRecord ทั้งหมดจะแสดงเป็น sql.active_record
เหตุการณ์
ลองคลิกที่ข้อความค้นหาระดับบนสุดเพื่อดูรายละเอียด แดชบอร์ดแสดงระยะเวลาเฉลี่ยและข้อความค้นหา
การเลื่อนด้านล่างจะแสดงเวลาตอบกลับของคำค้นหาในไม่กี่ชั่วโมงที่ผ่านมาและการดำเนินการเริ่มต้น
คุณอาจพบว่าการดำเนินการบางอย่างมีเหตุการณ์ที่เกี่ยวข้อง ซึ่งหมายความว่า AppSignal ได้สร้างเหตุการณ์ด้านประสิทธิภาพในขณะที่การสืบค้นกำลังทำงานอยู่ แต่ก็ไม่ได้หมายความว่า ActiveRecord เป็นสาเหตุของปัญหาเสมอไป
แดชบอร์ดการวัดประสิทธิภาพ
เหตุการณ์การวัดประสิทธิภาพจะเปิดขึ้นเมื่อ AppSignal บันทึกปลายทางใหม่หรืองานพื้นหลัง
เหตุการณ์อยู่ที่ประสิทธิภาพ> รายการปัญหา แดชบอร์ด
หน้าเหตุการณ์แสดงเวลาที่ผ่านไปและจำนวนการจัดสรรสำหรับส่วนประกอบ MVC แต่ละรายการ ปัญหา ActiveRecord จะแสดงระยะเวลานานใน active_record
หมวดหมู่.
ไทม์ไลน์ของกิจกรรมแสดงให้เห็นว่ากิจกรรมดำเนินไปอย่างไรเมื่อเวลาผ่านไป
การค้นหาปัญหา ActiveRecord
ในส่วนนี้ เราจะมาดูกันว่า AppSignal สามารถช่วยเราระบุปัญหา ActiveError ทั่วไปได้อย่างไร
การเลือกคอลัมน์ที่เกี่ยวข้อง
ภูมิปัญญาฐานข้อมูลบอกว่าเราควรดึงคอลัมน์ที่จำเป็นสำหรับงานเสมอ ตัวอย่างเช่น แทนที่จะ SELECT * FROM people
เราควร SELECT first_name, surname, birthdate FROM people
. นั่นเป็นทั้งหมดที่ดีและดี แต่เราจะทำอย่างไรกับ Rails?
โดยค่าเริ่มต้น ActiveRecord จะดึงข้อมูลคอลัมน์ทั้งหมด
Person.all.each {
# process data
}
โชคดีที่เรามี select
วิธีการเลือกและเลือกคอลัมน์ที่ต้องการ:
Person.select(:name, :address, :birthdate).each {
# process data
}
อาจดูเหมือนฉันกำลังหมกมุ่นอยู่กับรายละเอียดเล็กๆ น้อยๆ แต่สำหรับตารางกว้าง การเลือกคอลัมน์ทั้งหมดนั้นเป็นการสิ้นเปลือง คุณจะสังเกตเห็นว่าเมื่อสิ่งนี้เกิดขึ้น ActiveRecord จะจัดสรรหน่วยความจำขนาดใหญ่:
ปัญหา N+1
ปัญหา N+1 เกิดขึ้นเมื่อแอปพลิเคชันได้รับชุดระเบียนจากฐานข้อมูลและวนซ้ำ ซึ่งทำให้แอปพลิเคชันดำเนินการค้นหา N+1 โดยที่ N คือจำนวนแถวที่ได้รับในตอนแรก อย่างที่คุณอาจจินตนาการ รูปแบบนี้จะขยายได้ไม่ดีเมื่อโต๊ะโตขึ้น เป็นปัญหาที่สร้างความเสียหายอย่างมากที่ AppSignal เตือนคุณโดยเฉพาะ:
ปัญหา N+1 มักปรากฏขึ้นพร้อมกับโมเดลที่เกี่ยวข้อง ลองนึกภาพว่าเรามีโมเดลบุคคล:
class Person < ApplicationRecord
has_many :addresses
end
แต่ละคนสามารถมีที่อยู่ได้หลายที่อยู่:
class Address < ApplicationRecord
belongs_to :person
end
วิธีที่ตรงไปตรงมาที่สุดในการดึงข้อมูลนำไปสู่ปัญหา N+1:
class RelatedTablesController < ApplicationController
def index
Person.all.each do |person|
person.addresses.each do |address|
address.address
end
end
end
end
คุณสามารถเห็นใน AppSignal ว่าแอปพลิเคชันกำลังเรียกใช้ SELECT
ต่อคน:
การแก้ไขสำหรับกรณีนี้โดยเฉพาะนั้นง่ายมาก:ใช้รวมถึง ซึ่งบอกให้ ActiveRecord เพิ่มประสิทธิภาพการสืบค้นสำหรับคอลัมน์ที่จำเป็น:
class RelatedTablesController < ApplicationController
def index
Person.all.includes(:addresses).each do |person|
person.addresses.each do |address|
address.address
end
end
end
end
ตอนนี้เรามีข้อความค้นหาสองรายการแทนที่จะเป็น N+1:
Processing by RelatedTablesController#index as HTML
(0.2ms) SELECT sqlite_version(*)
↳ app/controllers/related_tables_controller.rb:12:in `index'
Person Load (334.6ms) SELECT "people".* FROM "people"
↳ app/controllers/related_tables_controller.rb:12:in `index'
Address Load (144.4ms) SELECT "addresses".* FROM "addresses" WHERE "addresses"."person_id" IN (1, 2, 3, . . .)
ดำเนินการแบบสอบถามที่จำเป็นน้อยที่สุดต่อตาราง
บางครั้งอาจสับสนกับปัญหา N+1 แต่ก็แตกต่างกันเล็กน้อย เมื่อเราสืบค้นตาราง เราควรดึงข้อมูลทั้งหมดที่เราคิดว่าเราจะต้องใช้ในการย่อขนาดการดำเนินการอ่าน อย่างไรก็ตาม มีโค้ดที่ดูไร้เดียงสาจำนวนมากที่ทำให้เกิดการสืบค้นซ้ำซ้อน ตัวอย่างเช่น ดูว่า count
. เป็นอย่างไร ให้ผลลัพธ์เป็น SELECT COUNT(*)
. เสมอ แบบสอบถามในมุมมองต่อไปนี้:
<ul>
<% @people.each do |person| %>
<li><%= person.name %></li>
<% end %>
</ul>
<h2>Number of Persons: <%= @people.count %></h2>
ตอนนี้ ActiveRecord ทำการสืบค้นสองรายการ:
Rendering duplicated_table_query/index.html.erb within layouts/application
(69.1ms) SELECT COUNT(*) FROM "people" WHERE "people"."name" = ? [["name", "John Waters"]]
↳ app/views/duplicated_table_query/index.html.erb:3
Person Load (14.6ms) SELECT "people".* FROM "people" WHERE "people"."name" = ? [["name", "John Waters"]]
↳ app/views/duplicated_table_query/index.html.erb:6
ใน AppSignal อาการที่คุณจะสังเกตเห็นคือมี active_record
. สองตัว เหตุการณ์บนโต๊ะเดียวกัน:
ความจริงก็คือเราไม่ต้องการคำถามสองข้อ เรามีข้อมูลทั้งหมดที่เราต้องการในหน่วยความจำอยู่แล้ว ในกรณีนี้ วิธีแก้ไขคือสลับ count
ด้วย size
:
<ul>
<% @people.each do |person| %>
<li><%= person.name %></li>
<% end %>
</ul>
<h2>Number of Persons: <%= @people.size %></h2>
ตอนนี้เรามี SELECT
. ตัวเดียว อย่างที่ควรจะเป็น:
Rendering duplicated_table_query/index.html.erb within layouts/application
Person Load (63.2ms) SELECT "people".* FROM "people" WHERE "people"."name" = ? [["name", "Abdul Strosin"]]
↳ app/views/duplicated_table_query/index.html.erb:5
อีกวิธีหนึ่งคือใช้พรีโหลดเพื่อแคชข้อมูลในหน่วยความจำ
การคำนวณข้อมูลรวมใน Rails
การรวมใช้เพื่อคำนวณค่าตามชุดข้อมูล ฐานข้อมูลทำงานได้ดีกับชุดข้อมูลขนาดใหญ่ นี่คือสิ่งที่พวกเขาทำและสิ่งที่เราใช้สำหรับ ในทางกลับกัน การใช้ Rails ในการรวบรวมนั้นไม่ได้ปรับขนาดเนื่องจากต้องรับบันทึกทั้งหมดจากฐานข้อมูล เก็บไว้ในหน่วยความจำ แล้วคำนวณโดยใช้โค้ดระดับสูง
เรากำลังทำการรวมใน Rails ทุกครั้งที่เราใช้ฟังก์ชัน Ruby เช่น max
, min
, หรือ sum
เหนือองค์ประกอบ ActiveRecord หรือรายการอื่นๆ
class AggregatedColumnsController < ApplicationController
def index
@mean = Number.pluck(:number).sum()
end
end
โชคดีที่โมเดล ActiveRecord มีเมธอดเฉพาะที่แมปกับฟังก์ชันการรวมในฐานข้อมูล ตัวอย่างเช่น เคียวรีต่อไปนี้จะจับคู่กับ SELECT SUM(number) FROM ...
ซึ่งเร็วกว่าและถูกกว่ามากในการรันกว่าตัวอย่างก่อนหน้า:
# controller
class AggregatedColumnsController < ApplicationController
def index
@mean = Number.sum(:number)
end
end
Processing by AggregatedColumnsController#index as */*
(2.4ms) SELECT SUM("numbers"."number") FROM "numbers"
หากคุณต้องการฟังก์ชันการรวมที่ซับซ้อนหรือรวมกันมากขึ้น คุณอาจต้องใส่โค้ด SQL ดิบบางส่วน:
sql = "SELECT AVG(number), STDDEV(number), VAR(number) FROM ..."
@results = ActiveRecord::Base.connection.execute(sql)
การจัดการธุรกรรมขนาดใหญ่
ธุรกรรม SQL ช่วยให้มั่นใจถึงการปรับปรุงที่สม่ำเสมอและเป็นอะตอม เมื่อเราใช้ธุรกรรมเพื่อทำการเปลี่ยนแปลง ทุกแถวจะได้รับการอัปเดตสำเร็จหรือย้อนกลับทั้งหมด ไม่ว่าในกรณีใด ฐานข้อมูลจะยังคงอยู่ในสถานะที่สอดคล้องกันเสมอ
เราสามารถรวมกลุ่มการเปลี่ยนแปลงไว้ในธุรกรรมเดียวด้วย ActiveRecord::Base.transaction
.
class BigTransactionController < ApplicationController
def index
ActiveRecord::Base.transaction do
(1..1000).each do
Person.create(name: 'Les Claypool')
end
end
end
end
มีหลายกรณีที่ถูกต้องตามกฎหมายสำหรับการใช้ธุรกรรมขนาดใหญ่ อย่างไรก็ตาม สิ่งเหล่านี้มีความเสี่ยงที่จะทำให้ฐานข้อมูลช้าลง นอกจากนี้ ธุรกรรมที่เกินขีดจำกัดบางอย่างจะส่งผลให้ฐานข้อมูลปฏิเสธธุรกรรมเหล่านั้น
สัญญาณแรกของการทำธุรกรรมที่ใหญ่เกินไปคือการใช้เวลามากกับ commit transaction
เหตุการณ์:
ยกเว้นปัญหาการกำหนดค่าในฐานข้อมูลเอง วิธีแก้ไขคือแบ่งธุรกรรมออกเป็นชิ้นเล็กๆ
การตรวจสอบฐานข้อมูล
บางครั้งแม้ว่าเราจะค้นหาโค้ด แต่เราไม่พบสิ่งผิดปกติในโค้ดดังกล่าว จากนั้นอาจมีปัญหาในฐานข้อมูลเอง กลไกฐานข้อมูลมีความซับซ้อน และมีหลายสิ่งหลายอย่างที่อาจผิดพลาดได้ เช่น หน่วยความจำเหลือน้อย การตั้งค่าเริ่มต้น ดัชนีที่ขาดหายไป งานสำรองข้อมูลตามกำหนดเวลาที่ไม่สะดวก รูปภาพจะไม่สมบูรณ์เว้นแต่เราจะได้รับข้อมูลเกี่ยวกับเครื่องที่รันฐานข้อมูล
หากเราใช้ฐานข้อมูลของเราเอง เราสามารถติดตั้งเอเจนต์แบบสแตนด์อโลนเพื่อบันทึกเมตริกของโฮสต์ได้ หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีใช้เอเจนต์แบบสแตนด์อโลน โปรดอ่าน:การมอนิเตอร์ทุกระบบด้วย StatsD และเอเจนต์แบบสแตนด์อโลนของ AppSignal
สัญญาณต่อไปนี้อาจแสดงว่ามีบางอย่างเกิดขึ้นกับฐานข้อมูล ไปที่ ตรวจสอบ> ตัวชี้วัดโฮสต์ แดชบอร์ดเพื่อดูการใช้ทรัพยากรในเซิร์ฟเวอร์ของคุณ:
- การใช้หน่วยความจำสูง :ฐานข้อมูลต้องการหน่วยความจำจำนวนมาก — มากกว่าระบบอื่นๆ ส่วนใหญ่ — เพื่อให้ทำงานได้อย่างถูกต้อง เมื่อชุดข้อมูลเติบโตขึ้น ความต้องการหน่วยความจำก็มักจะขยายตามไปด้วย เราจำเป็นต้องเพิ่มหน่วยความจำหรือแบ่งชุดข้อมูลระหว่างเครื่องต่างๆ เป็นครั้งคราว
- สลับการใช้งาน :ตามหลักการแล้วเครื่องฐานข้อมูลไม่ควรต้องการหน่วยความจำแบบสลับเลย การสลับฆ่าประสิทธิภาพของฐานข้อมูล การปรากฏตัวของมันหมายความว่ามีการกำหนดค่าเชิงลึกหรือปัญหาหน่วยความจำมากขึ้น
- การใช้งาน I/O สูง :จุดสูงสุดในกิจกรรมดิสก์อาจเกิดจากงานบำรุงรักษา เช่น การทำดัชนีใหม่หรือการสำรองข้อมูลฐานข้อมูล การทำงานเหล่านี้ในช่วงชั่วโมงเร่งด่วนจะส่งผลต่อประสิทธิภาพอย่างแน่นอน
👋 ถ้าคุณชอบบทความนี้ ยังมีอีกมากมายที่เราเขียนเกี่ยวกับประสิทธิภาพของ Ruby (on Rails) ให้ดูที่รายการตรวจสอบการตรวจสอบประสิทธิภาพของ Ruby
บทสรุป
การวินิจฉัยปัญหาด้านประสิทธิภาพไม่ใช่เรื่องง่าย วันนี้ เราได้เรียนรู้วิธีใช้ Appsignal เพื่อระบุแหล่งที่มาของปัญหาอย่างรวดเร็ว
มาเรียนรู้เกี่ยวกับ AppSignal และ Ruby on Rails กัน:
- ประสิทธิภาพ ActiveRecord:แอนตี้แพทเทิร์นการสืบค้น N+1
- Rails นั้นเร็ว:เพิ่มประสิทธิภาพการดูของคุณ
- แนะนำ Ruby on Rails Patterns และ Anti-patterns
ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!