Background jobs trong Ruby
Tài liệu: Background jobs in Ruby Bạn đang phát triển một ứng dụng Ruby nơi mà người dùng có thể đăng ký và submit form, hay người dùng tiếp nhận một email. Bạn sẽ gửi nó ngay lập tức? Nếu vậy, người dùng phải đợi khi mà ứng dụng kết nối đến email server và gửi email. Đó không phải là một thiết kế ...
Bạn đang phát triển một ứng dụng Ruby nơi mà người dùng có thể đăng ký và submit form, hay người dùng tiếp nhận một email. Bạn sẽ gửi nó ngay lập tức? Nếu vậy, người dùng phải đợi khi mà ứng dụng kết nối đến email server và gửi email. Đó không phải là một thiết kế UX(user experience) tốt.
Để giải quyết vấn đề này, email nên được gửi trong Background. Với cách này email sẽ được đẩy vào hàng đợi để gửi và người dùng sẽ được thấy ngay trang xác nhận gửi mail sau đó. Điều này qúa tốt.
Có một vài background hay được sử dụng, và một vài background cần một database giống như Delayed::job, Một số background khác thì lại sử dụng Redis thay thế database giống như Resque và Sidekiq.
Một số jobs nên được thực thi trong background là:
gửi một email hay một số lượng lớn Resize một ảnh Import hàng loạt dữ liệu Update một search server
Delayed::Job Đây là một background cần một database, bởi vì nó sử dụng table để quản lý các jobs. Phần "delayded" trong tên của nó đến từ cách nó đầy một job vào hàng đợi: sử dụng phương thức delay. Bởi vậy nếu chúng ta có một đối tượng để chạy chỉ việc chạy như này:
object.method!
Với Delayed::Job nó được đẩy vào hàng đợi bằng cách này:
object.delay.method!
Nó chỉ là một phương thức được đặt ở giữa, rất dễ sử dụng. Tuy nhiên, lớp này cũng có thể là một adapted bởi vì một phương thức luôn được xử lý bởi Delayed::Job asynchronously. Điều này được thực hiện bởi với helper là handle_asynchronously:
class Stats def calculate_totals(param1, param2) # long running method end handle_asynchronously :calculate_totals end stats = Stats.new stats.calculate_totals(param1, param2) # no need to call delay
Delayed::Job có thể thậm chí được gán độ ưu tiên(priorities) chạy ở một thời điểm và được đẩy vào nhiều hàng đợi. Cho đối tượng, để chạy một job trong 5 phút với priority = 2, đấy nó tới hàng đợi như sau:
object.delay(run_at: 5.minutes.from_now, priority: 2).method!
Hay sử dụng handle_synchronously, chúng ta sẽ viết lại như sau:
handle_asynchronously :calculate_totals, run_at: proc { 5.minutes.from_now }, priority: 2
Và sau đó gọi phương thức trước đó:
stats.calculate_totals(param1, param2)
Resque Resque được dựa trên Delayed::Job, nhưng nó sử dụng Redis thay cho database. Nó cũng cung cấp một ứng dụng Sinatra để quản lý các jobs bởi vậy bạn có thể nhìn thấy những gì đang chạy và các queues.
Để tạo ra một lớp tương thích với Resque để nó có thể chạy trong Background thì bạn phải cài đặt một phương thức mới là perform. Đây là phương thức sẽ được gọi bởi Resque:
class Stats def self.perform(param1, param2) calculate_totals(param1, param2) end private def calculate_totals(param1, param2) # long running method end end Resque.enqueue(Stats, param1, param2)
Phương thức self.perform có thể thực hiện mọi thứ. Nó không cần một hàm gọi giống như trong trường hợp này. Tôi có thể chuyển những phương thức chạy vào bên trong.
Để xác định schedule cho một job chạy vào một thời điểm cụ thể, Resque cần gem "resque-scheduler". Một khi được caì đặt các jobs được đẩy vào hàng đợi giống như điều này:
Resque.enqueue_in(5.minutes, Stats, param1, param2)
Resque không hỗ trợ priorities bằng số giống như Delayed::Job, nhưng nó dựa vào trình tự các queues được định nghĩa. Bởi vậy một trong số các queue đầu tiên có độ ưu tiên cao hơn. Điều này được xác định khi launch một process.
$ QUEUES=high,low rake resque:work
Resque sẽ kiểm tra queue có độ ưu tiên cao và chạy các jobs của nó. Khi queue đó rỗng, nó sẽ bắt đầu kiểm tra queue có độ ưu tiên thấp.
Do đó lớp phải định nghĩa queue để sử dụng:
class Stats @queue = :high # rest of the code end
Sidekiq Sidekiq cũng cần có Redis để làm việc, nhưng điểm khác biệt chính của nó là nó sử dụng các threads, bởi vậy một vài jobs có thể được thực thi song song sử dụng Ruby process giống nhau. Ngoài ra nó sử dụng cùng một định dạng message nên migration rất dễ dàng.
Như đã biết Sidekiq sử dụng các threads, rõ ràng đó là mục đích chính của nó để là một background xử lý các job nhanh nhất cho Ruby. Nó nhanh hơn Resque và Delayed::Job. Nói tóm lại là nó rất dễ sử dụng và là một hệ thống mà tôi thích sử dụng cho các dự án mới và ngày nay nó được khuyến khích nên dùng.
Giống như Resque, nó bao gồm một ứng dụng sinatra để quản lý các jobs, scheduled, failed, pedding để retry,...
Do đó để tương thích với Resque, các lớp được xử lý bởi Sidekiq phải cài đặt phương thức perform:
class Stats include Sidekiq::Worker def self.perform(param1, param2) calculate_totals(param1, param2) end private def calculate_totals(param1, param2) # long running method end end Stats.perform_async(param1, param2)
Tương tự để chạy job này vào một thời điểm trong tương lai ta làm như sau:
Stats.perform_in(5.minutes, param1, param2)
Nó thậm chí hỗ trợ cú pháp của Delayed::Job
object.delay.method!
Đề gán độ ưu tiên cho một job, nó sử dụng phương pháp tiếp cận tương tự Resque: trình tự nghiêm ngặt queue. Khi tạo chúng, một trong số cái đầu tiên sẽ có độ ưu tiên cao hơn, điều này được định nghĩa khi launch Sidekiq:
sidekiq -e development -i 0 -q high -q low
Bây giờ có 2 queues được biểu diễn dưới dạng một mảng:
# define the queues array = ["high", "high", "high", "low"] # choose one array.sample
Đây là cách mà queue có độ ưu tiên thấp sẽ có nhiều cơ hội được chọn độc lập với các queue có độ ưu tiên cao là rỗng hay không rỗng.
Giống như Resque, class phải định nghĩa queue sử dụng:
class Stats queue_as :high # rest of the code end
Có một số gem là unofficial mà hỗ trợ độ ưu tiên bằng số cho Delayed::job, nhưng nó không được hỗ trợ bên ngoài box bởi Sidekiq.
ActiveJob Bởi mỗi một background có cú pháp riêng của nó, Rails cung cấp ActiveJob là một chuẩn để thực hiện với các background, bởi vậy ứng dụng có thể không biết trước các background đưa vào và có thể chuyển đổi giữa các background giống như khi sử dụng ActiveRecord cho database,mà vẫn giữ nguyên cú pháp.
Bước đầu tiên để cấu hình ActiveJob với một background được chọn. Thực hiện như đang làm với chính background được chọn đó.
Các class và các đối tượng được đẩy vào hàng đợi tương tự như Sidekiq:
class Stats def perform(param1, param2) calculate_totals(param1, param2) end private def calculate_totals(param1, param2) # long running method end end Stats.perform_later(param1, param2)
Tất nhiên, nó hỗ trỡ schedule:
Stats.set(wait: 5.minutes).perform_later(param1, param2)
Cách để định nghĩa một queue cho lớp tương tự Sidekiq:
class Stats queue_as :high # rest of the code end
Nhưng nó cũng có thể thiết lập khi đang đẩy job tới queue:
Stats.set(queue: :high).perform_later(param1, param2)
Nhớ lưu ý rằng queues phải được tạo sử dụng hệ thống queue được chọn khi bắt đầu xử lý.
Kết Luận:
Background jobs là một phần cần thiết trong mọi dự án lớn. Chọn một hệ thống quản lý queue là một quyết định dù sớm hay muộn đều phải thực hiện. Mặc dù có một số lựa chọn khác ngoài các hệ thống trên mà hoạt động tốt nhưng 3 hệ thống đó vẫn được sử dụng nhiều nhất.
Tôi đề xuất sử dụng Sidekiq bởi vì nó dễ dàng để sử dụng và mang lại hiệu quả. Nó cũng nhanh hơn so với Delayed::Job và Resque. Nó có một giao diện người dùng tuyệt vời để quản lý các jobs và các queues về mọi mặt khác nhau.
Tuy nhiên nó yêu cầu Redis, bởi vậy nếu ứng dụng của bạn chạy trên ram thấp, thì ứng dụng sẽ chạy tốt với Delayed::Job và database của bạn. Nó cũng có thể là một bắt đầu tốt với việc sử dụng ActiveJob và bạn có thể luôn luôn migrate tới Sidekiq trong tương lai mà không cần thay đổi một dòng code nào.