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

Ba cách để tránh trùng lặp các công việc Sidekiq

Rất có thể, nếu bạn đang viết mã Ruby, bạn đang sử dụngSidekiq để xử lý xử lý nền. Nếu bạn đến từ ActiveJob hoặc một số nền tảng khác, hãy chú ý theo dõi, một số mẹo được đề cập cũng có thể được áp dụng ở đó.

Những người sử dụng công việc nền (Sidekiq) cho các trường hợp khác nhau. Một số số crunchnumbers, một số công văn gửi email chào mừng đến người dùng và một số lên lịch dữ liệu. Dù trường hợp của bạn có thể là gì, cuối cùng bạn có thể gặp phải yêu cầu tránh các công việc trùng lặp. Bằng các công việc trùng lặp, tôi hình dung ra hai công việc thực hiện đúng như tên gọi. Hãy đi sâu vào vấn đề đó một chút.

Tại sao loại bỏ công việc trùng lặp?

Hãy tưởng tượng một tình huống mà công việc của bạn trông giống như sau:

class BookSalesWorker
  include Sidekiq::Worker
 
  def perform(book_id)
    crunch_some_numbers(book_id)
 
    upload_to_s3
  end
 
  ...
end

BookSalesWorker luôn làm điều tương tự - truy vấn DB cho một cuốn sách dựa trên book_id và tìm nạp dữ liệu bán hàng mới nhất để tính toán một số. Sau đó, nó tải chúng lên một dịch vụ lưu trữ. Hãy nhớ rằng mỗi khi tàu được bán trên trang web của bạn, bạn sẽ có công việc này được xếp hạng.

Bây giờ, điều gì sẽ xảy ra nếu bạn có 100 doanh số bán hàng cùng một lúc? Bạn sẽ có 100 công việc trong số này thực hiện cùng một việc. Có lẽ bạn ổn với điều đó. Bạn không quan tâm đến việc S3 ghi nhiều như vậy và hàng đợi của bạn không bị tắc nghẽn như vậy, vì vậy bạn có thể xử lý tải. Nhưng, "nó có mở rộng quy mô không?" ™ ️

Chà, chắc chắn là không. Nếu bạn bắt đầu nhận được nhiều doanh số bán sách hơn, hàng đợi của bạn sẽ nhanh chóng chồng chất với những công việc không cần thiết. Nếu bạn có 100 công việc làm tương tự cho một cuốn sách và bạn có 10 cuốn sách bán song song, bạn hiện có 1000 công việc trong hàng đợi của mình, trong thực tế, bạn chỉ có thể có 10 công việc cho mỗi cuốn sách.

Bây giờ, hãy xem qua một số tùy chọn về cách bạn có thể ngăn chặn các công việc trùng lặp do xếp chồng lên nhau hàng đợi của bạn.

1. Cách tự làm

Nếu bạn không phải là người thích phụ thuộc bên ngoài và logic phức tạp, bạn có thể bắt đầu và thêm một số giải pháp tùy chỉnh vào cơ sở mã của mình. Tôi đã tạo repo ví dụ để thử trực tiếp các ví dụ của chúng tôi. Sẽ có một liên kết trong eachapproach tới ví dụ.

1.1 Phương pháp tiếp cận một lá cờ

Bạn có thể thêm một cờ quyết định có xếp hàng chờ công việc hay không. Một cờ có thể thêm sales_enqueued_at trong bảng Sách của họ và duy trì mục đó. Ví dụ:

module BookSalesService
  def schedule_with_one_flag(book)
    # Check if the job was enqueued more than 10 minutes ago
    if book.sales_enqueued_at < 10.minutes.ago
      book.update(sales_enqueued_at: Time.current)
 
      BookSalesWorker.perform_async(book.id)
    end
  end
end

Điều đó có nghĩa là không có công việc mới nào sẽ được xếp vào hàng cho đến khi 10 phút trôi qua kể từ thời điểm công việc cuối cùng được xếp vào hàng. Sau 10 phút trôi qua, chúng tôi cập nhật sales_enqueued_at và sắp xếp một công việc mới.

Một điều khác bạn có thể làm là đặt một cờ là boolean, ví dụ:crunching_sales . Bạn đặt crunching_sales thành true trước khi công việc đầu tiên được xếp lại. Sau đó, bạn đặt nó thành false khi công việc hoàn tất. Tất cả các công việc khác cố gắng lên lịch sẽ bị từ chối cho đến khi crunching_sales là sai.

Bạn có thể thử điều này trong khoảng repo mẫu mà tôi đã tạo.

1.2 Phương pháp tiếp cận hai lá cờ

Nếu "khóa" một công việc không được xếp vào hàng trong 10 phút nghe có vẻ quá đáng sợ, nhưng bạn vẫn thấy ổn với các cờ bổ sung trong mã của mình, thì gợi ý tiếp theo có thể khiến bạn quan tâm.

Bạn có thể thêm một cờ khác vào sales_enqueued_at hiện có - sales_calculated_at Sau đó mã của chúng ta sẽ giống như sau:

module BookSalesService
  def schedule_with_two_flags(book)
    # Check if sales are being calculated right now
    if book.sales_enqueued_at <= book.sales_calculated_at
      book.update(sales_enqueued_at: Time.current)
 
      BookSalesWorker.perform_async(book.id)
    end
  end
end
 
class BookSalesWorker
  include Sidekiq::Worker
 
  def perform(book_id)
    crunch_some_numbers(book_id)
 
    upload_to_s3
 
    # New adition
    book.update(sales_calculated_at: Time.current)
  end
 
  ...
end

Để dùng thử, hãy xem hướng dẫn trong repo ví dụ.

Bây giờ chúng tôi kiểm soát một phần thời gian từ khi một công việc được xếp hàng và kết thúc. Trong khoảng thời gian đó, không có công việc nào có thể được xếp vào hàng. Trong khi công việc đang chạy, sales_enqueued_at sẽ lớn hơn sales_calculated_at . Khi hoàn thành công việc đang chạy, sales_calculated_at sẽ lớn hơn (gần đây hơn) thanthe sales_enqueued_at và một công việc mới sẽ được xếp vào hàng.

Sử dụng hai cờ có thể thú vị, vì vậy bạn có thể hiển thị số lần bán hàng gần đây nhất được cập nhật trong giao diện người dùng. Sau đó, những người dùng đọc chúng có thể có ý tưởng về dữ liệu gần đây như thế nào. Một tình huống đôi bên cùng có lợi.

Cờ Tổng kết

Có thể rất hấp dẫn để tạo ra các giải pháp như thế này trong những lúc cần thiết, nhưng đối với tôi, chúng trông hơi vụng về và chúng thêm một số chi phí. Tôi khuyên bạn nên sử dụng tính năng này nếu trường hợp sử dụng của bạn đơn giản, nhưng ngay khi nó tỏ ra phức tạp hoặc đủ lưu ý, tôi khuyên bạn nên thử các tùy chọn khác.

Một khó khăn lớn với cách tiếp cận cờ là bạn sẽ mất tất cả công việc đã cố gắng xếp hàng trong 10 phút đó. Một điểm chuyên nghiệp lớn là bạn không bị phụ thuộc và điều đó sẽ làm giảm số lượng công việc trong hàng đợi một cách nhanh chóng.

1.3 Duyệt qua Hàng đợi

Một cách tiếp cận khác mà bạn có thể thực hiện là tạo cơ chế khóa tùy chỉnh loại bỏ các công việc tương tự khỏi xếp hàng. Chúng tôi sẽ kiểm tra hàng đợi Sidekiq mà người ta quan tâm và xem liệu công việc (công nhân) đã ở đó chưa. Codewill trông giống như sau:

module BookSalesService
  def schedule_unique_across_queue(book)
    queue = Sidekiq::Queue.new('default')
 
    queue.each do |job|
      return if job.klass == BookSalesWorker.to_s &&
        job.args == [book.id]
    end
 
    BookSalesWorker.perform_async(book.id)
  end
end
 
class BookSalesWorker
  include Sidekiq::Worker
 
  def perform(book_id)
    crunch_some_numbers(book_id)
 
    upload_to_s3
  end
 
  ...
end

Trong ví dụ trên, chúng tôi đang kiểm tra xem 'default' hàng đợi có một công việc với tên lớp là BookSalesWorker . Chúng tôi cũng đang kiểm tra xem các đối số công việc có khớp với ID sách hay không. Nếu BookSalesWorker công việc với cùng một ID Sách trong hàng đợi, chúng tôi sẽ trở lại sớm và không lên lịch cho cái khác.

Lưu ý rằng một số người trong số họ có thể được lên lịch nếu bạn sắp xếp công việc quá nhanh vì hàng đợi trống. Điều chính xác đã xảy ra với tôi khi kiểm tra nó tại chỗ với:

100.times { BookSalesService.schedule_unique_across_queue(book) }

Bạn có thể dùng thử trong repo ví dụ.

Ưu điểm của cách tiếp cận này là bạn có thể duyệt qua tất cả các hàng đợi để tìm kiếm một công việc hiện có nếu bạn cần. Vấn đề là bạn vẫn có thể sao lưu các công việc nếu hàng đợi của bạn trống và bạn lên lịch cho nhiều công việc cùng một lúc. hàng đợi của bạn.

2. Nâng cấp lên Sidekiq Enterprise

Nếu bạn hoặc tổ chức của bạn có một số tiền, bạn có thể nâng cấp lên phiên bản Doanh nghiệp của Sidekiq. Nó bắt đầu từ 179 đô la mỗi tháng và nó có một tính năng tuyệt vời giúp bạn tránh bị trùng lặp công việc. Thật không may, tôi không có SidekiqEnterprise, nhưng tôi tin rằng tài liệu của họ là đủ. Bạn có thể dễ dàng có các công việc duy nhất (không trùng lặp) với mã sau:

class BookSalesWorker
  include Sidekiq::Worker
  sidekiq_options unique_for: 10.minutes
 
  def perform(book_id)
    crunch_some_numbers(book_id)
 
    upload_to_s3
  end
 
  ...
end

Và đó là nó. Bạn có cách triển khai công việc tương tự như những gì chúng tôi đã mô tả trong phần 'Phương pháp tiếp cận một lá cờ'. Công việc sẽ là duy nhất trong 10 phút, có nghĩa là không có công việc nào khác có cùng các đối số có thể được lên lịch trong khoảng thời gian đó.

Một lớp lót khá tuyệt, phải không? Chà, nếu bạn có Enterprise Sidekiq và bạn vừa tìm hiểu về tính năng này, tôi thực sự rất vui vì đã giúp được. Hầu hết chúng ta sẽ không sử dụng nó, vì vậy hãy chuyển sang giải pháp tiếp theo.

3. sidekiq-unique-Jobs To The Rescue

Vâng, tôi biết chúng ta sắp đề cập đến một viên đá quý. Và có, nó có một số tệp Lua trong đó có thể khiến một số người thất vọng. Nhưng chịu đựng với tôi, đó là một thỏa thuận thực sự ngọt ngào mà bạn đang nhận được với nó. Sidekiq-unique-jobgem đi kèm với nhiều tùy chọn khóa và cấu hình khác - có thể nhiều hơn bạn cần.

Để bắt đầu nhanh chóng, hãy đặt sidekiq-unique-jobs gem vào Gemfile của bạn, thực hiện bundle và định cấu hình công nhân của bạn như được hiển thị:

class UniqueBookSalesWorker
  include Sidekiq::Worker
 
  sidekiq_options lock: :until_executed,
                  on_conflict: :reject
 
  def perform(book_id)
    book = Book.find(book_id)
 
    logger.info "I am a Sidekiq Book Sales worker - I started"
    sleep 2
    logger.info "I am a Sidekiq Book Sales worker - I finished"
 
    book.update(sales_calculated_at: Time.current)
    book.update(crunching_sales: false)
  end
end

Có rất nhiều tùy chọn, nhưng tôi quyết định đơn giản hóa và sử dụng tùy chọn này:

sidekiq_options lock: :until_executed, on_conflict: :reject

Khóa lock: :until_executed sẽ khóa UniqueBookSalesWorker đầu tiên công việc cho đến khi nó được thực thi. Với on_conflict: :reject , chúng tôi đang nói rằng chúng tôi muốn tất cả các công việc khác cố gắng thực hiện để bị từ chối vào hàng đợi chết. Những gì được trang bị ở đây tương tự như những gì chúng tôi đã làm trong các ví dụ DIY của chúng tôi trong các chủ đề ở trên.

Một cải tiến nhỏ so với những ví dụ DIY đó là chúng tôi có một loại logof những gì đã xảy ra. Để biết nó trông như thế nào, hãy thử những cách sau:

5.times { UniqueBookSalesWorker.perform_async(Book.last.id) }

Chỉ một công việc sẽ thực thi hoàn toàn, và bốn công việc khác sẽ được chuyển đến hàng đợi thedead, nơi bạn có thể thử lại chúng. Cách tiếp cận này khác với các ví dụ của chúng tôi khi các công việc tương tự chỉ bị bỏ qua.

Có rất nhiều tùy chọn để lựa chọn khi nói đến khóa và giải quyết xung đột, tôi khuyên bạn nên tham khảo tài liệu của gem cho trường hợp sử dụng cụ thể của mình.

Thông tin chi tiết tuyệt vời

Điều tuyệt vời về đá quý này là bạn có thể xem các ổ khóa và lịch sử của những gì đã xảy ra trong hàng đợi của bạn. Tất cả những gì bạn cần làm là thêm các dòng sau vào config/routes.rb :

# config/routes.rb
require 'sidekiq_unique_jobs/web'

Rails.application.routes.draw do
  mount Sidekiq::Web, at: '/sidekiq'
end

Nó sẽ bao gồm ứng dụng khách Sidekiq ban đầu, nhưng nó cũng sẽ cung cấp cho bạn hai trang khác - một trang dành cho khóa công việc và trang còn lại dành cho bảng thay đổi. Đây là cách nó trông:

Lưu ý cách chúng tôi có hai trang mới, "Locks" và "Changelogs". Tính năng khá thú vị.

Bạn có thể thử tất cả những điều này trong dự án ví dụ nơi đá quý được cài đặt và sẵn sàng hoạt động.

Tại sao lại là Lua?

Trước hết, tôi không phải là tác giả của viên ngọc, vì vậy tôi chỉ giả định những thứ ở đây. Lần đầu tiên tôi nhìn thấy viên đá quý, tôi đã tự hỏi:tại sao lại sử dụng Lua bên trong viên ngọc Ruby? Thoạt đầu có vẻ hơi kỳ cục, nhưng Redis hỗ trợ chạy các tập lệnh Lua. Tôi đoán tác giả của viên đá quý đã nghĩ đến điều này và muốn thực hiện logic nhanh nhẹn hơn trong Lua.

Nếu bạn nhìn vào các tệp theLua trong repo của gem, chúng không phức tạp như vậy. Tất cả các tập lệnh Lua được gọi sau này từ mã Ruby trong SidekiqUniqueJobs::Script::Caller tại đây. Vui lòng xem mã nguồn, thật thú vị khi đọc và tìm ra cách hoạt động của mọi thứ.

Đá quý thay thế

Nếu bạn sử dụng ActiveJob rộng rãi, bạn có thể thử active-job-uniqueness gem ngay tại đây. Ý tưởng cũng tương tự, nhưng thay vì các tập lệnh Lua tùy chỉnh, nó sử dụng [Redlock] để khóa các vật phẩm trong Redis.

Để có một công việc độc đáo bằng cách sử dụng viên ngọc này, bạn có thể tưởng tượng một công việc như thế này:

class BookSalesJob < ActiveJob::Base
  unique :until_executed
 
  def perform
    ...
  end
end

Cú pháp ít dài dòng hơn nhưng rất giống với sidekiq-unique-jobs đá quý. Nó có thể giải quyết trường hợp của bạn nếu bạn thực sự tin tưởng vào ActiveJob .

Lời kết

Tôi hy vọng bạn đã có được một số kiến ​​thức về cách đối phó với các công việc trùng lặp trong yourapp. Tôi chắc chắn đã rất vui khi nghiên cứu và chơi với các giải pháp khác nhau. Nếu cuối cùng bạn không tìm thấy thứ bạn đang tìm kiếm, tôi hy vọng rằng một số ví dụ đã truyền cảm hứng cho bạn để tạo ra thứ gì đó của riêng mình.

Đây là dự án mẫu với tất cả các đoạn mã.

Hẹn gặp lại các bạn trong chương trình tiếp theo, chúc mừng.

Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng xuất hiện trên báo chí, hãy đăng ký bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!