Tối ưu Rails app với Redis
Việc tối ưu một trang web là một công việc khá quan trọng , nó làm cho việc trải nghiệm người dùng (UX) tốt hơn khi mà chương trình của chúng ta trở nên lớn hơn về số lượng người dùng hay dữ liệu. Khi tối ưu ở server mà ta đã tối ưu câu query hết mức, loại bỏ N+1... mà ta vẫn thấy chậm, lúc đó ...
Việc tối ưu một trang web là một công việc khá quan trọng , nó làm cho việc trải nghiệm người dùng (UX) tốt hơn khi mà chương trình của chúng ta trở nên lớn hơn về số lượng người dùng hay dữ liệu. Khi tối ưu ở server mà ta đã tối ưu câu query hết mức, loại bỏ N+1... mà ta vẫn thấy chậm, lúc đó ta có thể nghĩ đến một phương án khác là sử dụng cache dữ liệu.
Trong bài viết này mình sẽ giới thiệu Redis và demo một ứng dụng Rails sử dụng Redis để cache dữ liệu.
Redis là một dự án mã nguồn mở, dự án có hơn 20k stars và hơn 7k forks trên Github (một con số ấn tượng phải không). Redis thường được coi như là data structures server, điều đó có nghĩa là nó cung cấp quyền truy cập dữ liệu thông qua một tập các câu lệnh, các request sử dụng cấu trúc server-client với giao thức TCP sockets và một giao thức đơn giản khác. Vì vậy, các tiến trình khác nhau có thể query hay modify cùng một dữ liệu dưới nhiều cách khác nhau.
Redis là một in-memory data structure store, điều này có nghĩa là Redis lưu dữ liệu ở trong bộ nhớ chính (RAM), lí giải tại sao Redis lại nhanh.
Tại sao lại chọn Redis?
Có rất nhiều hệ thống lưu trữ dữ liệu ngoài kia như Memcached, Voldemort, MongoDB, Apache Casandra... Vậy tại sao lại chọn Redis?
Việc sử dụng Redis có một số tính năng đặc biệt:
- Redis sẽ đảm bảo việc lưu dữ liệu vào đĩa, thậm chí dữ liệu được thay đổi, sửa chữa thường xuyên. Ngoài ra Redis cũng rất nhanh nhưng vẫn ổn định.
- Redis quan tâm đặc biệt vào hiệu quả bộ nhớ, vì vậy dữ liệu bên trong Redis sẽ sử dụng ít bộ nhớ hơn so với những hệ thống lưu trữ dữ liệu sử dụng ngôn ngữ lập trình bậc cao cùng loại.
- Redis cung cấp mọt số các tính năng như sự sao chép (replication), tính bền bỉ (durability), phân cụm (cluster) hay độ khả dụng cao (high availability).
Kiểu dữ liệu
Redis sử dụng dạng lưu trữ key-value, nhưng không hẳn là text thông thường, Redis hỗ trợ nhiều loại dữ liệu
|_.Loại dữ liệu|_.Mô tả| |String|Redis sử dụng Binary-safe strings| |Set|Một tập các string duy nhất và không sắp xếp| |List|Tập các string được sắp xếp theo thứ tự được chèn vào, cơ bản giống như linked lists| |Sorted set|Giống Set nhưng các phần tử được sắp xếp thông qua một giá trị được gọi là score| |Hash|Các cặp key-value, nó giống như Hash ở trong Ruby hay Python| |Bit array|Lưu trữ dữ liệu ở dạng một mảng các bit| |HyperLogLog|Được sử dụng để ước lượng các yếu tố của một tập|
Cài đặt
Lan man với lí thuyết thế là đủ, cùng download, giải nén và compile Redis với:
$ wget http://download.redis.io/releases/redis-3.2.5.tar.gz $ tar xzf redis-3.2.5.tar.gz $ cd redis-3.2.5 $ make $ cp src/redis-server src/redis-cli /usr/bin
Để khởi động Redis ta sử dụng câu lệnh:
$ redis-server
Tạo dữ liệu
Ở phần demo này mình có 2 bảng là User và Post
# app/models/post.rb class Post < ActiveRecord::Base belongs_to :user end # app/models/user.rb class User < ActiveRecord::Base has_many :posts end
Việc truy vấn với một lượng lớn dữ liệu sẽ mất khá nhiều thời gian query, sẽ mất nhiều thời gian hơn nữa khi ta response cho client. Tiếp theo ta tạo dữ liệu từ file seed.rb, ta cần cài đặt gem faker để tạo dữ liệu ảo.
gem "faker"
Ta tạo ra 10 user và mỗi user có 10000 post
# db/seed.rb 10.times do |n| user = User.create! name: Faker::Name.name, address: Faker::Address.city 10000.times do |m| Post.create! title: Faker::Lorem.sentence, content: Faker::Lorem.paragraph, user: user end end
Ở controller index ta load hết tất cả 100000 post và trả về ở dạng json.
# app/controllers/posts_controller.rb class PostsController < ApplicationController def index @posts = Post.includes(:user).all respond_to do |format| format.json { render json: @posts, status: :ok } end end end
Thử chạy chương trình xem có ổn không nào.
Ta có thể thấy là với 100000 bản ghi server mất 86ms để truy vấn dữ liệu và mất toàn bộ gần 21s để trả về được cho client dữ liệu dưới dạng json.
Khởi tạo Redis Rails
Tiếp theo ta cần cài đặt một số gem để có thể sử dụng Redis
gem "redis" gem "redis-namespace" gem "redis-rails" gem "redis-rack-cache"
Ta cần khai báo với Rails rằng là sử dụng Redis như một cache store, ở đây ta cần khai báo địa chỉ host, cổng và số thứ tự database (Redis mặc định có 16 database được đánh số thứ tự từ 0-15)
# config/application.rb config.cache_store = :redis_store, { host: "localhost", port: 6379, db: 0, }, {expires_in: 7.days}
Ta cần phải tạo ra một Redis instance để có thể gọi được ở trong ứng dụng Rails, bằng việc sử dụng redis-namespace điều này khá dễ dàng. Sau này khi cần thực hiện query Redis sẽ thông qua biến này.
# config/initializers/redis.rb $redis = Redis::Namespace.new "demo-redis", :redis => Redis.new
Giờ thì ta đã có thể sử dụng được Redis để lưu trữ dữ liệu rồi
# app/controllers/posts_controller.rb class PostsController < ApplicationController def index @posts = fetch_from_redis respond_to do |format| format.json { render json: @posts, status: :ok } end end private def fetch_from_redis posts = $redis.get "posts" if posts.nil? posts = Post.includes(:user).all.to_json $redis.set "posts", posts end JSON.load posts end end
Chạy thử và xem kết quả nào
Server không hề mất thời gian truy vấn dữ liệu thay vào đó là lấy dữ liệu từ Redis (rất nhanh) và cũng chỉ mất tổng cộng hơn 7s để trả lại dữ liệu cho client dưới dạng json, thời gian đã được giảm xuống còn 1/3 so với lúc trước.
Khi Redis bị lỗi thì server của chúng ta cũng bị lỗi
Để khắc phục điều này ta cần tạo 1 exception cho việc gọi Redis (good practice), ta có thể viết lại hàm fetch_from_redis
# app/controllers/posts_controller.rb def fetch_from_redis begin posts = $redis.get "posts" if posts.nil? posts = Post.includes(:user).all.to_json $redis.set "posts", posts end posts = JSON.load posts rescue => error puts error.inspect posts = Post.includes(:user).all end posts end
Dữ liệu trả về không còn là Active Record
Một điều cần lưu ý là khi ta load dữ liệu từ Redis thì ta cần phải chuyển dữ liệu cần lưu thành string thì mới có thể lưu vào được Redis, và khi lấy ra ta cần phải convert từ string thành hash. Vì vậy khi sử dụng dữ liệu ở view thì cần chú ý vì dữ liệu bây giờ không phải là Active Record nữa.
Việc convert sang json và dump lại thành hash có thể mất nhiều thời gian, ta có thể sử dụng yajl-ruby hay Oj
Dữ liệu khi bị sửa đổi hay xóa thì dữ liệu trong redis sẽ không còn đúng nữa
Có một vấn đề là khi ta cập nhật hay xóa dữ liệu thì khi ta lấy dữ liệu từ Redis ra sẽ không còn đúng nữa, vì vậy ta cần phải có một bước cập nhật dữ liệu Redis mỗi khi có thay đổi về dữ liệu.
Điều này giải quyết khá đơn giản là ta lại xóa dữ liệu trong Redis đi.
class Post < ActiveRecord::Base after_save :clear_cache private def clear_cache $redis.del "posts" end end
Đặt key có tính phân biệt
Giả định ở index ta chỉ lấy những posts của user hiện tại, khi đó ta sẽ gặp trường hợp là 2 user khác nhau sẽ lấy cùng một dữ liệu ở Redis vì vậy kết quả sẽ không đúng.
Để giải quyết vấn đề này cũng khá đơn giản, là ta chỉ cần đặt key khi lưu vào Redis có thể phân biệt được 2 user đó, ví dụ ta có thể đặt key là posts&user_id=1 thay vì là posts
- https://www.sitepoint.com/rails-model-caching-redis/
- Trang chủ Redis
- Redis Rails
- https://github.com/antirez/redis