Computer >> Máy Tính >  >> Lập trình >> Ruby

Hiệu suất Rails:Khi nào thì Caching là lựa chọn phù hợp?

Theo thuật ngữ lập trình, bộ nhớ đệm đề cập đến việc lưu trữ một giá trị (hoặc các giá trị) để truy xuất nhanh chóng trong tương lai. Thông thường, bạn sẽ làm điều này với các giá trị được tính toán chậm vì một số lý do; ví dụ:chúng yêu cầu nhấn một API bên ngoài để truy xuất hoặc chúng liên quan đến nhiều thao tác xử lý số để tạo.

Các giá trị được lưu trong bộ nhớ cache thường được lưu trữ trên một máy chủ riêng biệt, như memcached hoặc Redis. Chúng có thể được lưu trữ trên đĩa hoặc trong RAM. Trong mã, chúng ta thường 'cache' dữ liệu bên trong các biến để tránh gọi các hàm đắt tiền nhiều lần.

data = some_calculation()
a(data)
b(data)

Sự đánh đổi cho tất cả tốc độ bạn đạt được là bạn đang sử dụng dữ liệu cũ. Điều gì sẽ xảy ra nếu dữ liệu được lưu trong bộ nhớ cache trở nên 'cũ' và không còn chính xác? Bạn sẽ phải xóa bộ nhớ cache để 'làm mất hiệu lực' của nó.

Lập luận chống lại bộ nhớ đệm

Như người ta thường nói, chỉ có 2 vấn đề khó trong khoa học máy tính:1. Đặt tên cho sự vật2. Vô hiệu bộ nhớ cache3. Lỗi từng lỗi một

Tại sao việc vô hiệu hóa bộ nhớ cache lại khó đến vậy? Về bản chất, một giá trị được lưu trong bộ nhớ cache 'ẩn' một giá trị thực. Bất kỳ lúc nào giá trị 'thực' thay đổi, bạn (vâng, bạn, lập trình viên) phải nhớ 'làm mất hiệu lực' bộ nhớ cache để nó được cập nhật.

Giả sử bạn đang thêm tiện ích con 'đếm từ' vào trình soạn thảo văn bản. Bạn cần cập nhật số lượng từ khi người dùng nhập. Cách tiếp cận đơn giản nhất là đếm lại các từ trên mỗi lần gõ phím, nhưng cách này quá chậm. Có một cách tiếp cận khác:

  1. Đếm các từ khi tải tệp.
  2. Lưu số từ này vào một biến (hoặc 'lưu vào bộ nhớ cache').
  3. Hiển thị nội dung của biến ra màn hình.

Việc triển khai này nhanh hơn nhiều, nhưng 'số lượng từ' được lưu trong bộ nhớ cache không thay đổi khi chúng ta nhập. Để làm như vậy, chúng tôi cần phải 'vô hiệu hóa' bộ nhớ cache bất cứ khi nào chúng tôi mong đợi số lượng từ thay đổi.

Bây giờ, khi các lần nhấn phím được thực hiện, bạn sẽ phát hiện các từ (tức là dấu cách) và tăng bộ đếm từ. Tất nhiên, bạn cũng sẽ giảm nó khi người dùng xóa các từ. Dễ. Xong. Vé tiếp theo.

... Nhưng khoan đã, bạn có nhớ cập nhật số từ khi người dùng cắt văn bản vào khay nhớ tạm không? Còn khi họ dán văn bản? Còn khi trình kiểm tra chính tả chia lỗi đánh máy thành hai từ thì sao?

Vấn đề ở đây không phải là cập nhật giá trị, điều này khá nhỏ. Vấn đề là bạn phải nhớ cập nhật nó ở mọi nơi . Chỉ thiếu một trong các bản cập nhật này sẽ gây ra sự cố mất hiệu lực bộ nhớ cache, nghĩa là bạn sẽ hiển thị giá trị cũ cho người dùng.

Với suy nghĩ này, bạn có thể thấy rằng việc thêm vào bộ nhớ đệm mang lại sự phức tạp về kỹ thuật và các nguồn lỗi tiềm ẩn. Tất nhiên, những vấn đề này có thể được giải quyết, nhưng đó là điều cần lưu ý trước khi chuyển sang bộ nhớ đệm làm giải pháp.

Tốc độ không có bộ nhớ đệm

Nếu chúng ta không cần bàn đến bộ nhớ đệm, thì việc tăng tốc ứng dụng của chúng ta chỉ là xác định và khắc phục sự cố tắc nghẽn hiệu suất - những hệ thống chậm hơn mức có thể. Chúng ta có thể nhóm chúng thành ba loại tổng thể:

  1. Truy vấn cơ sở dữ liệu (quá nhiều hoặc quá chậm)
  2. Xem kết xuất
  3. Mã ứng dụng (ví dụ:thực hiện các phép tính nặng)

Khi làm việc về hiệu suất, có hai kỹ thuật bạn cần biết để đạt được bước tiến:lập hồ sơ và đo điểm chuẩn.

Cấu hình

Lập hồ sơ là cách bạn biết ở đâu vấn đề nằm trong ứng dụng của bạn:Trang này có chậm vì hiển thị mẫu chậm không? Hay là nó chậm vì nó đánh vào cơ sở dữ liệu hàng triệu lần?

Đối với Ruby on Rails, tôi khuyên bạn nên dùng trình biên dịch dạng rack-mini, bổ sung một tiện ích nhỏ xinh xắn vào rìa ứng dụng của bạn. Nó cung cấp cho bạn một cái nhìn tổng quan tốt về những gì cần thiết để hiển thị trang bạn đang xem, chẳng hạn như có bao nhiêu truy vấn cơ sở dữ liệu đã được thực hiện, chúng mất bao lâu và bao nhiêu phần tử đã được hiển thị.

Đối với sản xuất (thủ thuật chuyên nghiệp:rack-mini-profiler hoạt động tốt trong sản xuất; chỉ cần đảm bảo rằng nó chỉ xuất hiện cho một số người dùng nhất định, chẳng hạn như quản trị viên hoặc nhà phát triển), có các dịch vụ trực tuyến, bao gồm Skylight, New Relic và Scout, giám sát hiệu suất trang.

Mục tiêu thường được trích dẫn <= 100ms rất tốt cho việc kết xuất trang, vì bất kỳ thứ gì nhỏ hơn điều này rất khó để người dùng phát hiện trong việc sử dụng Internet trong thế giới thực. Mục tiêu của bạn sẽ khác nhau tùy thuộc vào nhiều yếu tố. Tại một thời điểm, khi làm việc trên một ứng dụng cũ với hiệu suất khủng khiếp, tôi đã đặt mục tiêu là <=1 giây, điều này không lớn nhưng tốt hơn rất nhiều so với khi tôi bắt đầu.

Đo điểm chuẩn

Sau khi chúng tôi tìm ra ở đâu vấn đề là, sau đó chúng tôi có thể sử dụng các điểm chuẩn để xem (nếu có) ảnh hưởng của việc tối ưu hóa của chúng tôi đến hiệu suất. Cá nhân tôi thích sử dụng đá quý benchmark-ips cho loại công việc này, vì nó cung cấp cho bạn một cách dễ dàng mà con người có thể đọc được để xem sự khác biệt mà mã của bạn đã tạo ra.

Là một ví dụ nhỏ, đây là một so sánh giữa nối chuỗi và nội suy chuỗi:

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

Điều này mang lại cho chúng tôi một kết quả tốt đẹp mà con người có thể đọc được và phép nội suy chậm hơn 1,5 lần so với phép nối (ít nhất là đối với các chuỗi nhỏ của chúng tôi). Vì lý do này, tôi cũng khuyên bạn nên sao chép phương pháp bạn đang cố gắng cải thiện và đặt tên mới cho nó. Sau đó, bạn có thể chạy so sánh nhanh để xem liệu bạn có đang cải thiện hiệu suất của nó khi bạn tiếp tục hay không.

Khắc phục các Vấn đề về Hiệu suất

Tại thời điểm này, chúng tôi biết những phần nào của ứng dụng của chúng tôi chậm. Chúng tôi có các điểm chuẩn để đo lường bất kỳ sự cải thiện nào khi nó xảy ra. Bây giờ, chúng tôi chỉ cần thực hiện công việc thực tế là tối ưu hóa hiệu suất. Các kỹ thuật bạn chọn sẽ tùy thuộc vào vị trí vấn đề của bạn:trong cơ sở dữ liệu, chế độ xem hoặc ứng dụng.

Hiệu suất Cơ sở dữ liệu

Đối với các vấn đề hiệu suất liên quan đến cơ sở dữ liệu, có một số điều cần xem xét. Đầu tiên, hãy tránh 'N + 1 truy vấn' đáng sợ. Những tình huống như thế này thường xảy ra khi hiển thị một bộ sưu tập trong một chế độ xem. Ví dụ:bạn có một người dùng có 10 bài đăng trên blog và bạn muốn hiển thị người dùng đó và tất cả các bài đăng của họ. Sơ đồ đầu tiên ngây thơ có thể là như thế này:

# 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 tiếp cận được hiển thị ở trên sẽ lấy người dùng (1 truy vấn) và sau đó kích hoạt một truy vấn cho từng bài đăng riêng lẻ (N =10 truy vấn), dẫn đến tổng số 11 (hoặc N + 1). May mắn thay, Rails cung cấp một giải pháp đơn giản cho vấn đề này bằng cách thêm .includes(:post) cho truy vấn ActiveRecord của bạn. Vì vậy, trong ví dụ trên, chúng ta chỉ cần thay đổi mã bộ điều khiển thành như sau:

def show
  @user = User.includes(:post).find(params[:id])
end

Bây giờ, chúng tôi sẽ tìm nạp người dùng tất cả hoặc bài đăng của anh ấy trong một truy vấn cơ sở dữ liệu.

Một điều khác cần tìm là nơi bạn có thể đẩy các phép tính vào cơ sở dữ liệu, thường nhanh hơn so với việc thực hiện cùng một thao tác trong ứng dụng của bạn. Một dạng phổ biến của điều này là các tổng hợp như sau:

total = Model.all.map(&:somefield).sum

Điều này đang lấy tất cả các bản ghi từ cơ sở dữ liệu, nhưng việc tổng hợp các giá trị thực sự xảy ra trong Ruby. Chúng tôi có thể tăng tốc điều này bằng cách để cơ sở dữ liệu thực hiện phép tính cho chúng tôi như sau:

total = Model.sum(:somefield)

Có lẽ bạn cần một cái gì đó phức tạp hơn, chẳng hạn như nhân hai cột:

total = Model.sum('columna * columnb')

Các cơ sở dữ liệu phổ biến hỗ trợ số học cơ bản như thế này và cả các tổng hợp phổ biến như tổng và trung bình, vì vậy hãy chú ý đến map(...).sum cuộc gọi trong cơ sở mã của bạn.

Xem Hiệu suất

Mặc dù tôi có thể nói rằng những tai ương về hiệu suất liên quan đến mẫu giúp ích nhiều hơn cho việc sử dụng bộ nhớ đệm như một giải pháp, nhưng vẫn có một số kết quả thấp mà bạn có thể muốn loại trừ trước tiên.

Đối với thời gian tải trang chung, bạn có thể kiểm tra xem mình có đang sử dụng các nguồn rút gọn cho bất kỳ thư viện Javascript hoặc CSS nào không (ít nhất là trên máy chủ sản xuất).

Ngoài ra, hãy chú ý đến số lượng lớn các bữa tiệc. Nếu _widget.html.erb của bạn mẫu mất 1 mili giây để xử lý, nhưng bạn có 100 tiện ích con trên trang, sau đó 100 mili giây đã biến mất. Một giải pháp là xem xét lại giao diện người dùng của bạn. Có 100 widget trên màn hình cùng một lúc thường không phải là một trải nghiệm người dùng tuyệt vời và bạn có thể muốn xem xét bằng cách sử dụng một số hình thức phân trang hoặc có lẽ là một cuộc đại tu UI / UX thậm chí còn quyết liệt hơn.

Hiệu suất Mã Ứng dụng

Nếu vấn đề về hiệu suất của bạn nằm ở chính mã ứng dụng (tức là thao tác dữ liệu) chứ không phải ở chế độ xem hoặc lớp cơ sở dữ liệu, bạn có một số tùy chọn. Một là xem liệu ít nhất một số công việc có thể được đẩy vào cơ sở dữ liệu dưới dạng các truy vấn, như được mô tả ở trên, hoặc dưới dạng các chế độ xem cơ sở dữ liệu, có lẽ, một cái gì đó giống như viên ngọc cảnh quan).

Một tùy chọn khác là chuyển 'công việc nặng nhọc' sang công việc nền, mặc dù điều này có thể yêu cầu thay đổi giao diện người dùng của bạn để xử lý thực tế là giá trị hiện sẽ được tính toán không đồng bộ.

Tôi vẫn cần bộ nhớ đệm; Bây giờ là gì?

Sau khi vượt qua tất cả những điều này, có thể bạn đã quyết định rằng có, bộ nhớ đệm là giải pháp bạn cần. Vậy bạn nên làm gì? Hãy theo dõi vì đây là bài đầu tiên trong loạt bài viết về các dạng bộ nhớ đệm khác nhau có sẵn trong Ruby on Rails.