Background Processing in Ruby on Rails (Part 1)

Khi chúng ta xây dựng một ứng dụng rails với nhiều chức năng và đa người dùng thì yêu cầu đặt ra là cần các xử lí ngoài những request-respond tương tác trực tiếp với người dùng. Ví dụ: nếu bạn cần gửi mail cho một số lượng hữu hạn người dùng để báo cho họ biết về chương trình khuyến mãi, tải khoản dùng thử… thì bạn không thể cài đặt trong Controller vì nó không phải là một request cụ thể cho một số lượng người dùng xác định. Khi đó chúng ta cần một chức năng xử lí logic tách biệt hoàn toàn so với kiểu HTTP request.

Một ví dụ khác là trong các ứng dụng và chức năng cần xử lí mất nhiều thời gian (thông thường là lớn hơn 400ms) mà không muốn người dùng phải chờ đợi như: upload file lớn, upload video có thể phải mất 10 phút, rõ ràng là không có chuyện chúng ta giữ HTTP request với user trong khoảng thời gian đó, thay vào đó là chúng ta phải chạy ngầm và thông báo cho người dùng đại loại như “tiến trình đang được xử lý…” Đó gọi là tiến trình chạy ngầm (background processing)

Hiện nay có nhiều công nghệ được sử dụng trong Ruby on Rails để giảm tải và cải thiện performace của server. Về mặt cơ bản thì đó là các tiến trình chạy ngầm, tuy nhiên mỗi công nghệ đều có một cách thức hoạt động riêng. Hôm nay tôi sẽ giới thiệu về 2 công nghệ phổ biến sau:

  • Delayed Job
  • Sidekiq

1. Delayed Job

Delayed Job là một thư viện background processing mạnh mẽ, hay thực chất là một hàng đợi ưu tiến cấu hình cao. Nó cung cấp phương pháp tiếp cận khác nhau để xử lý các hành động không đồng bộ, bao gồm:

  • Tùy chỉnh background jobs
  • Vĩnh viễn đánh dấu các background method
  • Thực thi nền các method trong runtime

Delayed Job yêu cầu một persistence store để lưu lại tất cả các hoạt động liên quan đến hàng đợi. Cùng với việc sử dụng delayed_jobgem thì cần một gem hỗ trợ nữa để chạy. Tùy chọn hỗ trợ sau:

  • Với Active Record dùng gem delayed_job_active_record
  • Với Mongoid thì dùng gem delayed_job_mongoid

1.1. Cài đặt

Thêm hai gem delayed_jobdelayed_job_active_record vào Gemfile của ứng dụng, sau đó chạy câu lệnh generator:

  $ rails generator delayed_job:active_record

Câu lệnh này tạo ra một file migration cho bảng delayed_jobs nên sẽ cần rake:db migrate để có thể sử dụng Delayed Job.

Để thay đổi thiết lập mặc định cho Delayed Job, đầu tiên tạo một file delayed_job.rb trong đường dẫn config/initializers. Trong đó chứa các thiết lập thay đổi hành vi của hàm đợi đối với tries, timeouts, maximum run times, sleep delays và các tùy chọn khác.

Delayed::Worker.destroy_failed_jobs = false
Delayed::Worker.sleep_delay = 30
Delayed::Worker.max_attempts = 5
Delayed::Worker.max_run_time = 1.hour
Delayed::Worker.max_priority = 10

1.2. Creating Jobs

Delayed Job có thể tạo backgound jobs thông qua sử dụng 3 kỹ thuật khác nhau.

Tùy chọn đầu tiên là để chuỗi method bất kỳ nào bạn muốn thực thi không đồng bộ sau khi gọi Object#delay. Kỹ thuật này tốt cho trường hợp một số tính năng thường phải thực thi trong background trong các tình huống nhất định, nhưng là có thể chấp nhận để chạy đồng bộ trong những tình huống khác.

# Execute normally
mailer.send_email(user)
# Execute asynchronously
mailer.delay.send_email(user)

Kỹ thuật thứ 2 là bắt Delayed Job thực hiện mọi cuộc gọi đến một method trong background thông qua Object.handle_asynchronously.

class Mailer
  def send_email(user)
    UserMailer.activation(user).deliver
  end
  handle_asynchronously :send_email
end

Khi sử dụng handle_asynchronously hãy chắc chắn rằng việc khai báo là sau khi định nghĩa method, kể từ đâu Delayed Job sử dụng alias_method_chain nội bộ để thiết lập hành vi.

Cuối cùng, bạn có thể tạo ra một job tùy chỉnh bằng cách tạo một đối tượng Ruby riêng biệt mà chỉ cầ respond để thực hiện. Công việc đó có thể chạy bất cứ lúc nào bằng cách bắt Delayed Job xếp hành động đó vào hàng đợi.

class EmailJob < Struct.new(:user_id)
  def perform
   user = User.find(user_id)
   UserMailer.activation(user).deliver
  end
end

 # Enqueue a job with default settings
Delayed::Job.enqueue EmailJob.new(user.id)

 # Enqueue a job with priority of 1
Delayed::Job.enqueue EmailJob.new(user.id, 1)

 # Enqueue a job with priority of 0, starting tomorrow
Delayed::Job.enqueue EmailJob.new(user.id, 1, 1.day.from_now)

1.3. Running

Để khởi động các Delayed Job worker, sử dụng lệnh delayed_job được tạo ra bởi các generator. Điều này cho phép bắt đầu một worker hoặc nhiều worker trên từng tiến trình riêng, và cũng cung cấp khả năng dừng tất cả các worker.

 # Start a single worker
RAILS_ENV=staging bin/delayed_job start

 # Start multiple workers, each in a separate process
RAILS_ENV=production bin/delayed_job -n 4 start

 # Stop all workers
RAILS_ENV=staging bin/delayed_job stop

Delayed Job thường có vòng đơi tương đương với vòng đời ứng dụng. Do đó, dung lượng bộ nhớ chúng sử dụng tăng dần tới khả năngng dùng nhiều đến swap, khiến workers hoạt động không được mượt mà. Tốt nhất là nên có một công cụ giám sát như God hoặc monit giám sát các jobs, và khởi động lại chúng khi dung lượng bộ nhớ của chúng chạm một ngưỡng giới hạn.

1.4. Kết luận

Delayed Job là một sự lựa chọn tuyệt vời nếu bạn muốn cài đặt dễ dàng, cần phải sắp xếp các job cho những ngày sau đó, hoặc muốn thêm ưu tiên cho job trong hàng đợi của bạn. Nó hoạt động tốt trong các tình huống mà tổng số lượng job thấp và các tác vụ nó thực hiện không lâu hoặc tiêu thụ một lượng bộ nhớ lớn.

Chú ý rằng nếu bạn dùng Delayed Job với backend là CSDL quan hệ và có số lượng jobs lớn thì vấn đề về hiệu năng có thể xảy ra do table locking từ framework. Vì jobs có thể có vòng đời lâu, cần lưu ý tới vấn đề sử dụng tài nguyên khi worker không trả lại bộ nhớ sau khi jobs đã hoàn thành.

Hơn nữa, khi việc xử lý/thực hiện jobs mất nhiều thời gian, các jobs có độ ưu tiên cao hơn vẫn phải đợi các jobs khác được hoàn thành trước khi nó được xử lý. Trong những trường hợp này, sử dụng backend không phải là CSDL quan hệ như MongoDB hoặc thư viện khác như Sidekiq thì tốt hơn.

2. Sidekiq

Sidekiq là một thư viện background processing đầy đủ tính năng với hỗ trợ nhiều hàng đợi có trọng số, các job theo lịch trình, và gửi email không đồng bộ Action Mailer. Giống như Resque, Sidekiq sử dụng Redis cho công cụ lưu trữ của mình, giảm thiểu các chi phí xử lý công việc.

Sidekiq hiện là thư viện background processing thực hiện và hiệu quả bộ nhớ tốt nhất trong Ruby. Đó là đa luồng, cho phép Sidekiq xử lý job song song mà không cần thiết phải chạy nhiều quy trình. Điều này cũng có nghĩa là Sidekiq có thể xử lý các job với một bộ nhớ nhỏ hơn nhiều so với các thư viện background processing khác, chẳng hạn như Delayed Job hay Resque. Theo các tài liệu chính thức, một tiến trình Sidekiq có thể xử lý một độ lớn hơn nhiều so với các đối thủ cạnh tranh: Bạn sẽ thấy cần tới 50 tiến trình resque, mỗi tiến trình 200MB để ổn định CPU, trong khi chỉ cần một tiến trình Sidekiq 300MB để ổn định CPU và thực hiện cùng số lượng công việc.

Do nó là đa luồng, tất cả các mã thực thi bời Sidekiq nên được threadsafe.

2.1 Cài đặt

Để tích hợp Sidekiq vào ứng dụng Rails cần thêm gem sidekiq trong Gemfile và chạy bundle install.

# Gemfile
gem 'sidekiq'

Theo mặc định, Sidekiq sẽ cho rằng Redis có thể được tìm thấy tại localhost:6379. Để thay đổi vị trí của server Redis được sử dụng bởi Sidekiq cần tạo ra một initializer Rails mà cấu hình redis trong cả Sidekiq.configure_serverSidekiq.configure_client.

# config/initializers/sidekiq.rb

Sidekiq.configure_server do |config|
  config.redis = {
    url: 'redis://redis.example.com:6379/10',
    namespace: 'tr4w'
}
end

Sidekiq.configure_client do |config|
  config.redis = {
    url: 'redis://redis.example.com:6379/10',
    namespace: 'tr4w'
}
end

Lưu ý việc thiết lập tùy chọn :namespace là hoàn toàn tùy chọn, nhưng rất khuyến khích nếu Sidekiq chia sẻ quyền truy cập vào cơ sở dữ liệu Redis.

2.2. Workers

Để tạo một worker trong Sidekiq, phải tạo một class trong thư mục app/worker bao gồm môđun Sidekiq::Worker và respond để thực hiện.

class EmailWorker
  include Sidekiq::Worker

  def perform(user_id)
   user = User.find(@user_id)
   UserMailer.activation(user).deliver
  end
end

Đề xếp một job vào hàng đợi trong woker, chỉ cần gọi class method perform_async với tất cả các đối số cần thiết.

EmailWorker.perform_async(1)

Hãy biết rằng tất cả các worker job đều được lưu trong database Redis dưới dạng các object JSON, có nghĩa là bạn phải đảm bảo các đối số cung cấp cho worker có thể được ghép nối tới JSON. Để rõ ràng, trong ví dụ trên, thay vì truyền vào một đối tượng của User, chúng ta đã cung cấp worker với một số nguyên. Worker sau đó sẽ chịu trách nhiệm cho việc truy vấn bản ghi User từ cơ sở dữ liệu.

Các Sidekiq worker có thể được cấu hình thông qua phương thức dạng macro sidekiq_options. Các tùy chọn sẵn có như:

  • :backtrace - Xác định có hay không lưu các error backtrace để thử payload lại, mặc định là false. các error backtrace được dùng cho mục đích hiển thị trong Sidekiq web UI. Ngoài ra, bạn có thể chỉ định số dòng để tiết kiệm (ví dụ, backtrace: 15).
  • :queue - Tên của hàng đợi cho worker, mặc định để "default"
  • :retry - Theo mặc định, một worker có thể thử lại các jobs cho đến khi nó hoàn thành. Thiết lập tùy chọn :retry để false sẽ hướng dẫn Sidekiq chạy một job chỉ một lần. Ngoài ra, bạn có thể chỉ định tối đa số lần một job được thử lại (ví dụ, retry:5).
class SomeWorker
  include Sidekiq::Worker
  sidekiq_options queue: :high_priority, retry: 5, backtrace: true

  def perform
    ...
  end
end

2.3. Scheduled Jobs

Sidekiq có khả năng lên lịch job sẽ được thực thi. Để trì hoãn một job cho một khoảng thời gian cụ thể, xếp job vào hàng đợi bằng cách gọi perform_in.

  EmailWorker.perform_in(1.hour, 1)

Một job cũng có thể được lên lịch vào một thời gian cụ thể bằng cách sử dụng phương thức xếp vào hàng đợi perform_at.

  EmailWorker.perform_at(2.days.from_now, 1)

2.4. Delayed Action Mailer

Khi Sidekiq được thêm vào trong một ứng dụng Rails, nó thêm ba phương thức để Action Mailer cho phép các emai gửi đi được thực hiện không đồng bộ.

  • delay

Việc gọi hàm delay từ một mailer sẽ dẫn đến email được thêm vào DelayedMailer worker để xử lý.

   UserMailer.delay.activation(user.id)
  • delay_for(interval)

    Sử dụng delay_for, một email có thể được dự kiến gửi tại một khoảng thời gian cụ thể.

   UserMailer.delay_for(10.minutes).status_report(user.id)
  • delay_until(timestamp)

Phương thức Action Mailer cuối cùng được thêm bởi Sidekiq là delay_until. Sidekiq sẽ đợi cho đến khi thời gian quy định để cố gắng gửi email.

  UserMailer.delay_for(1.day).status_report(user.id)

2.5. Running

Để khởi động các Sidekiq worker, chạy lệnh sidekiq từ ứng dụng Rails của bạn.

   $ bundle exec sidekiq

Điều này cho phép khởi động một tiến trình Sidekiq mà bắt đầu xử lý hàng đợi mặc định. Để sử dụng nhiều hàng đợi, bạn có thể truyền vào tên của một hàng đợi và tùy chọn trọng số cho lệnh sidekiq.

   $ bundle exec sidekiq -q default -q critical,2

Hàng đợi có trọng số 1 theo mặc định. Nếu một hàng đợi có trọng số cao hơn, nó sẽ được kiểm tra ngần ấy lần so với hàng đợi có trọng số 1. Trong ví dụ trên các hàng đợi giới hạn được kiểm tra hai lần thường xuyên như mặc định.

Concurrency

Theo mặc định, Sidekiq bắt đầu đồng thời 25 tiến trình song song. Để thiết lập rõ ràng số lượng tiến trình cho Sidekiq sử dụng, thông qua tuỳ biến -c tới lệnh sidekiq

   $ bundle exec sidekiq -c 100

sidekiq.yml

Nếu bạn thấy mình phải xác định các tùy chọn khác nhau để các lệnh sidekiq cho nhiều môi trường, bạn cấu hình Sidekiq sử dụng một file YAML.

 #config/sidekiq.yml
 ---
 :concurrency:10
 :queues:
   - [default, 1]
   - [critical, 5]
 staging:
  :concurrency: 25
 production:
  :concurrency: 100

Bây giờ, khi bắt đầu lệnh sidekiq, truyền vào qua path của file sidekiq.yml.

 $bundle exec sidekiq -e $RAILS_ENV -C config/sidekiq.yml

2.6. Error Handling

Sidekiq hỗ trợ thông báo cho các dịch vụ thông báo ngoại lệ sau nếu có lỗi xảy ra ở trong một worker trong suốt quá trình:

  • Airbrake
  • Exceptional
  • ExceptionNotifier
  • Honeybadger

Các dịch vụ khác, chẳng hạn như Sentry và New Relic, implement Sidekiq middleware của riêng chúng xử lý các báo cáo lỗi. Cài đặt thông thường bao gồm việc thêm một câu lệnh require đơn giản tới một initializer Rails.

#config/initializers/sentry.rb
require'raven/sidekiq'

2.7. Monitoring

Resque là thư viện đầu tiên có một giao diện web để giám sát hàng đợi và các jobs. Sidekid cũng vậy.

Để chạy độc lập web interface, tạo ra một tập tin config.ru và khởi động nó với bất kỳ máy chủ Rack:

 require'sidekiq' 2
 Sidekiq.configure_clientdo|config|
   config.redis = { :size => 1 }
 end

 require'sidekiq/web'
 runSidekiq::Web

Nếu bạn muốn truy cập web interface bên trong ứng dụng Rails, một cách rõ ràng gắn kết Sidekiq::Web tới một path trong file config/routes.rb.

 require'sidekiq/web' 2
 Example::Application.routes.drawdo
   mount Sidekiq::Web => '/sidekiq'
   ...
 end

2.8. Kết luận

Sidekiq nên dùng bất kỳ ứng dụng Rails có một số lượng lớn job. Nó là thư viện xử lý tiến trình ngầm nhanh và hiệu quả nhất hiện có vì nó được đa luồng.

Với một Redis backend, Sidekiq không bị các vấn đề database locking mà có thể phát sinh khi sử dụng Delayed Job và có hiệu suất tốt hơn đáng kể đối với quản lý xếp hàng trên cả Delayed Job và Resque.

Lưu ý rằng Redis chứa tất cả các dữ liệu trong bộ nhớ, vì vậy nếu bạn có một số lượng lớn job nhưng lại không có một số lượng RAM đáng kể rảnh rỗi thì có thể bạn cần phải tìm một framework khác.

Tham khảo