Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

สามวิธีในการหลีกเลี่ยงงาน Sidekiq ที่ซ้ำกัน

เป็นไปได้ว่า หากคุณกำลังเขียนโค้ด Ruby แสดงว่าคุณกำลังใช้Sidekiq เพื่อจัดการการประมวลผลเบื้องหลัง หากคุณมาจาก ActiveJob หรือพื้นหลังอื่นๆ โปรดอดใจรอ เคล็ดลับบางส่วนที่กล่าวถึงก็สามารถนำมาใช้ที่นั่นได้เช่นกัน

ผู้คนใช้งานพื้นหลัง (Sidekiq) สำหรับกรณีต่างๆ ข้อมูลสำคัญบางประการ การส่งจดหมายต้อนรับบางส่วนไปยังผู้ใช้ และกำหนดการซิงค์ข้อมูลบางส่วน ไม่ว่ากรณีของคุณจะเป็นอย่างไร ในที่สุดคุณอาจพบกับข้อกำหนดเพื่อหลีกเลี่ยงงานซ้ำซ้อน โดยงานซ้ำซ้อน ฉันนึกภาพสองงานที่ทำสิ่งเดียวกันทุกประการ มาเจาะลึกเรื่องนั้นกันดีกว่า

เหตุใดจึงต้องยกเลิกการทำซ้ำงาน

ลองนึกภาพสถานการณ์ที่งานของคุณมีลักษณะดังนี้:

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

BookSalesWorker ทำสิ่งเดียวกันเสมอ — ค้นหาฐานข้อมูลสำหรับหนังสือตาม book_id และดึงข้อมูลการขายล่าสุดเพื่อคำนวณตัวเลขบางส่วน จากนั้นจะอัปโหลดไปยังบริการจัดเก็บข้อมูล โปรดทราบว่าทุกครั้งที่ขายหนังสือบนเว็บไซต์ คุณจะต้องจัดคิวงานนี้

แล้วถ้าคุณได้ยอดขาย 100 ครั้งในคราวเดียวล่ะ? คุณจะมีงานเหล่านี้ 100 งานที่ทำสิ่งเดียวกัน บางทีคุณอาจจะดีกับที่ คุณไม่สนใจ S3writes มากนัก และคิวของคุณก็ไม่แออัด ดังนั้นคุณจึงสามารถจัดการกับโหลดได้ แต่ "ขยายหรือไม่"™️

ไม่แน่นอน หากคุณเริ่มได้รับยอดขายเพิ่มขึ้นสำหรับหนังสือมากขึ้น คิวของคุณก็จะเต็มไปด้วยงานที่ไม่จำเป็นอย่างรวดเร็ว หากคุณมี 100 งานที่ทำสิ่งเดียวกันสำหรับหนังสือเล่มเดียว และคุณมีหนังสือ 10 เล่มขายไม่เท่ากัน ตอนนี้คุณอยู่ในคิวงาน 1000 ตำแหน่ง ซึ่งในความเป็นจริง คุณสามารถมี 10 งานสำหรับหนังสือแต่ละเล่ม

ตอนนี้ มาดูสองตัวเลือกเกี่ยวกับวิธีป้องกันไม่ให้งานซ้ำซ้อนคิวของคุณ

1. DIY วิธี

หากคุณไม่ได้ชื่นชอบการพึ่งพาภายนอกและตรรกะที่ซับซ้อน คุณสามารถดำเนินการและเพิ่มโซลูชันแบบกำหนดเองบางอย่างลงในฐานโค้ดของคุณได้ ฉันสร้างตัวอย่าง repo เพื่อลองใช้ตัวอย่างของเราโดยตรง จะมีลิงค์ไปยังตัวอย่างในแต่ละแนวทาง

1.1 วิธีธงเดียว

คุณสามารถเพิ่มหนึ่งแฟล็กที่ตัดสินใจว่าจะเข้าคิวงานหรือไม่ หนึ่งอาจเพิ่ม sales_enqueued_at ในตารางหนังสือของพวกเขาและคงไว้ซึ่งสิ่งนั้น ตัวอย่างเช่น:

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

นั่นหมายความว่าจะไม่มีการจัดคิวงานใหม่จนกว่าจะผ่านไป 10 นาทีนับจากเวลาที่งานสุดท้ายเข้าคิว หลังจากผ่านไป 10 นาที เราจะอัปเดตsales_enqueued_at และจัดคิวงานใหม่

สิ่งที่คุณทำได้อีกอย่างคือตั้งค่าสถานะหนึ่งตัวที่เป็นบูลีน เช่นcrunching_sales . คุณตั้งค่า crunching_sales เป็นจริงก่อนที่จะเข้าคิวงานแรก จากนั้นคุณตั้งค่าเป็นเท็จเมื่องานเสร็จสมบูรณ์ งานอื่นๆ ทั้งหมดที่พยายามจัดกำหนดการจะถูกปฏิเสธจนถึง crunching_sales เป็นเท็จ

คุณสามารถลองใช้แนวทางนี้ใน repoI ตัวอย่างที่สร้างขึ้น

1.2 วิธีสองธง

หากการ "ล็อก" งานจากการถูกเข้าคิวเป็นเวลา 10 นาทีฟังดูน่ากลัวเกินไป แต่คุณยังคงใช้แฟล็กเพิ่มเติมในโค้ดได้ ข้อเสนอแนะต่อไปอาจสนใจคุณ

คุณสามารถเพิ่มการตั้งค่าสถานะอื่นให้กับ sales_enqueued_at . ที่มีอยู่ — sales_calculated_at .จากนั้นโค้ดของเราจะมีลักษณะดังนี้:

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

หากต้องการทดลองใช้ โปรดดูคำแนะนำในตัวอย่างที่เก็บ

ตอนนี้เราควบคุมช่วงเวลาระหว่างเวลาที่งานเข้าคิวและเสร็จสิ้น ในช่วงเวลานั้นไม่สามารถจัดคิวงานได้ ในขณะที่งานกำลังทำงาน sales_enqueued_at จะมีขนาดใหญ่กว่า sales_calculated_at . เมื่องานเสร็จสิ้น sales_calculated_at จะใหญ่กว่า (ใหม่กว่า) กว่า sales_enqueued_at และจะมีการเข้าคิวงานใหม่

การใช้สองแฟล็กอาจมีความน่าสนใจ คุณจึงสามารถแสดงเวลาล่าสุดที่ตัวเลขยอดขายได้รับการอัปเดตใน UI จากนั้นผู้ใช้ที่อ่านข้อมูลเหล่านี้สามารถทราบได้ว่าข้อมูลล่าสุดเป็นอย่างไร สถานการณ์แบบวิน-วิน

สรุปธง

มันอาจจะน่าดึงดูดใจที่จะสร้างวิธีแก้ปัญหาเช่นนี้ในยามจำเป็น แต่สำหรับฉัน พวกเขาดูงุ่มง่ามเล็กน้อย และพวกเขาเพิ่มค่าใช้จ่ายบางอย่าง ฉันขอแนะนำให้ใช้ตัวเลือกนี้หากกรณีการใช้งานของคุณเรียบง่าย แต่ทันทีที่พิสูจน์ได้ว่าซับซ้อนหรือไม่เพียงพอ เราขอแนะนำให้คุณลองใช้ตัวเลือกอื่นๆ

ข้อเสียเปรียบอย่างมากกับวิธีการตั้งค่าสถานะคือคุณจะสูญเสียงานทั้งหมดที่พยายามเข้าคิวในช่วง 10 นาทีนั้น ข้อดีอย่างมากคือคุณไม่ได้ทำให้เกิดการพึ่งพา และจะช่วยลดจำนวนงานใน Quespretty ได้อย่างรวดเร็ว

1.3 ลัดเลาะตามคิว

อีกวิธีหนึ่งที่คุณสามารถทำได้คือการสร้างกลไกการล็อคแบบกำหนดเองเพื่อป้องกันไม่ให้งานเดียวกันเข้าคิว เราจะตรวจสอบคิว Sidekiq ที่คุณสนใจและดูว่ามีงาน (คนงาน) แล้วหรือยัง รหัสจะมีลักษณะดังนี้:

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

ในตัวอย่างข้างต้น เรากำลังตรวจสอบว่า'default' คิวมีงานชื่อคลาส BookSalesWorker . เรากำลังตรวจสอบว่าอาร์กิวเมนต์งานตรงกับรหัสหนังสือหรือไม่ ถ้า BookSalesWorker งานที่มีรหัสหนังสือเดียวกันอยู่ในคิว เราจะคืนก่อนกำหนดและไม่กำหนดเวลาอื่น

โปรดทราบว่าบางงานอาจได้รับการกำหนดเวลาหากคุณกำหนดเวลางานเร็วเกินไปเนื่องจากคิวว่าง สิ่งที่แน่นอนเกิดขึ้นกับฉันเมื่อทำการทดสอบในพื้นที่ด้วย:

100.times { BookSalesService.schedule_unique_across_queue(book) }

คุณสามารถลองใช้ได้ในตัวอย่าง repo

ข้อดีของวิธีนี้คือคุณสามารถข้ามคิวทั้งหมดเพื่อค้นหางานที่มีอยู่ได้หากต้องการ ข้อเสียคือ คุณยังสามารถมีงานที่ซ้ำกันได้หากคิวของคุณว่างเปล่าและคุณกำหนดเวลางานจำนวนมากในคราวเดียว นอกจากนี้ คุณยังอาจสำรวจผ่านงานทั้งหมดในคิวก่อนที่จะจัดกำหนดการงาน ซึ่งอาจมีค่าใช้จ่ายสูงขึ้นอยู่กับขนาดของ คิวของคุณ

2. กำลังอัปเกรดเป็น Sidekiq Enterprise

หากคุณหรือองค์กรของคุณมีเงินอยู่บ้าง คุณสามารถอัปเกรดเป็น Sidekiq เวอร์ชัน Enterprise ได้ เริ่มต้นที่ $179 ต่อเดือน และมีคุณสมบัติเจ๋งๆ ที่ช่วยให้คุณหลีกเลี่ยงงานซ้ำซ้อนได้ ขออภัย ฉันไม่มี SidekiqEnterprise แต่ฉันเชื่อว่าเอกสารประกอบนั้นเพียงพอ คุณสามารถมีงานที่ไม่ซ้ำ (ไม่ซ้ำกัน) ได้อย่างง่ายดายด้วยรหัสต่อไปนี้:

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

และนั่นแหล่ะ คุณมีการใช้งานที่คล้ายกับที่เราอธิบายไว้ในส่วน 'แนวทางเดียว' งานจะไม่ซ้ำกันเป็นเวลา 10 นาที หมายความว่างานอื่นที่มีข้อโต้แย้งเดียวกันไม่สามารถกำหนดเวลาได้ในช่วงเวลานั้น

หนึ่งซับสวยดีฮะ? ถ้าคุณมี Enterprise Sidekiq และเพิ่งทราบเกี่ยวกับคุณลักษณะนี้ ฉันดีใจจริงๆ ที่ได้ช่วย พวกเราส่วนใหญ่จะไม่ใช้มัน ดังนั้นเรามาเข้าสู่แนวทางแก้ไขปัญหาถัดไปกัน

3. sidekiq-unique-jobs To The Rescue

ใช่ ฉันรู้ว่าเรากำลังจะพูดถึงอัญมณี และใช่ มันมีไฟล์ Lua อยู่ด้วยซึ่งอาจทำให้บางคนหยุดทำงาน แต่อดทนกับฉัน มันเป็นข้อตกลงที่น่ารักจริงๆ ที่คุณได้รับกับมัน sidekiq-unique-jobgem มาพร้อมกับตัวเลือกการล็อคและการกำหนดค่าอื่นๆ มากมาย — อาจมากกว่าที่คุณต้องการ

หากต้องการเริ่มต้นอย่างรวดเร็ว ให้ใส่ sidekiq-unique-jobs อัญมณีลงใน Gemfile ของคุณ ทำbundle และกำหนดค่าคนงานของคุณตามที่แสดง:

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

มีตัวเลือกมากมาย แต่ฉันตัดสินใจที่จะลดความซับซ้อนและใช้ตัวเลือกนี้:

sidekiq_options lock: :until_executed, on_conflict: :reject

lock: :until_executed จะล็อก UniqueBookSalesWorker ตัวแรก jobunt จนกว่ามันจะดำเนินการ ด้วย on_conflict: :reject เรากำลังบอกว่าเราต้องการงานอื่นๆ ทั้งหมดที่พยายามดำเนินการให้ถูกปฏิเสธในคิวที่มีปัญหา สิ่งที่ทำให้ที่นี่มีความคล้ายคลึงกับสิ่งที่เราทำในตัวอย่าง DIY ในหัวข้อด้านบน

การปรับปรุงเล็กน้อยจากตัวอย่าง DIY เหล่านั้นคือการที่เราออกจากระบบว่าเกิดอะไรขึ้น เพื่อให้เข้าใจถึงรูปลักษณ์ ให้ลองทำดังนี้:

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

มีเพียงหนึ่งงานเท่านั้นที่จะดำเนินการอย่างสมบูรณ์ และอีกสี่งานจะถูกส่งไปที่คิวที่ไม่ทำงาน ซึ่งคุณสามารถลองใหม่ได้ วิธีการนี้แตกต่างจากตัวอย่างของเราที่งานที่ซ้ำกันถูกละเลย

มีตัวเลือกมากมายให้เลือกเมื่อต้องการแก้ไขการล็อกและข้อขัดแย้ง เราขอแนะนำให้คุณศึกษาเอกสารประกอบของ gem สำหรับกรณีการใช้งานเฉพาะของคุณ

ข้อมูลเชิงลึกที่ยอดเยี่ยม

สิ่งที่ยอดเยี่ยมเกี่ยวกับอัญมณีนี้คือ คุณสามารถดูล็อคและประวัติของสิ่งที่ค้างอยู่ในคิวของคุณ สิ่งที่คุณต้องทำคือเพิ่มบรรทัดต่อไปนี้ใน config/routes.rb ของคุณ :

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

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

มันจะรวมไคลเอนต์ Sidekiq ดั้งเดิมไว้ด้วย แต่จะให้หน้าเพิ่มเติมสองหน้าแก่คุณ หน้าหนึ่งสำหรับการล็อคงานและอีกหน้าสำหรับบันทึกการเปลี่ยนแปลง นี่คือลักษณะ:

ขอให้สังเกตว่าเรามีหน้าใหม่สองหน้า "ล็อก" และ "บันทึกการเปลี่ยนแปลง" ฟีเจอร์เด็ดมาก

คุณสามารถลองทั้งหมดนี้ได้ในตัวอย่างโครงการที่มีการติดตั้งอัญมณีและพร้อมใช้งาน

ทำไมต้องลัวะ

ก่อนอื่น ฉันไม่ใช่ผู้เขียนอัญมณี ดังนั้นฉันจึงสมมติว่าที่นี่ ครั้งแรกที่ฉันเห็นอัญมณีฉันสงสัยว่าทำไมต้องใช้ Lua ใน Ruby gem? อาจดูแปลกในตอนแรก แต่ Redis รองรับการรันสคริปต์ Lua ฉันเดาว่าผู้เขียนอัญมณีมีความคิดนี้และต้องการใช้ตรรกะที่คล่องแคล่วมากขึ้นใน Lua

หากคุณดูไฟล์ theLua ใน repo ของ gem ไฟล์เหล่านั้นไม่ได้ซับซ้อนขนาดนั้น สคริปต์ Lua ทั้งหมดถูกเรียกในภายหลังจากรหัส Ruby ใน SidekiqUniqueJobs::Script::Caller ที่นี่โปรดดูที่ซอร์สโค้ด เป็นที่น่าสนใจที่จะอ่านและทำความเข้าใจว่าสิ่งต่างๆ ทำงานอย่างไร

อัญมณีทางเลือก

หากคุณใช้ ActiveJob อย่างกว้างขวาง คุณสามารถลองใช้ active-job-uniqueness อัญมณีที่นี่ แนวคิดคล้ายกัน แต่แทนที่จะใช้สคริปต์ Lua แบบกำหนดเอง มันใช้ [Redlock] เพื่อล็อกรายการใน Redis

หากต้องการมีงานที่ไม่เหมือนใครโดยใช้อัญมณีนี้ คุณสามารถจินตนาการถึงงานแบบนี้:

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

ไวยากรณ์นั้นละเอียดน้อยกว่า แต่คล้ายกับ sidekiq-unique-jobs มาก อัญมณี. อาจแก้ปัญหากรณีของคุณได้หากคุณพึ่งพา ActiveJob .

ความคิดสุดท้าย

ฉันหวังว่าคุณจะได้รับความรู้เกี่ยวกับวิธีการจัดการกับงานที่ซ้ำกันในแอปของคุณ ฉันสนุกกับการค้นคว้าและลองใช้วิธีแก้ปัญหาต่างๆ อย่างสนุกสนาน หากคุณไม่พบสิ่งที่ต้องการ ฉันหวังว่าบางตัวอย่างจะเป็นแรงบันดาลใจให้คุณสร้างสรรค์สิ่งที่คุณต้องการ

นี่คือตัวอย่างโปรเจ็กต์ที่มีข้อมูลโค้ดทั้งหมด

แล้วเจอกันใหม่ตอนหน้า ไชโย

ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!