Sidekiq Best Practices

Don't place logic in your Worker

Đầu tiên là 10 dòng code của bạn và bạn nghĩ

Code ở đây là chuẩn rồi

6 tháng sau, sau một hồi fix bugs, bạn chợt nhận ra check thiếu điều kiện

OK, thêm mấy dòng vào đây rồi assign lại cho QA

Bạn thêm vào Worker thêm 20 dòng. Chưa đầy 10 ngày sau, team bạn thêm vào 10 dòng... Câu chuyện vẫn tiếp tục, và Worker của bạn phình quá to.

Một điểm mà bạn luôn phải thừa nhận là

Logic sẽ luôn luôn phình lên

Vậy nên, trước khi viết một dòng code logic nào, hãy luôn luôn nghĩ về nơi sẽ đặt những logic đó:

  • Interactor
  • Service Object
  • Model

Don't make your worker too big

Sikdekiq được làm ra để chạy small tasks, nó không được sinh ra để chạy những long running workers

Vậy làm cách nào để biết Worker của mình đang quá lớn?

Dấu hiện nhận biết đầu tiên là vòng lặp LOOP. Tưởng tượng bạn đang code cho Shopee, bạn có đơn đặt hàng Order, một Order sẽ sinh ra hóa đơn Invoice tương ứng, một Invoice sẽ hết hạn sau 5 ngày. Bạn và team quyết định đưa việc expire một Invoice vào trong Worker:

  class InvoiceExpirerWorker
    include Sidekiq::Worker

    def perform
      expired_invoices = Invoice.expirable

      expired_invoices.each do |invoice|
        invoice.expire!
      end
    end
  end

Nhìn đoạn code có vẻ đang đơn giản, không hề có vấn đề gì quá lớn Nhưng giả sử, trong model Invoice bạn sẽ phải xử lý cả đống việc trong việc expire một invoice:

  • Cancel order
  • Đưa hàng đã order về kho
  • gửi email cho khách hàng
  • update các record liên quan

Ví dụ một ngày bạn có 10,000 invoices bị expire, mỗi invoice cần 20 giây để hoàn thành, tính sơ sơ bạn cần mất:

10,000 * 20 = 200,000s

Vậy là Worker phải mất gần 200,000s để chạy xong.

Too long!

Vậy phải làm gì?

Lời khuyên được đưa ra ở đây là sử dụng một Master Worker, nhiệm vụ của master worker này là sinh ra các worker nhỏ hơn. Giả sử bạn có 10,000 invoices cần expire thì nó sinh ra 10,000 worker nhỏ để expire từng invoice.

  class BatchInvoiceExpirerWorker
    include Sidekiq::Worker

    def perform
      expired_invoices = Invoice.expirable

      expired_invoices.each do |invoice|
        InvoiceExpirerWorker.perform_async invoice.id
      end
    end
  end

  class InvoiceExpirerWorker
    include Sidekiq::Worker

    def perform invoice_id
      invoice = Invoice.find invoice_id
      invoice.expire!
    end
  end

Thay vì bạn có 1 worker chạy 200,000s thì bạn sẽ có 10,000 worker nhỏ hơn chạy với 20s.

Trade off

Một chú ý mà chúng ta phải quan tâm khi tách ra quá nhiều worker là có thể DB connection pool sẽ bị "cạn kiệt", nên hãy chú ý trong các trường hợp mà nó có thể xảy ra.

Bạn có thể tham khảo: https://github.com/mperham/sidekiq/issues/1047


All Rights Reserved