Cache Json hiệu quả trong ứng dụng Rails API
Bài đăng này đã không được cập nhật trong 5 năm
Hiện tại với sự phát triển mạnh mẽ của các thư viện Javascript như ReactJs, VueJs, AngularJS, chúng ta có thể sử dụng các thư viện này kết hợp với Rails API để tạo ra những ứng dụng Web. Tất cả dữ liệu trả về API trả về đều là JSON. Điều này làm mình hứng thú với việc tìm cách lưu response của API vào bộ nhớ đệm để có thể trả về nhanh hơn mà không cần truy vấn vào cơ sở dữ liệu. Trong bài viết này, mình muốn chia sẻ về cách cache Json hiệu quả.
Xây dựng một ứng dụng không áp dụng kỹ thuật cache
Trong ứng dụng này, mình có 2 model Post
class Post < ApplicationRecord
has_many :comments, dependent: :destroy
end
Và Comment
class Comment < ApplicationRecord
belongs_to :post
end
Và Post
has_many Comment
Tiếp theo, chúng ta có một controller và method index, chúng ta cần lấy ra tất cả các bài đăng và nhận xét
class PostsController < ApplicationController
def index
render json: Post.includes(:comments).to_json(include: :comments)
end
end
Và tất nhiên cần thêm một route posts#index
# config/routes.rb
Rails.application.routes.draw do
get 'posts/index'
end
Đây là một điều tồi tệ khi chúng ta phải lấy ra hết mọi thứ như thế này. Giả định chúng ta đang gặp điều tồi tệ nhất và để thấy nó chạy chậm như nào.
Tiếp theo, giả sử chúng ta có rất nhiều dữ liệu. Ở đây mình sẽ sử dụng seeds.rb
để fake dữ liệu
# db/seeds.rb
post_description =
%(Simply dummy text of the printing and typesetting industry)
(1..10).to_a.each do |index|
post = Post.create(title: "Post #{index}", content: post_description)
(1..1000).to_a.each do |comment_index|
post.comments.create(content: "Comment #{comment_index}")
end
end
Như bạn có thể thấy, chúng ta có 10 bài viết và mỗi bài viết có 1000 bình luận. Dữ liệu này đủ lớn để minh họa mức độ tồi tệ khi chúng ta lấy hết dữ liệu ra và làm thế nào để cache giúp tăng tốc độ đáng kể.
Bây giờ, hay thử lấy dữ liệu ra bằng curl
time curl -s http://localhost:3000/posts/index > /dev/null
Kết quả:
curl -s http://localhost:3000/posts/index > /dev/null 0.00s user 0.00s system 0% cpu 1.364 total
Và trong Rails log, với request này, kết quả là:
Started GET "/posts/index" for 127.0.0.1 at 2017-08-13 16:35:38 +0800
Processing by PostsController#index as */*
Post Load (0.3ms) SELECT `posts`.* FROM `posts`
Comment Load (11.3ms) SELECT `comments`.* FROM `comments` WHERE `comments`.`post_id` IN ('1', '2', '3', '4', '5', '6', '7', '8', '9', '10')
Completed 200 OK in 1351ms (Views: 0.2ms | ActiveRecord: 11.6ms)
Như bạn có thể thấy, mất khoảng 1,3 giây để trả về dữ liệu. Điều này khá là tốn thời gian nếu chúng ta dành 1,3 giây cho 1 request. Có nghĩa là theo tính toán thì:
1 request / 1.3s = 0.769 requests / second (req/s)
0.769 req/s
rất chậm so với một ứng dụng thông thường. Hãy tưởng chúng ta có 50 người dùng cùng gọi đến method index của controller trên, người cuối cùng cần phải đợi
50 requests / (0.769 req/s) = 65 seconds ~= 1 minute
1 minute
chờ dữ liệu trả về là điều không phải ai cũng muốn. Chúng ta cần tăng tốc độ trả về bằng cách thêm kỹ thuật caching.
Thêm Rails cache với redis trong môi trường phát triển
Trong phạm vi bài viết này mình sẽ hướng dẫn cách thêm cache vào trong môi trường phát triển để đánh giá hiệu suất. Nó sẽ khá giống với staging hay production một khi bạn biết cách làm điều đó trên mội trường development.
Hãy bắt đầu với đoạn code caching mà Rails có sẵn trong config/development.rb
:
# config/development.rb
# ...
if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true
config.cache_store = :memory_store
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}"
}
else
config.action_controller.perform_caching = false
config.cache_store = :null_store
end
# ...
Như bạn có thể thấy, mặc định cache_store
trong rails là null_store
, có nghĩa là không có bộ nhớ cache.
Để thử cache, chúng ta chỉ cần tạo một tệp trống tmp/caching-dev.txt
để nó chạy vào điều kiện if
trong đoạn code trên.
touch tmp/caching-dev.txt
Lưu ý rằng, mặc định cache store là memory_store
. Có nghĩa là nó sẽ sử dụng Ram để lưu trữ dữ liệu. Với memory_store
chúng ta không thể mở rộng quy mô lưu trữ bộ nhớ đệm thành nhiều máy nên nếu bạn có nhiều máy chủ chạy ứng dụng, bộ nhớ đệm sẽ không chia sẻ giữa các máy chủ này. Chúng ta nên sử dụng memcache
hoặc redis_store
để các máy chủ đang chạy chia sẻ cùng mộ bộ nhớ đệm. Hãy thay đổi cache store thành redis_store
config.cache_store = :redis_store
Chúng ta cần cài thêm gem redis_rails
, hãy thêm vào gem file và bundle:
# Gemfile
# ...
gem 'redis-rails'
# ...
Khi bạn thực việc này, bạn cần khởi động lại máy chủ để cập nhật config mới. Để cache chúng ta cần thay đổi trong controller:
class PostsController < ApplicationController
def index
json = Rails.cache.fetch('posts') do
Post.includes(:comments).to_json(include: :comments)
end
render json: json
end
end
Mình sẽ giới thiệu về block:
Rails.cache.fetch('posts') do
# ...
end
Nó sẽ lưu trữ bất kỳ kết nào được tạo trong block vào bộ nhớ đệm với key là posts
. Và chúng ta cần chạy máy chủ redis :
redis-server
Bây giờ, hãy thử chạy lại tới controller:
time curl -s http://localhost:3000/posts/index > /dev/null
Và kết quả là:
curl -s http://localhost:3000/posts/index > /dev/null 0.00s user 0.00s system 0% cpu 1.432 total
Khá chậm 1,432 giây
. Tuy nhiên, đây là trạng thái khởi động và nó khá là trậm. Hãy chạy thử lại 1 lần nữa nhé. Bây giờ đã nhanh hơn rất nhiều so với lần trước.
time curl -s http://localhost:3000/posts/index > /dev/null
curl -s http://localhost:3000/posts/index > /dev/null 0.00s user 0.00s system 34% cpu 0.016 total
time curl -s http://localhost:3000/posts/index > /dev/null
curl -s http://localhost:3000/posts/index > /dev/null 0.00s user 0.00s system 35% cpu 0.013 total
Điều này khá là tuyệt vời
1 request / 0.013s = 76.9 requests / second (req/s)
Nói cách khác nó đãnh nhanh gấp 100 lần chỉ với vài dòng cốt.
Nguồn: http://jameshuynh.com/cache/json/rails/2017/08/13/how-to-effectively-cache-json-in-api-rails-app/
All rights reserved