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

Làm Chủ HTTP Caching Trong Ứng Dụng Ruby on Rails

Một cách chung để mô tả caching là lưu trữ một số dữ liệu để chúng ta có thể truy xuất nhanh nó sau. Đôi khi, điều này nghĩa là lưu trữ dữ liệu đã tính toán để không cần tính toán lại, nhưng cũng có thể tham chiếu đến việc lưu trữ dữ liệu cục bộ để tránh phải lấy nó lại. Máy tính của bạn làm điều này liên tục, khi hệ điều hành cố gắng giữ dữ liệu thường xuyên truy cập trong RAM để nó không phải lấy lại từ ổ cứng hoặc SSD.

Tương tự, trình duyệt cố gắng tái sử dụng tài nguyên đã tải xuống. Bạn có thể đã thấy điều này khi truy cập trang web mới lần đầu. Lần tải đầu tiên lâu hơn vì trình duyệt phải kéo xuống mọi thứ nó cần, bao gồm tất cả hình ảnh, javascript, và stylesheet. Một sự thật thú vị là khi bạn mới tải trang CNN homepage, trình duyệt tải nhiều dữ liệu hơn game Doom gốc năm 1993. Với sự tò mò, tại thời điểm viết bài, CNN tải xuống hơn 3MB trên máy của tôi, nén từ ~15MB, và đó là với ad blocker bật, trong khi cài đặt Doom gốc là ~2.2MB.

Để trình duyệt cache dữ liệu này, cần có sự phối hợp với server. Trình duyệt cần biết cái gì nó có thể cache và bao lâu; nếu không, nó có thể hiển thị nội dung cũ khi server có phiên bản mới hơn. Trong bài này, chúng ta sẽ xem cách phối hợp caching client-server này được thực hiện như thế nào và Rails cung cấp gì để thay đổi nó.

Mặc dù trọng tâm là cách Ruby on Rails xử lý điều này, cơ chế thực tế là một phần của đặc tả HTTP. Nói cách khác, caching chúng ta nói về ở đây được nướng vào hạ tầng của internet, làm cho nó trở thành đá góc của cách website và framework hiện đại được phát triển. Các framework khác nhau, như Rails, single-page applications (SPAs), và thậm chí static site, đều có thể dùng các cơ chế này để cải thiện hiệu suất.

HTTP Request-Response (Yêu Cầu-Phản Hồi HTTP)

Bạn có thể quen với vòng đời request-response, ít nhất ở mức cao: bạn click link trên trang web, trình duyệt gửi request đến server cho nội dung đó, và server gửi lại nội dung đó (Lưu ý tôi đang cố ý lướt qua nhiều độ phức tạp ở đây).

Hãy đào sâu một chút vào dữ liệu thực tế được gửi trong giao dịch qua lại này. Mỗi thông điệp HTTP có header và body (không nhầm lẫn với thẻ HTML <head> và <body>). Request header nói cho server biết path bạn đang cố truy cập và HTTP method nào dùng (ví dụ, GET/PUT/PATCH/POST). Nếu cần, bạn có thể đào vào các header này dùng developer tools của trình duyệt hoặc công cụ dòng lệnh, như curl:

# curl -v honeybadger.io
...
> GET / HTTP/1.1
> Host: honeybadger.io
> User-Agent: curl/7.64.1
> Accept: */*

Phần đầu của output này là request header. Chúng ta đang phát hành GET đến honeybadger.io. Điều này được theo sau bởi cái server gửi lại (response header):

> < HTTP/1.1 301 Moved Permanently
< Cache-Control: public, max-age=0, must-revalidate
< Content-Length: 39
< Content-Security-Policy: frame-ancestors 'none'
...
< Content-Type: text/plain

Response bao gồm HTTP code (ví dụ, 200 cho success hoặc 404 cho not found). Trong ví dụ này, là redirect vĩnh viễn (301) vì curl đang cố contact URL http, chuyển hướng đến https an toàn. Response header cũng bao gồm content type, là text/plain ở đây, nhưng một vài option phổ biến khác là text/html, text/css, text/javascript, và application/json.

Response body theo sau header. Trong trường hợp chúng ta, body trống vì 301 redirect không cần body. Nếu thử lại với curl -v https://www.honeybadger.io, bạn sẽ thấy nội dung homepage, giống như xem source trong trình duyệt.

Nếu bạn muốn tự thử, đây là hai mẹo:

  1. Để chỉ hiển thị response header với curl (ví dụ, không request header hay response body), dùng option -I, như curl -I localhost:3000.
  2. Mặc định, Rails không cache trong môi trường development; bạn có thể cần chạy rails dev:cache trước.

Header HTTP Cache-Control

Header chính chúng ta quan tâm, về caching, là header Cache-Control. Nó giúp xác định máy nào có thể cache response từ server Rails của chúng ta và khi nào dữ liệu cache đó hết hạn. Trong header Cache-Control, có vài trường, hầu hết tùy chọn. Chúng ta sẽ đi qua một số entry liên quan nhất, nhưng để biết thêm, bạn có thể kiểm tra spec HTTP chính thức tại w3.org.

Đây là mẫu từ response header Rails out-of-the-box cơ bản:

< Content-Type: text/html; charset=utf-8
< Etag: W/"b41ce6c6d4bde17fd61a09e36b1e52ad"
< Cache-Control: max-age=0, private, must-revalidate

max-age

Trường max-age là integer chứa số giây response hợp lệ. Mặc định, response Rails cho view sẽ có giá trị này là 0 (tức là response hết hạn ngay lập tức, và trình duyệt luôn nên lấy phiên bản mới).

public/private

Bao gồm public hoặc private trong header thiết lập server nào được phép cache response. Nếu header bao gồm private, nó chỉ được cache bởi client yêu cầu (ví dụ, trình duyệt), không phải server trung gian nào nó có thể đi qua để đến đó, như content delivery networks (CDNs) hoặc proxy. Nếu header bao gồm public, thì các server trung gian này được phép cache response. Rails đặt mỗi header thành private mặc định.

must-revalidate

Rails cũng đặt trường must-revalidate mặc định. Nghĩa là client phải contact server để xác nhận phiên bản cache của nó vẫn hợp lệ trước khi dùng. Để xác định phiên bản cache có hợp lệ, client và server dùng ETags.

ETags

ETags là header HTTP tùy chọn được thêm bởi server khi gửi response cho client. Thông thường, đây là loại checksum trên response bản thân. Khi client (tức là trình duyệt) cần request tài nguyên này lại, nó bao gồm ETag nó nhận được (giả sử nó có response trước cache) trong header HTTP If-None-Match. Server có thể phản hồi với HTTP code 304 ("Not Modified") và body rỗng. Điều này nghĩa là phiên bản trên server không thay đổi, vì vậy client nên dùng phiên bản cache.

Có hai loại ETags: strong (mạnh) và weak (yếu) (weak tag được ký hiệu bằng prefix W/). Chúng hoạt động giống nhau, nhưng strong ETag nghĩa là hai bản sao tài nguyên (phiên bản trên server và cái trong cache cục bộ) là 100% byte-for-byte identical (giống hệt byte per byte). Weak ETags, tuy chỉ ra rằng hai bản sao có thể không giống hệt byte, nhưng phiên bản cache vẫn có thể dùng. Ví dụ phổ biến là helper csrf_meta_tags của Rails, tạo token thay đổi liên tục; do đó, ngay cả khi bạn có trang tĩnh trong ứng dụng, nó sẽ không giống hệt byte khi refresh do token Cross-Site-Request-Forgery (CSRF). Rails dùng weak ETags mặc định.

ETags Trong Rails

Rails xử lý ETags tự động trên view. Nó bao gồm ETag trong header đi ra và có middleware kiểm tra header ETag đến và trả về code 304 (Not Modified) khi thích hợp. Đáng chú ý, tuy nhiên, vì Rails tạo view động, nó vẫn phải làm tất cả việc render trước khi có thể tìm ra ETag cho view đó. Điều này nghĩa là ngay cả khi ETags match, bạn chỉ tiết kiệm thời gian và băng thông gửi dữ liệu qua mạng, trái ngược với view caching, nơi bạn có thể bỏ qua bước render hoàn toàn nếu có phiên bản cache. Tuy nhiên, Rails cung cấp vài cách để tinh chỉnh ETag được tạo.

stale?

Một cách để khắc phục token CSRF luôn thay đổi làm thay đổi ETag là dùng helper stale? trong ActionController. Điều này cho phép bạn đặt ETag (strong hoặc weak) trực tiếp. Tuy nhiên, bạn cũng có thể đơn giản truyền nó một object, như model ActiveRecord, và nó sẽ tính ETag dựa trên timestamp updated_at của object hoặc dùng max updated_at nếu bạn truyền collection:

class UsersController < ApplicationController
  def index
    @users = User.includes(:posts).all
    render :index if stale?(@users)
  end
end

Bằng cách hit trang với curl, chúng ta thấy kết quả:

# curl -I localhost:3000 -- first page load
ETag: W/"af9ae8f2d66b9b6c4d0513f185638f1a"
# curl -I localhost:3000 -- reload (thay đổi do CSRF token)
ETag: W/"f06158417f290334f47ea2124e08d89d"
-- Thêm stale? vào controller code
# curl -I localhost:3000 -- reload
ETag: W/"04b9b99835c359f36551720d8e3ca6fe" -- giờ dùng `@users` tạo ETag
# curl -I localhost:3000 -- reload
ETag: W/"04b9b99835c359f36551720d8e3ca6fe" -- không thay đổi

Điều này cho chúng ta nhiều kiểm soát hơn khi client phải tải lại full payload, nhưng nó vẫn phải check với server mỗi lần để xác định cache có còn hợp lệ. Cái nếu chúng ta muốn bỏ qua check đó hoàn toàn? Đây là nơi trường max-age trong header đến.

expires_in Và http_cache_forever

Rails cho chúng ta vài phương thức helper trong ActionController để điều chỉnh trường max-age: expires_in và http_cache_forever. Cả hai hoạt động như bạn mong đợi dựa trên tên:

class UsersController < ApplicationController
  def index
    @users = User.includes(:posts).all
    expires_in 10.minutes
  end
end
# curl -I localhost:3000
Cache-Control: max-age=600, private

Rails đặt max-age thành 600 (10 phút tính bằng giây) và xóa trường must-revalidate. Bạn cũng có thể thay trường private bằng cách truyền named argument public: true.

http_cache_forever chủ yếu chỉ là wrapper quanh expires_in đặt max-age thành 100 năm và nhận block:

class UsersController < ApplicationController
  def index
    @users = User.includes(:posts).all
    http_cache_forever(public: true) do
      render :index
    end
  end
end
# curl -I localhost:3000
Cache-Control: max-age=3155695200, public

Loại caching cực dài hạn này là lý do Rails asset có "fingerprint" (dấu vân tay) appended, là hash của nội dung tệp và tạo tên tệp, như packs/js/application-4028feaf5babc1c1617b.js. "Fingerprint" ở cuối liên kết hiệu quả nội dung tệp với tên tệp. Nếu nội dung bao giờ thay đổi, tên tệp sẽ thay đổi. Nghĩa là trình duyệt có thể cache tệp này mãi mãi vì nếu nó bao giờ thay đổi, ngay cả nhỏ, fingerprint sẽ thay đổi; về mặt trình duyệt, đó là tệp hoàn toàn riêng biệt cần tải xuống.

Vùng Ảnh Hưởng (Spheres of Influence)

Bây giờ chúng ta đã bao phủ một vài option caching, lời khuyên có vẻ hơi lạ, nhưng tôi đề nghị bạn cố gắng tránh dùng bất kỳ phương thức nào trong bài này! ETags và HTTP caching tốt để biết, và Rails cho chúng ta công cụ cụ thể cho vấn đề cụ thể. Rào cản, tuy nhiên, và nó lớn, là tất cả caching này xảy ra bên ngoài ứng dụng của bạn và do đó, phần lớn ngoài kiểm soát của bạn. Nếu bạn dùng view caching hoặc low-level caching trong Rails, như được bao phủ trong các phần trước của series, và gặp vấn đề invalidation, bạn có option; bạn có thể touch model, push code cập nhật, hoặc thậm chí truy cập Rails.cache trực tiếp từ console nếu phải, nhưng không với HTTP caching. Cá nhân, tôi thích hơn phải chạy Rails.cache.clear trong production hơn đối mặt với vấn đề trang web bị hỏng cho user cho đến khi họ clear browser cache (team customer service cũng sẽ yêu bạn).

Kết Luận

Đây là cuối series về caching trong Rails; hy vọng nó hữu ích và cung cấp thông tin. Lời khuyên caching của tôi tiếp tục như sau: làm ít nhất bạn có thể, nhưng nhiều nhất bạn phải. Nếu bạn gặp vấn đề hiệu suất, bắt đầu bằng tìm phương thức bị hit thường xuyên; có thể, chúng có thể được memoized. Cần giá trị tồn tại qua request? Có thể phương thức nặng đó có thể dùng low-level caching. Hoặc, có thể không phải phương thức cụ thể; chỉ là nghiền qua tất cả nested partial, làm chậm; trong trường hợp này, có thể view level caching giúp. Rails cho chúng ta "con dao sắc bén" để nhắm từng vấn đề, và chúng ta chỉ cần biết khi nào dùng chúng.