Về mặt lập trình, caching đề cập đến lưu trữ giá trị (hoặc các giá trị) để truy xuất nhanh trong tương lai. Thông thường, bạn làm điều này với giá trị chậm tính toán vì lý do nào đó; ví dụ, cần gọi external API để lấy, hoặc liên quan đến nhiều number-crunching để sinh ra.
Giá trị cache thường lưu trên server riêng, như memcached hoặc Redis. Chúng có thể lưu trên disk hoặc RAM. Trong code, chúng ta thường 'cache' dữ liệu trong biến để tránh gọi hàm tốn kém nhiều lần.
data = some_calculation() a(data) b(data)
Đánh đổi cho tốc độ là bạn dùng dữ liệu cũ. Nếu dữ liệu cache trở nên 'stale' (lỗi thời) và không còn chính xác? Bạn phải clear cache để 'invalidate' nó.
Lý do chống lại Caching
Như câu nói cũ, chỉ có 2 vấn đề khó trong khoa học máy tính: 1. Đặt tên thứ 2. Cache invalidation 3. Off-by-one errors
Tại sao cache invalidation khó? Giá trị cache, bản chất của nó, 'giấu' giá trị thực. Bất kỳ khi nào giá trị 'thực' thay đổi, bạn (có, bạn, lập trình viên) phải nhớ 'invalidate' cache để nó cập nhật.
Giả sử bạn thêm widget 'word count' vào text editor. Cần cập nhật word count khi user gõ. Cách đơn giản nhất là đếm lại từ mỗi keystroke, nhưng quá chậm. Có cách khác:
- Đếm từ khi load file.
- Lưu word-count vào biến (hoặc 'cache nó').
- Hiển thị nội dung biến ra màn hình.
Triển khai này nhanh hơn nhiều, nhưng cached 'word count' không thay đổi khi gõ. Để làm vậy, cần 'invalidate' cache khi mong đợi word count thay đổi.
Khi keystroke đến, bạn detect từ (tức space) và tăng word counter. Tất nhiên, cũng giảm khi user xóa từ. Dễ. Xong. Ticket tiếp.
...Nhưng đợt, bạn nhớ cập nhật word count khi user cut text vào clipboard không? Khi paste text? Khi spell-checker tách typo thành hai từ?
Vấn đề ở đây không phải cập nhật giá trị, khá tầm thường. Vấn đề là bạn phải nhớ cập nhật ở mọi nơi đơn lẻ. Bỏ lỡ chỉ một cập nhật gây vấn đề cache invalidation, nghĩa là bạn hiển thị giá trị stale cho user.
Với điều này, bạn thấy thêm caching mang lại phức tạp kỹ thuật và nguồn bug tiềm ẩn. Tất nhiên, vấn đề này giải quyết được, nhưng cần nhớ trước khi nhảy vào caching như giải pháp.
Tốc độ không cần Caching
Nếu bỏ caching ra, tăng tốc ứng dụng là xác định và sửa performance bottlenecks - hệ thống chậm hơn khả năng. Chúng ta nhóm thành ba loại:
- Database queries (quá nhiều hoặc quá chậm)
- View rendering
- Application code (ví dụ: tính toán nặng)
Khi làm performance, có hai kỹ thuật cần biết để tiến bộ: profiling và benchmarking.
Profiling
Profiling cho bạn biết ở đâu vấn đề trong app: Trang này chậm vì render template chậm? Hay vì hit database triệu lần?
Cho Ruby on Rails, khuyên dùng rack-mini-profiler, thêm widget nhỏ vào góc app. Nó cho overview tốt về việc render trang, như số query database fired, thời gian, số partial render.
Cho production (pro-tip: rack-mini-profiler chạy tốt production; chỉ chắc chắn nó chỉ hiện cho user nhất định, như admin hoặc developer), có dịch vụ online như Skylight, New Relic, Scout monitor hiệu năng trang.
Mục tiêu điển hình <= 100ms tốt cho render trang, vì dưới này khó user detect trong usage internet thực. Mục tiêu bạn khác tùy nhiều yếu tố. Có lúc làm legacy app hiệu năng tồi, tôi đặt mục tiêu <= 1 giây, không tốt nhưng tốt hơn nhiều so với lúc bắt đầu.
Benchmarking
Khi biết ở đâu vấn đề, dùng benchmark xem tác động (nếu có) của tối ưu lên hiệu năng. Tôi thích gem benchmark-ips cho loại công việc này, vì nó cho cách dễ đọc xem sự khác biệt code tạo ra.
Ví dụ tầm thường, so sánh string concatenation vs interpolation:
require 'benchmark/ips' @a = "abc" @b = "def" Benchmark.ips do |x| x.report("Concatenation") { @a + @b } x.report("Interpolation") { "#{@a}#{@b}" } x.compare! end
và kết quả:
Warming up -------------------------------------- Concatenation 316.022k i/100ms Interpolation 282.422k i/100ms Calculating ------------------------------------- Concatenation 10.353M (± 7.4%) i/s - 51.512M in 5.016567s Interpolation 6.615M (± 6.8%) i/s - 33.043M in 5.023636s Comparison: Concatenation: 10112435.3 i/s Interpolation: 6721867.3 i/s - 1.50x slower
Cho kết quả dễ đọc, interpolation chậm 1.5 lần concatenation (ít nhất cho string nhỏ). Vì vậy, khuyên copy phương thức muốn cải tiến và đặt tên mới. Bạn có thể chạy so sánh nhanh xem có cải thiện performance khi đi.
Sửa vấn đề hiệu năng
Lúc này, biết phần nào app chậm. Có benchmark đo cải thiện khi xảy ra. Giờ chỉ cần làm việc tối ưu thực sự. Kỹ thuật chọn phụ thuộc vào nơi vấn đề: database, view, hay application.
Hiệu năng Database
Cho vấn đề performance liên quan database, vài điều xem xét. Trước, tránh 'N+1 queries' đáng sợ. Tình huống này thường xảy ra render collection trong view. Ví dụ, user có 10 blog post, muốn hiển thị user và tất cả post. Cách naive đầu tiên:
# Controller def show @user = User.find(params[:id]) end # View Name: <%= @user.name %> Posts: <%= @user.posts each do |post| %> <div>Title: <%= post.title %></div> <% end %>
Cách trên lấy user (1 query) rồi fire query cho mỗi post (N=10 queries), tổng 11 (hoặc N+1). May mắn Rails giải quyết đơn giản bằng .includes(:post) vào ActiveRecord query. Ví dụ trên, chỉ đổi controller code:
def show @user = User.includes(:post).find(params[:id]) end
Giờ fetch user và tất cả post trong một query database.
Điều khác xem là nơi đẩy tính toán vào database, thường nhanh hơn làm cùng phép toán trong app. Dạng phổ biến là aggregations như:
total = Model.all.map(&:somefield).sum
Đây lấy tất cả record từ database, nhưng tổng giá trị xảy ra trong Ruby. Tăng tốc bằng cách để database tính:
total = Model.sum(:somefield)
Có thể cần phức tạp hơn, như nhân hai cột:
total = Model.sum('columna * columnb')
Database phổ biến hỗ trợ arithmetic cơ bản và aggregations như sum, average, nên hãy tìm map(...).sum trong codebase.
Hiệu năng View
Mặc dù tôi nói template-related performance woes thiên về caching như giải pháp, vẫn có low-hanging fruit muốn loại trừ trước.
Cho page-load times tổng quát, kiểm tra dùng minified sources cho Javascript hoặc CSS libraries (ít nhất production servers).
Cũng chú ý số lượng lớn partial được include. Nếu template _widget.html.erb mất 1ms process, nhưng có 100 widget trên trang, đó là 100ms mất rồi. Giải pháp là xem lại UI. 100 widget cùng lúc thường không UX tốt, có thể xem pagination hoặc thậm chí UI/UX overhaul triệt để.
Hiệu năng Application Code
Nếu vấn đề performance ở chính application code (tức thao tác dữ liệu) chứ không phải view hay database layer, bạn có vài lựa chọn. Một là xem ít nhất phần việc có thể đẩy vào database như query, như mô tả, hoặc database views với gem scenic.
Lựa chọn khác là chuyển 'heavy lifting' vào background job, tuy nhiên có thể yêu cầu thay đổi UI để handle việc giá trị giờ tính async.
Vẫn cần Caching; Bây giờ làm sao?
Đi qua tất cả, có thể bạn quyết định caching là giải pháp cần. Vậy làm gì? Hãy theo dõi vì đây là bài đầu trong loạt bài về các dạng caching trong Ruby on Rails.