Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Tối ưu Hiệu suất ActiveRecord: Hướng dẫn Thực tế

ActiveRecord là tính năng "ma thuật" nhất của Ruby on Rails. Chúng ta thường không cần lo lắng về cách nó hoạt động bên trong, nhưng khi cần, đây là cách AppSignal giúp chúng ta biết điều gì đang xảy ra under the hood.

ActiveRecord Là Gì?

Để nói về ActiveRecord, chúng ta cần trước tiên nghĩ về framework, cụ thể là MVC framework. MVC là viết tắt của Model-View-Controller, và đây là software design pattern phổ biến cho ứng dụng đồ họa và web.

MVC framework bao gồm:

  • Model: xử lý business logic và data persistence.
  • View: điều khiển presentation layer và vẽ user interface.
  • Controller: gắn kết mọi thứ lại với nhau.

ActiveRecord là thành phần model trong framework Ruby on Rails. Nó giới thiệu một abstraction layer giữa code và data, nên chúng ta không phải tự viết SQL. Mỗi model được map đến một table và cung cấp nhiều method để thực hiện CRUD operations (Create, Read, Update, Delete).

Giám sát ActiveRecord với AppSignal

Abstraction cảm thấy ma thuật — chúng giúp chúng ta bỏ qua chi tiết không cần biết và tập trung vào nhiệm vụ. Nhưng khi thứ không hoạt động như kỳ vọng, độ phức tạp chúng thêm vào có thể làm khó xác định nguyên nhân gốc rễ. AppSignal có thể cho chúng ta breakdown chi tiết những gì thực sự xảy ra trong Rails.

Biểu đồ Response Time

Hãy nhìn đầu tiên vào vấn đề. Troubleshooting performance issue là quá trình lặp lại. Khi đã có Rails app report lên AppSignal, vào Incidents → Performance.

Biểu đồ Response Time sẽ hiển thị response time percentiles cho mỗi namespace. ActiveRecord events tự động được gán cho namespace của request hoặc background job mà query được execute.

Tiếp theo, xem biểu đồ Event Group. Nó hiển thị thời gian tiêu thụ theo category. Kiểm tra xem active_record tiêu tốn bao nhiêu thời gian tương đối. Kiểm tra usage trong tất cả namespace.

Biểu đồ sẽ ngay lập tức cho chúng ta biết nên tập trung nỗ lực tối ưu code ở đâu.

Khi đang ở performance graph dashboard, hãy check response time và throughput để đảm bảo không có hoạt động higher-than-usual trên ứng dụng.

Dashboard Slow Queries (Truy vấn Chậm)

Bây giờ chúng ta đã xác định vấn đề là data-bound, hãy xem có thể zoom in để xác định nguyên nhân gốc rễ không.

Mở dashboard Improve → Slow Queries. Trang này hiển thị danh sách SQL query được rank theo impact trên tổng thời gian. Tất cả query gốc từ ActiveRecord hiển thị như event sql.active_record.

Thử click vào query top để xem chi tiết. Dashboard hiển thị average duration và query text.

Cuộn xuống sẽ hiển thị response time của query trong vài giờ qua và action gốc.

Bạn có thể tìm thấy một số action có incident liên quan. Nghĩa là AppSignal tạo performance incident trong khi query chạy, nhưng không nhất thiết nghĩa là ActiveRecord là nguyên nhân.

Dashboard Performance Measurements (Đo lường Hiệu suất)

Performance measurement incidents được mở khi AppSignal ghi nhận endpoint hoặc background job mới.

Incidents nằm ở dashboard Performance → Issue List.

Trang incident hiển thị elapsed time và số allocations cho từng thành phần MVC. Vấn đề ActiveRecord sẽ thể hiện duration dài trong category active_record.

Event timeline hiển thị event tiến triển như thế nào theo thời gian.

Tìm Vấn đề ActiveRecord

Trong phần này, chúng ta sẽ thấy AppSignal giúp xác định một số issue ActiveRecord phổ biến như thế nào.

Chọn Column Phù Hợp (Selecting the Relevant Columns)

Kiến thức database khuyên luôn retrieve column cần thiết cho việc. Ví dụ, thay vì SELECT * FROM people chúng ta nên SELECT first_name, surname, birthdate FROM people. Đó là tốt, nhưng trên Rails làm sao?

Mặc định, ActiveRecord retrieve tất cả column.

Person.all.each { # process data }

May mắn thay, chúng ta có method select để pick column cần thiết:

Person.select(:name, :address, :birthdate).each { # process data }

Có thể nghe như tôi ám ảnh chi tiết nhỏ. Nhưng trên wide table, select tất cả column chỉ là lãng phí. Bạn sẽ nhận ra khi điều này xảy ra, ActiveRecord cấp phát chunks bộ nhớ lớn:

Vấn đề N+1

Vấn đề N+1 xảy ra khi app lấy một set record từ database và loop qua nó. Điều này khiến app execute N+1 query, với N là số row ban đầu lấy được. Như bạn tưởng tượng, pattern này scale kém khi table lớn. Nó gây hại đến mức AppSignal cụ thể cảnh báo bạn về nó:

N+1 problem thường xuất hiện với associated model. Hãy tưởng tượng chúng ta có model Person:

class Person < ApplicationRecord has_many :addresses end

Mỗi person có thể có nhiều address:

class Address < ApplicationRecord belongs_to :person end

Cách đơn giản nhất retrieve data dẫn đến N+1 problem:

class RelatedTablesController < ApplicationController def index Person.all.each do |person| person.addresses.each do |address| address.address end end end end

Bạn thấy trong AppSignal app đang chạy SELECT per person:

Fix cho case này đơn giản: dùng includes, nói với ActiveRecord optimize query cho column cần thiết:

class RelatedTablesController < ApplicationController def index Person.all.includes(:addresses).each do |person| person.addresses.each do |address| address.address end end end end

Bây giờ chúng ta có 2 query thay vì 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, . . .)

Thực thi Ít Query Nhất Mỗi Table (Execute the Least Necessary Queries Per Table)

Điều này đôi khi nhầm lẫn với N+1 problem, nhưng hơi khác. Khi query một table, chúng ta nên retrieve tất cả data chúng ta nghĩ sẽ cần để minimize read operation. Tuy nhiên, có nhiều code innocent-looking trigger redundant query. Ví dụ, xem count luôn dẫn đến query SELECT COUNT(*) trong view sau:

<ul> <% @people.each do |person| %> <li><%= person.name %></li> <% end %> </ul> <h2>Number of Persons: <%= @people.count %></h2>

Bây giờ ActiveRecord làm 2 query:

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

Trong AppSignal, triệu chứng bạn nhận ra là có 2 event active_record trên cùng table:

Thực tế là chúng ta không cần 2 query; chúng ta đã có tất cả data cần trong memory. Trong case này, giải pháp là swap count với size:

<ul> <% @people.each do |person| %> <li><%= person.name %></li> <% end %> </ul> <h2>Number of Persons: <%= @people.size %></h2>

Bây giờ chúng ta có single SELECT, như nên là:

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

Giải pháp khác là dùng preload để cache data trong memory.

Tính toán Dữ liệu Tổng hợp trong Rails (Computing Aggregated Data in Rails)

Aggregation dùng để tính giá trị dựa trên set data. Database rất giỏi làm việc với dataset lớn. Đó là việc chúng làm và việc chúng ta dùng chúng. Mặt khác, dùng Rails để aggregate không scale vì nó yêu cầu lấy tất cả record từ database, giữ trong memory, rồi tính toán bằng high-level code.

Chúng ta đang aggregate trong Rails bất cứ khi nào dùng Ruby function như max, min, hoặc sum trên ActiveRecord elements hoặc enumerables khác.

class AggregatedColumnsController < ApplicationController def index @mean = Number.pluck(:number).sum() end end

May mắn thay, ActiveRecord model include method cụ thể map đến aggregation function trong database. Ví dụ, query sau map đến SELECT SUM(number) FROM ..., nhanh và rẻ hơn nhiều so với ví dụ trước:

# 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"

Nếu cần aggregation function phức tạp hoặc kết hợp hơn, bạn có thể cần include raw SQL:

sql = "SELECT AVG(number), STDDEV(number), VAR(number) FROM ..." @results = ActiveRecord::Base.connection.execute(sql)

Quản lý Big Transaction

SQL transaction đảm bảo update consistent và atomic. Khi dùng transaction để thay đổi, hoặc mọi row được update thành công, hoặc toàn bộ bị roll back. Trong mọi trường hợp, database luôn ở trạng thái consistent.

Chúng ta có thể gom batch thay đổi trong một transaction với 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

Có nhiều case hợp lý cho large transaction. Tuy nhiên, chúng rủi ro làm chậm database. Bên cạnh đó, transaction vượt ngưỡng nhất định sẽ dẫn đến database reject chúng.

Dấu hiệu đầu tiên transaction quá lớn là dành nhiều thời gian cho event commit transaction:

Trừ configuration issue trên database, giải pháp là chia transaction thành chunks nhỏ hơn.

Giám sát Database (Monitoring The Database)

Đôi khi dù search code, chúng ta không tìm thấy gì sai. Khi đó, có thể có vấn đề ở chính database. Database engine phức tạp, nhiều thứ có thể sai: low memory, default setting, missing index, backup job scheduled không thuận tiện. Hình ảnh không hoàn chỉnh trừ khi có info về machine chạy database.

Nếu chạy database riêng, chúng ta có thể cài standalone agent để capture host metrics. Để học thêm về standalone agent, đọc: Monitoring Any System with StatsD and AppSignal's Standalone Agent.

Các dấu hiệu sau có thể cho thấy điều gì đang xảy ra với database. Vào dashboard Inspect → Host Metrics để xem resource usage server:

  • High memory usage: database cần nhiều memory — hơn hầu hết hệ thống khác — để chạy đúng. Khi dataset lớn, memory requirement thường scale cùng. Chúng ta cần add memory hoặc chia dataset trên máy khác từ thời gian đến thời gian.
  • Swap usage: lý tưởng database machine không nên cần swap memory. Swapping giết performance database. Sự hiện diện của nó nghĩa là có vấn đề cấu hình hoặc memory sâu hơn.
  • High I/O usage: peak disk activity có thể do maintenance task như re-indexing hoặc backup database. Làm task này trong peak hours chắc chắn hit performance.

👋 Nếu bạn thích bài này, chúng tôi đã viết nhiều hơn về Ruby (on Rails) performance, xem Ruby performance monitoring checklist của chúng tôi.

Kết luận

Chẩn đoán performance issue không bao giờ là nhiệm vụ dễ dàng. Hôm nay, chúng ta đã học cách dùng AppSignal để nhanh chóng pinpoint nguồn gốc vấn đề.

Hãy tiếp tục học về AppSignal và 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. Nếu bạn muốn đọc bài Ruby Magic ngay khi ra mắt, hãy subscribe newsletter Ruby Magic và không bao giờ bỏ lỡ bài nào!