Tăng tốc độ Rails app bằng Rails Cach

Ruby những phiên bản gần đây được cải thiện rất nhiều nhưng hiện tại vẫn còn chậm so với một số ngôn ngữ khác. Sau đây tôi muốn giới thiệu đến mọi người một trong những kỹ thuật phổ biến nhất được sử dụng để cải thiện hiệu suất ứng dụng là bộ nhớ đệm. Nhờ phương pháp này, ứng dụng có thể đạt được hiệu năng cao hơn thậm chí vài lần mà không phải chi nhiều hơn cho các server mới và mạnh mẽ hơn. Trong trường hợp chúng ta đã dự đoán các điểm làm giảm performance, ta nên sử dụng Caching làm chiến lược đầu tiên để giải quyết vấn đề. Chỉ nên cân nhắc áp dụng rộng rãi khi chúng ta không có khả năng lưu trữ nhiều bộ nhớ hơn.Tuy nhiên, việc sử dụng bộ nhớ cache thông minh thường là con đường duy nhất để đạt được thời gian đáp ứng nhanh của server trong Rails - dễ dàng đẩy nhanh thời gian đáp ứng.

Caching

Caching nghĩa là lưu trữ nội dung được sinh ra trong chu trình request-response và tái nội dung khi có một request tương tự trước đó. Nó giúp nâng cao hiệu suất của ứng dụng và có thể phục vụ được ngàn người dùng đồng thời.

Có 6 kĩ thuật caching được sử dụng chủ yếu trong Ruby On Rails (ROR): Page Caching, Action Caching, Fragment Caching, Russian Doll Caching, Low-Level Caching, SQL Caching. Với một project khi tạo ra thì ROR luôn mặc định cung cấp Fragment Caching. Để có thể sử dụng được các kĩ thuật khác ví dụ như page và action caching chúng ta cần thêm gem actionpack-page_cachingactionpack-action_caching vào Gemile. Hơn nữa caching chỉ mặc định trong trong môi trường production nên để có thể cho caching hoạt động ở môi trường khác chúng ta phải thêm vào trong file config/enviroments/*.rb dòng:

config.action_controller.perform_caching = true

1. Page Caching Page Caching là hình thức đơn giản nhất của bộ nhớ đệm. Trong request đầu tiên, nội dung trang được tự động tạo ra và được lưu dưới dạng tệp HTML tĩnh. Trong request tiếp theo, khi người dùng muốn truy cập vào cùng một URL này, một trang không phải là trang động động nhưng được phục vụ từ tệp tĩnh từ nội dung đã lưu ở request trước. Mặc dù kỹ thuật này rất nhanh nhưng trên thực tế, thường không được sử dụng. Lý do chính là nó chỉ có thể sử dụng chúng cho một tình huống cụ thể, chủ yếu trong các trường hợp khi ứng dụng của chúng ta phân phối cùng một nội dung cho tất cả người dùng. Nó được được thi hành bởi webserver mà không cần phải đi qua ngăn xếp của Rails.

Page Caching không thể được sử dụng cho các actions mà có before_action. Ví dụ như, các trang cần chứng thực người dùng.

Kỹ thuật này không còn có sẵn theo mặc định từ Rails 4. Nếu bạn muốn sử dụng nó, như đã nói ở trên đầu tiên bạn phải thêm một actionpack-page_caching gem cho app.

2. Action Caching Tương tự như việc lưu trữ trang, kỹ thuật này cũng lưu trữ đầy đủ nội dung của trang vào lần request đầu tiên nhưng ngược lại với phương pháp trước đó là kỹ thuật này yêu cầu phải đi qua ngăn xếp Rails. Đây là sự khác biệt chính, nhưng nhờ có sự khác biệt đó, chúng ta có thể sử dụng cách tiếp cận này cho các ứng dụng yêu cầu xác thực. Tuy nhiên kĩ thuật này đã không dùng được từ Rails 4.

class ItemsController < ActionController
  before_filter :authenticate
  caches_action :show

  def show
    @items = Items.find(params[:id])
  end
end

3. Fragment Caching Lưu trữ bộ nhớ đệm có lẽ là kỹ thuật thường được sử dụng nhất. Các ứng dụng web động đa số xây từ các trang với rất nhiều thành phần mà không phải tất cả trong số chúng có chùng chung các đặc điểm caching. Khi các phần khác nhau của 1 trang cần được cache thì bạn có thể sử dụng Fragme4nt Caching.

Fragment Caching cho phép 1 fragment được bọc trong khối cache và phục vụ ra khỏi cache khi có request tiếp theo đến. Để hiểu rõ hơn bạn xem ví dụ dưới đây. Nếu bạn muốn cache mỗi product trên mỗi trang bạn có thể làm như sau:

<% @products.each do |product| %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

Khi ứng dụng của bạn nhận được request đầu tiên thì Rails sẽ viết một cache entry mới với key duy nhất như ví dụ dưới đây:

views/products/1-201505056193031061005000/bea67108094918eeba42cd4a6e786901

Số ở giữa là product_id và sau nó là giá trị timestamp trong thuộc tính updated_at của bản bản ghi product. Rails sử dụng giá trị timestamp để đảm bảo rằng nó không phải là dữ liệu cũ. Nếu giá trị của updated_at thay đổi thì sẽ có một key mới sẽ đc sinh ra. Sau đó sẽ ghi một cache mới vào key đó và cache cũ đc ghi đè để nó sẽ không được sử dụng lại (đảm bảo dữ liệu cache luôn là dữ liệu mới nhất). Đây được gọi là key-based expiration.

Các Cache fragment cũng sẽ đc đặt thời gian hết hạn khi fragment view thay đổi (ví dụ như HTML trong view thay đổi). Xâu các kí tự ở cuối của key là một mảng băm md5 được tính dựa trên nội dung của view fragment mà bạn đang caching. Nếu bạn thây đổi view fragment thì mảng băm md5 cũng sẽ thay đổi theo.

Nếu bạn muốn cache một fragment dưới các điều kiện cụ thể nào thì bạn có thể sử dụng cache_if hoặc cache_unless

Collection Caching Render helper cũng có thể cache các templates riêng biệt mà được render cho một tập hợp. Nó có thể thậm chí thay thế ví dụ trước với việc nó đọc tất cả các cache templates một lần thay vì đọc từ cái một. Đoạn code dưới sẽ mô tả rõ hơn.

<%= render partial: 'products/product', collection: @products, cached: true %>

4. Russian Doll Caching Bạn có lẽ muốn rằng các fragments cached lồng bên trong các fragment cached khác. Điều đó được gọi là Russian doll caching.

Ưu điểm của Russian doll caching là nếu một sản phẩm được update thì tất cả các fragment khác có thể được tái sử dụng khi sinh lại fragment.

Như được giải thích ở phần trước thì một file được cache sẽ có thời gian hết hạn nếu giá trị của updated_at thay đổi trong bản ghi mà file cache phụ thuộc một cách trực tiếp. Tuy nhiên, Nó sẽ không expire bất kì cache fragment nào mà bị lồng nhau.

Ví dụ:

<% cache product do %>
  <%= render product.games %>
<% end %>

hoặc cách khác:

 <% cache game do %>
    <%= render game %>
 <% end %>

Nếu bất kì thuộc tính nào của game bị thay đổi, thì giá trị updated_at sẽ được đặt lại là thời gian hiện tại. Tuy nhiên bởi vì updated_at sẽ không được thay đổi cho đối tượng product nên cache đó sẽ không được expired và ứng dụng của bạn sẽ đáp ứng dữ liệu cũ. Để fix điều này, thì chúng ta sẽ thắt chặt các model với nhau với phương thức touch.

class Product < ApplicationRecord
   has_many :games
end

class Game < ApplicationRecord
  belongs_to :product, touch: true
end

Với touch được đặt là true thì bất kì action nào mà thay đổi updated_at cho một bản ghi game cũng sẽ thay đổi theo nó.

5. Low-Level Caching Thi thoảng bạn cần cache một giá trị cụ thể hoặc kết quả của một câu lệnh query thay vì caching cả view fragments. Cơ chế caching của rails thực hiện rất tốt cho việc lưu trữ bất kì các kiểu dữ liệu nào.

Cách hiệu quả nhất để thực hiện low-level caching là việc sử dụng phương thức Rails.cache.fetch. Phương thức này làm cả việc đọc và ghi cache. Khi chỉ được gửi một đối số duy nhất thì key được lấy về và giá trị từ cache được trả lại. Nếu một block được gửi thì kết của block sẽ đc cached để key được cho vào kết đc trả về.

Với ví dụ dưới thì sẽ rõ hơn. Một ứng dụng có model Product với instant method là tra cứu giá sản phẩm trên một website cạnh tranh. Dữ liệu được trả về bởi phương thức này sẽ là hoàn hảo cho low-level caching.

class Product < ApplicationRecord
  def competing_price
    Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
      Competitor::API.find_price(id)
    end
  end
end

6. SQL Caching Query caching là một điểm của Rails mà cache được tập hợp kết quả bởi mỗi câu lệnh query. Nếu Rails gặp lại cùng câu lệnh query lần nữa cho request, thì nó sẽ sử dụng kết quả được cache mà không phải vào database lấy dữ liệu lại lần nữa.

Ví dụ:

class ProductsController < ApplicationController
  def index
    # Run a find query
    @products = Product.all
    ...
    # Run the same query again
    @products = Product.all
  end
end

Lần thứ 2 cùng 1 câu query chạy với database nhưng nó không thực sự vào database. Lần thứ nhất kết quả được trả lại và được lưu trong query cache (trong bộ nhớ) và lần thứ 2 kết quả đc lấy từ trong bộ nhớ đó.

Tuy nhiên, dữ liệu được cache tại thời điểm bắt đầu của một action và bị xóa ở thời điểm cuối cùng của action (thay đổi dữ liệu cache mới) do đó nó tồn tại duy nhất trong suốt thời gian hành động. Nếu bạn muốn lưu trữ các kết quả của câu query trong thời gian dài (dù dữ liệu đã được thay đổi) thì bạn nên dùng low level caching.