ActiveRecord เป็นฟีเจอร์เวทมนต์ที่สุดของ Ruby on Rails เราปกติไม่ต้องกังวลเกี่ยวกับ Inner Workings ของมัน แต่เมื่อเราต้องการ นี่คือวิธีที่ AppSignal ช่วยให้เรารู้ว่ากำลังเกิดอะไรขึ้นใต้ฮุ๊ด
ActiveRecord คืออะไร?
เพื่อพูดถึง ActiveRecord เราต้องคิดถึง Frameworks ก่อน โดยเฉพาะ MVC Frameworks MVC ย่อมาจาก Model-View-Controller และเป็น Software Design Pattern ยอดนิยมสำหรับ Graphical และ Web Applications
MVC Frameworks ประกอบด้วย:
- Model: จัดการ Business Logic และ Data Persistence
- View: ขับเคลื่อน Presentation Layer และวาด User Interface
- Controller: ผูกมั่นทุกอย่างเข้าด้วยกัน
ActiveRecord เป็น Component Model ใน Ruby on Rails Framework มัน Introduce Abstraction Layer ระหว่าง Code และ Data เพื่อให้เราไม่ต้องเขียน SQL Code เอง แต่ละ Model Map ไปยัง Table หนึ่งและให้ Methods ต่างๆ สำหรับ CRUD Operations (Create, Read, Update และ Delete)
การ Monitor ActiveRecord ด้วย AppSignal
Abstractions รู้สึกเวทมนต์ — พวกเขาช่วยให้เราเพิกเฉยรายละเอียดที่เราไม่ต้องรู้ และ Focus บน Task ที่มีอยู่ แต่เมื่อสิ่งต่างๆ ไม่ทำงานตามที่คาด ความซับซ้อนที่พวกเขาเพิ่มเข้ามาสามารถทำให้ยากต่อการหา Root Cause AppSignal ให้เรา Detailed Breakdown ของสิ่งที่เกิดขึ้นจริงใน Rails
Response Time Graph
มาได้ First Look ปัญหา การ Troubleshoot Performance Issues เป็น Iterative Process เมื่อคุณมี Rails Application Report ไป AppSignal แล้ว ไปที่ Incidents > Performance
Response Time Graph จะแสดง Response Time Percentiles สำหรับแต่ละ Namespace ActiveRecord Events ถูก Assign ให้ Namespace ของ Request หรือ Background Job ที่ Queries ถูก Execute อย่างอัตโนมัติ
ต่อไป ให้ดู Event Group Graph มันแสดงเวลาที่ Consumed โดย Category เช็คว่าเวลา Relative มากน้อยแค่ไหนที่ถูกใช้โดย active_record เช็ค Usage ใน Namespaces ทั้งหมดของคุณ
Graph จะบอกทันทีว่าเราควร Focus Code-optimizing Efforts ที่ไหน
ขณะที่คุณอยู่ใน Performance Graph Dashboard ให้เช็ค Response Time และ Throughput เพื่อให้แน่ใจว่าไม่มี Activity สูงกว่าปกติบนแอปพลิเคชันของคุณ
Slow Queries Dashboard
ตอนนี้เราระบุแล้วว่าปัญหาเป็น Data-bound มาดูว่าสามารถ Zoom ในเพื่อหา Root Cause ได้ไหม
เปิด Dashboard Improve > Slow Queries หน้านี้แสดงรายการ SQL Queries จัดอันดับโดย Impact บนเวลาทั้งหมด ActiveRecord-originated Queries ทั้งหมดแสดงเป็น Events sql.active_record
ลองคลิกที่ Query บนสุดเพื่อดู Details Dashboard แสดง Average Duration และ Query Text
Scroll ลงมาจะแสดง Response Time ของ Query ในช่วงไม่กี่ชั่วโมงที่ผ่านมา และ Originating Action
คุณอาจพบว่า Actions บางอย่างมี Associated Incident หมายความว่า AppSignal สร้าง Performance Incident ขณะที่ Query กำลังรัน แต่ไม่ได้หมายความว่า ActiveRecord เป็น Cause โดยจำเป็น
Performance Measurements Dashboard
Performance Measurement Incidents ถูกเปิดเมื่อ AppSignal Record Endpoint ใหม่หรือ Background Job
Incidents อยู่ที่ Dashboard Performance > Issue List
Incident Page แสดง Elapsed Time และ Number of Allocations สำหรับแต่ละ MVC Component ActiveRecord Problems จะแสดง Long Durations ใน Category active_record
Event Timeline แสดงว่า Event Progress ตลอดเวลาอย่างไร
การค้นหา ActiveRecord Issues
ในส่วนนี้ เราจะเห็นว่า AppSignal ช่วยเราระบุ ActiveError Issues ทั่วไปได้อย่างไร
การเลือก Column ที่เกี่ยวข้อง
Database Wisdom บอกว่าเราควร Retrieve Columns ที่จำเป็นสำหรับงานเสมอ ตัวอย่างเช่น แทน SELECT * FROM people เราควร SELECT first_name, surname, birthdate FROM people นั่นดีแล้ว แต่นั่นทำอย่างไรใน Rails?
โดย Default ActiveRecord Retrieve ทุก Columns
Person.all.each { # process data }
โชคดีที่เรามี Method select เพื่อเลือก Columns ที่จำเป็น:
Person.select(:name, :address, :birthdate).each { # process data }
อาจ聽起來เหมือนว่าฉัน กระตือรือร้นเกี่ยวรายละเอียดเล็กๆ แตบน Wide Tables การ Select ทุก Columns ก็แค่ Wasteful คุณจะสังเกตเห็นว่าเมื่อสิ่งนี้เกิดขึ้น ActiveRecord Allocate Memory Chunks ใหญ่ๆ:
N+1 Problems
N+1 Problem เกิดขึ้นเมื่อ Application ได้ Set ของ Records จาก Database และ Loop ผ่านมัน สิ่งนี้ทำให้ Application Execute N+1 Queries โดย N คือจำนวน Rows ที่ได้รับเริ่มต้น ตามที่คุณจินตนาการได้ Pattern นี้ Scale Poorly เมื่อ Table โต มันเป็นปัญหาที่ทำลายล้างมากจน AppSignal เตือนคุณโดยเฉพาะ:
N+1 Problems ปกติปรากฏกับ Associated Models จินตนาการว่าเรามี Person Model:
class Person < ApplicationRecord has_many :addresses end
แต่ละ Person สามารถมี Addresses หลายตัว:
class Address < ApplicationRecord belongs_to :person end
วิธีตรงไปตรงมาที่สุดในการ Retrieve Data นำไปสู่ N+1 Problem:
class RelatedTablesController < ApplicationController def index Person.all.each do |person| person.addresses.each do |address| address.address end end end end
คุณเห็นใน AppSignal ว่า Application กำลังรัน SELECT ต่อ Person:
Fix สำหรับกรณีนี้ Simple: ใช้ Includes ซึ่งบอก ActiveRecord ให้ Optimize Query สำหรับ Columns ที่ต้องการ:
class RelatedTablesController < ApplicationController def index Person.all.includes(:addresses).each do |person| person.addresses.each do |address| address.address end end end end
ตอนนี้เรามี 2 Queries แทน 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, . . .)
Execute น้อยที่สุดที่จำเป็น Queries ต่อ Table
นี่บางครั้ง Confused กับ N+1 Problem แต่ต่างกันนิดหน่อย เมื่อเรา Query Table เราควร Retrieve ทุก Data ที่เราคิดว่าจะต้องการ เพื่อ Minimize Read Operations มีโค้ดที่ดู Innocent มากที่ Trigger Redundant Queries มาก ตัวอย่างดูว่า count ทำให้เกิด SELECT COUNT(*) Query เสมอใน View ต่อไป:
<ul> <% @people.each do |person| %> <li><%= person.name %></li> <% end %> </ul> <h2>Number of Persons: <%= @people.count %></h2>
ตอนนี้ ActiveRecord ทำ 2 Queries:
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 อาการที่คุณจะสังเกตคือมี 2 Events active_record บน Table เดียวกัน:
ความเป็นจริงคือเราไม่ต้องการ 2 Queries เรามี Data ที่จำเป็นทั้งหมดใน Memory แล้ว ในกรณีนี้ Solution คือ Swap count ด้วย size:
<ul> <% @people.each do |person| %> <li><%= person.name %></li> <% end %> </ul> <h2>Number of Persons: <%= @people.size %></h2>
ตอนนี้เรามี Single 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
Solution อีกอย่างคือใช้ Preload เพื่อ Cache Data ใน Memory
การ Compute Aggregated Data ใน Rails
Aggregation ใช้ Compute Value จาก Set ของ Data Databases ยอดเยี่ยมในการทำงานกับ Big Datasets นี่คือสิ่งที่พวกเขาทำและสิ่งที่เราใช้พวกเขาทำ ขณะที่การใช้ Rails เพื่อ Aggregate ไม่ Scale เนื่องจากต้อง Get All Records จาก Database Hold ใน Memory และ Calculate ด้วย High-level Code
เราทำ Aggregation ใน Rails ทุกครั้งที่เราใช้ Ruby Functions เหมือน max, min, หรือ sum บน ActiveRecord Elements หรือ Enumerables อื่นๆ
class AggregatedColumnsController < ApplicationController def index @mean = Number.pluck(:number).sum() end end
โชคดีที่ ActiveRecord Models Include Methods เฉพาะที่ Map ไป Aggregation Functions ใน Database ตัวอย่าง Query ต่อไปนี้ Map ไป 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"
หากคุณต้องการ Aggregation Functions ที่ซับซ้อนหรือ Combined มากขึ้น คุณอาจต้อง Include Raw SQL เล็กน้อย:
sql = "SELECT AVG(number), STDDEV(number), VAR(number) FROM ..." @results = ActiveRecord::Base.connection.execute(sql)
การจัดการ Big Transactions
SQL Transactions ให้ Consistent และ Atomic Updates เมื่อเราใช้ Transaction เพื่อทำการเปลี่ยนแปลง ไม่ว่าทุก Row จะ Update สำเร็จ หรือทั้งหมดจะ Roll Back ในทุกกรณี Database จะคงอยู่ใน Consistent State เสมอ
เราสามารถ Bundle Batch ของ Changes ใน Single Transaction ด้วย 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
มี Cases ถูกต้องมากสำหรับการใช้ Large Transactions นี่อย่างไรก็ตาม รันความเสี่ยงที่จะทำให้ Database ช้า นอกจากนี้ Transactions ที่เกิน Thresholds จะทำให้ Database Reject พวกเขา
Sign แรกของ Transactions ที่ใหญ่เกินไปคือใช้เวลามากบน Events commit transaction:
นอกจาก Configuration Issues บน Database เอง Solution คือ Break Down Transaction เป็น Chunks เล็กๆ
การ Monitor Database
บางครั้ง แม้เราจะ Search โค้ด เราก็ไม่พบอะไรผิดปกติ แล้วอาจมีปัญหาใน Database เอง Database Engines ซับซ้อน และหลายอย่างอาจผิดปกติ: Low Memory, Default Settings, Missing Indexes, Backup Jobs ที่ Scheduled ไม่สะดวก Picture ไม่สมบูรณ์หากเราไม่ได้ข้อมูลเกี่ยวกับ Machine ที่รัน Database
หากเรา Run Database เอง เราสามารถ Install Standalone Agent เพื่อ Capture Host Metrics เพื่อเรียนรู้เพิ่มเติมเกี่ยวกับการใช้ Standalone Agent อ่าน: Monitoring Any System with StatsD and AppSignal's Standalone Agent
Signs ต่อไปนี้อาจแสดงว่ามีบางอย่างเกิดขึ้นกับ Database ไปที่ Dashboard Inspect > Host Metrics เพื่อดู Resource Usage ใน Server ของคุณ:
- High Memory Usage: Databases ต้องการ Memory มาก — มากกว่าระบบส่วนใหญ่ — เพื่อรันได้ถูกต้อง เมื่อ Dataset โต Memory Requirements ปกติ Scale ตามไป เราจะต้อง Add Memory มากขึ้น หรือ Split Dataset ระหว่าง Machines ต่างๆ ไปเรื่อยๆ
- Swap Usage: ในลักษณะที่เป็น Ideal Database Machines ไม่ควรต้องการ Swap Memory เลย Swapping ฆ่า Database Performance การมีอยู่หมายความว่ามี Configuration หรือ Memory Problem ลึกกว่า
- High I/O Usage: Peaks ใน Disk Activity อาจมาจาก Maintenance Tasks เช่น Re-indexing หรือ Backing Up Database การทำ Tasks เหล่านี้ใน Peak Hours จะ Hit Performance แน่นอน
👋 หากคุณชอบบทความนี้ มีอีกมากมายที่เราเขียนเกี่ยว Ruby (on Rails) Performance ดู Ruby Performance Monitoring Checklist ของเรา
สรุป
การ Diagnose Performance Issues ไม่มีวันเป็นงานง่าย วันนี้ เราเรียนรู้วิธีใช้ Appsignal เพื่อ Pinpoint Source ของปัญหาได้อย่างรวดเร็ว
มาเรียนรู้ต่อเกี่ยวกับ AppSignal และ Ruby on Rails:
- ActiveRecord Performance: N+1 Queries Antipattern
- Rails is Fast: Optimize Your View Performance
- Introduction to Ruby on Rails Patterns and Anti-patterns
P.S. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่เผยแพร่ สมัครรับข่าวสาร Ruby Magic newsletter และจะไม่พลาดโพสต์เด็ดๆ ไปเลย!