18 ธ.ค. 2568·อ่าน 2 นาที

การกำหนดเวลางานพื้นหลังโดยไม่ต้องปวดหัวกับ cron

เรียนรู้รูปแบบการกำหนดเวลางานพื้นหลังโดยใช้ workflow และ jobs table เพื่อรันการเตือน สรุประหว่างวัน และการล้างข้อมูลอย่างเชื่อถือได้

การกำหนดเวลางานพื้นหลังโดยไม่ต้องปวดหัวกับ cron

ทำไม cron ดูเรียบง่ายจนกว่าจะไม่ใช่

Cron ดีตรงวันแรก: เขียนบรรทัด เลือกเวลา แล้วลืมมันไป สำหรับเซิร์ฟเวอร์หนึ่งเครื่องและงานหนึ่งอย่าง มันมักจะใช้ได้

ปัญหาจะปรากฏเมื่อคุณพึ่งพาการกำหนดเวลาเป็นพฤติกรรมของผลิตภัณฑ์จริงๆ: การเตือน ความสรุปประจำวัน งานล้างข้อมูล หรือการซิงก์ เรื่องราวของ “งานที่พลาด” ส่วนใหญ่ไม่ใช่ cron ล้มเหลวโดยตรง แต่เป็นสิ่งรอบ ๆ มัน: เซิร์ฟเวอร์บูตใหม่ การดีพลอยที่เขียนทับ crontab งานที่รันนานกว่าที่คาด หรือตัวนาฬิกา/ข้อแตกต่างโซนเวลา และเมื่อคุณรันหลายอินสแตนซ์ของแอป ก็อาจได้ความล้มเหลวอีกแบบ: งานซ้ำ เพราะสองเครื่องคิดว่าตัวเองต้องรันงานเดียวกัน

การทดสอบเป็นจุดอ่อนอีกจุด บรรทัด cron ไม่ได้ให้วิธีที่สะอาดสำหรับการทดสอบว่า “จะเกิดอะไรขึ้นเวลา 09:00 น. พรุ่งนี้” แบบทำซ้ำได้ ดังนั้นการกำหนดเวลามักกลายเป็นการตรวจด้วยมือ เหตุการณ์ที่น่าประหลาดใจในโปรดักชัน และการตามหาในล็อก

ก่อนเลือกวิธี ให้ชัดเจนว่าคุณกำลังกำหนดเวลาอะไร งานพื้นหลังส่วนใหญ่จะแบ่งได้เป็นกลุ่มเล็ก ๆ:

  • การเตือน (ส่งตอนเวลาที่กำหนด เพียงครั้งเดียว)
  • สรุปประจำวัน (รวบรวมข้อมูลแล้วส่ง)
  • งานล้างข้อมูล (ลบ, เก็บถาวร, หมดอายุ)
  • การซิงก์เป็นช่วง (ดึงหรือผลักการอัปเดต)

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

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

รูปแบบพื้นฐาน: scheduler, jobs table, worker

วิธีง่าย ๆ เพื่อลดปัญหา cron คือแยกความรับผิดชอบ:

  • scheduler ตัดสินใจว่าสิ่งใดควรรันและเมื่อใด
  • worker ทำงานจริง

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

jobs table กลายเป็นแหล่งความจริง แทนที่จะซ่อนสถานะไว้ในโปรเซสเซิร์ฟเวอร์หรือบรรทัด cron ทุกหน่วยงานของงานเป็นแถว: ทำอะไร ให้ใคร เมื่อใด และสิ่งที่เกิดขึ้นครั้งล่าสุด เมื่อมีปัญหาคุณสามารถตรวจสอบ มาลองใหม่ หรือยกเลิกได้โดยไม่ต้องเดา

โฟลว์ทั่วไปเป็นแบบนี้:

  • scheduler สแกนหางานที่ครบกำหนด (เช่น run_at \u003c= now และ status = queued)
  • มันอ้างสิทธิ์งานเพื่อให้มีเพียง worker เดียวรับงานนั้น
  • worker อ่านรายละเอียดงานและทำการกระทำ
  • worker บันทึกผลกลับไปที่แถวเดียวกัน

แนวคิดสำคัญคือทำให้งานสามารถกลับมาทำต่อได้ ไม่ใช่เวทมนตร์ หาก worker ล้มครึ่งทาง แถวงานก็ควรบอกว่าทำอะไรแล้วและต้องทำอะไรต่อ

ออกแบบ jobs table ให้ใช้งานได้ยาวนาน

jobs table ควรตอบสองคำถามได้เร็ว: อะไรควรรันต่อไป และครั้งที่แล้วเกิดอะไรขึ้น

เริ่มด้วยชุดฟิลด์เล็กๆ ที่ครอบคลุมตัวตน เวลา และความคืบหน้า:

  • id, type: id ที่ไม่ซ้ำกับ type สั้น ๆ เช่น send_reminder หรือ daily_summary
  • payload: JSON ที่ตรวจสอบแล้ว มีเฉพาะสิ่งที่ worker ต้องการ (เช่น user_id ไม่ใช่วัตถุผู้ใช้ทั้งหมด)
  • run_at: เวลาที่งานมีสิทธิ์จะรัน
  • status: queued, running, succeeded, failed, canceled
  • attempts: เพิ่มขึ้นทุกครั้งที่ลอง

จากนั้นเพิ่มคอลัมน์เชิงปฏิบัติการที่ช่วยให้ concurrency ปลอดภัยและจัดการเหตุการณ์ง่ายขึ้น locked_at, locked_by, และ locked_until ช่วยให้ worker เดียวอ้างสิทธิ์งานเพื่อไม่ให้รันซ้ำ last_error ควรเป็นข้อความสั้น (และอาจมีรหัสข้อผิดพลาด) ไม่ใช่ stack trace ทั้งหมดที่ทำให้แถวใหญ่เกินไป

สุดท้าย เก็บ timestamps ที่ช่วยฝ่ายสนับสนุนและรายงาน: created_at, updated_at, และ finished_at เพื่อให้ตอบคำถามเช่น “วันนี้มีการเตือนกี่รายการที่ล้มเหลว?” โดยไม่ต้องขุดผ่านล็อก

ดัชนีสำคัญเพราะระบบของคุณถามบ่อยว่า “ต่อไปคืออะไร?” สองดัชนีที่มักคุ้มค่า:

  • (status, run_at) เพื่อดึงงานครบกำหนดอย่างรวดเร็ว
  • (type, status) เพื่อดูหรือหยุดตระกูลงานหนึ่งในช่วงปัญหา

สำหรับ payload ให้ชอบ JSON ขนาดเล็ก โฟกัส และตรวจสอบก่อนใส่ ควรเก็บไอดีและพารามิเตอร์ ไม่ใช่ snapshot ของข้อมูลธุรกิจ ปฏิบัติกับโครง payload เหมือนสัญญา API เพื่อให้งานที่คิวอยู่เก่ากว่ายังคงรันได้หลังจากคุณเปลี่ยนแอป

วงจรชีวิตงาน: สถานะ การล็อก และ idempotency

ตัวรันงานจะเชื่อถือได้เมื่อทุกงานปฏิบัติตามวงจรชีวิตเล็กๆ ที่คาดเดาได้ วงจรนี้เป็นตาข่ายนิรภัยเมื่อสอง worker เริ่มพร้อมกัน เซิร์ฟเวอร์รีสตาร์ทกลางทาง หรือคุณต้องลองใหม่โดยไม่สร้างซ้ำ

เครื่องจักรสถานะง่ายๆ มักเพียงพอ:

  • queued: พร้อมรันเมื่อ run_at ถึงหรือเลย
  • running: ถูกอ้างสิทธิ์โดย worker
  • succeeded: เสร็จและไม่ควรรันอีก
  • failed: เสร็จด้วยข้อผิดพลาดและต้องการความสนใจ
  • canceled: หยุดโดยเจตนา (เช่น ผู้ใช้ยกเลิก)

อ้างสิทธิ์งานโดยไม่ทำซ้ำงาน

เพื่อป้องกันการซ้ำ การอ้างสิทธิ์งานต้องเป็นแบบอะตอม มักใช้ลักษณะล็อกพร้อม timeout (lease): worker อ้างสิทธิ์งานโดยตั้ง status=running และเขียน locked_by พร้อม locked_until หาก worker ล้ม ลีสหมดอายุและ worker อื่นสามารถอ้างสิทธิ์ใหม่ได้

กฎปฏิบัติสำหรับการอ้างสิทธิ์:

  • อ้างสิทธิ์เฉพาะงาน queued ที่ run_at \u003c= now
  • ตั้ง status, locked_by, และ locked_until ในการอัปเดตเดียวกัน
  • อ้างสิทธิ์งานที่กำลังรันอีกครั้งเมื่อ locked_until \u003c now เท่านั้น
  • ทำให้ลีสสั้นและต่ออายุถ้างานใช้เวลานาน

Idempotency (นิสัยที่ช่วยชีวิต)

Idempotency หมายความว่า: ถ้างานเดียวกันรันสองครั้ง ผลลัพธ์ยังถูกต้อง

เครื่องมือที่ง่ายที่สุดคือตัวคีย์ที่ไม่ซ้ำกัน ตัวอย่างเช่น สำหรับสรุประหว่างวัน คุณอาจบังคับหนึ่งงานต่อผู้ใช้ต่อวันด้วยคีย์เช่น summary:user123:2026-01-25 หากมีการแทรกซ้ำ มันจะชี้ไปยังงานเดียวกันแทนที่จะสร้างอันใหม่

ทำเครื่องหมายว่าสำเร็จเมื่อเอฟเฟกต์ข้างเคียงเสร็จจริง (อีเมลส่งแล้ว ระเบียนอัปเดตแล้ว) หากลองใหม่ เส้นทาง retry ต้องไม่สร้างอีเมลสองฉบับหรือเขียนซ้ำ

การ retry และการจัดการความล้มเหลวโดยไม่วุ่นวาย

จัดการความล้มเหลวโดยไม่ตื่นตระหนก
ใช้ attempts, backoff timing และ stop conditions เป็นส่วนหนึ่งของ workflow งานของคุณ
เพิ่มการ retry

การ retry คือจุดที่ระบบงานจะเชื่อถือได้หรือเปลี่ยนเป็นเสียงรบกวน เป้าหมายชัดเจน: ลองใหม่เมื่อข้อผิดพลาดน่าจะชั่วคราว หยุดเมื่อไม่ใช่

นโยบาย retry เริ่มต้นมักรวม:

  • จำนวนครั้งสูงสุด (เช่น พยายามทั้งหมด 5 ครั้ง)
  • ยุทธศาสตร์หน่วงเวลา (หน่วงคงที่หรือถดถอยแบบเอ็กซ์โพเนนเชียล)
  • เงื่อนไขหยุด (อย่า retry กับข้อผิดพลาดประเภท “ข้อมูลไม่ถูกต้อง”)
  • jitter (การกระจายสุ่มขนาดเล็กเพื่อหลีกเลี่ยงการกระโดดพร้อมกัน)

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

เมื่องานสามารถก้าวหน้าเป็นบางส่วน ให้ถือว่านั่นปกติ เก็บ checkpoint เพื่อให้การ retry สามารถต่อได้อย่างปลอดภัย บันทึกไว้ใน payload ของงาน (เช่น last_processed_id) หรือในตารางที่เกี่ยวข้อง

ตัวอย่าง: งานสรุประหว่างวันสร้างข้อความสำหรับผู้ใช้ 500 คน ถ้ามันล้มที่ผู้ใช้คนที่ 320 ให้เก็บไอดีผู้ใช้ล่าสุดที่สำเร็จแล้วและลองใหม่จาก 321 ถ้าคุณเก็บระเบียน summary_sent ต่อผู้ใช้ต่อวันอยู่แล้ว การรันซ้ำจะข้ามผู้ใช้ที่ทำเสร็จแล้วได้

การล็อกที่ช่วยแก้ปัญหาจริง

บันทึกพอที่จะดีบักในไม่กี่นาที:

  • job id, type, และหมายเลข attempt
  • อินพุตสำคัญ (user/team id, ช่วงวันที่)
  • เวลาที่ใช้ (started_at, finished_at, next run time)
  • สรุปข้อผิดพลาดสั้น ๆ (บวก stack trace ถ้ามี)
  • จำนวนผลข้างเคียง (อีเมลส่ง, แถวที่อัปเดต)

ขั้นตอนทีละขั้น: สร้างลูป scheduler ง่าย ๆ

ทำให้งานมองเห็นและควบคุมได้
จำลองสถานะ ล็อก และการ retry ในแบ็กเอนด์ที่ปรับแต่งได้โดยไม่ต้องเขียนใหม่
สร้างตอนนี้

ลูป scheduler เป็นโปรเซสขนาดเล็กที่ตื่นตามจังหวะคงที่ ค้นหางานครบกำหนด แล้วมอบให้ worker เป้าหมายคือความเชื่อถือได้ที่น่าเบื่อ ไม่ใช่ความแม่นยำสมบูรณ์ สำหรับแอปจำนวนมาก “ตื่นทุกนาที” ก็เพียงพอ

เลือกความถี่ตามความสำคัญของเวลาและโหลดฐานข้อมูล ถ้าการเตือนต้องใกล้เวลาจริง ให้รันทุก 30–60 วินาที ถ้าสรุปประจำวันเลื่อนได้บ้าง ให้รันทุก 5 นาทีถูกกว่า

ลูปง่าย ๆ:

  1. ตื่นและรับเวลาปัจจุบัน (ใช้ UTC)
  2. เลือกงานครบกำหนดที่ status = 'queued' และ run_at \u003c= now
  3. อ้างสิทธิ์งานอย่างปลอดภัยให้มี worker เพียงคนเดียว
  4. ส่งงานที่อ้างสิทธิ์ให้ worker แต่ละตัว
  5. นอนจนกว่าจะถึงติ๊กถัดไป

ขั้นตอนการอ้างสิทธิ์คือจุดที่หลายระบบพัง คุณต้องทำเครื่องหมายงานเป็น running (และเก็บ locked_by และ locked_until) ในทรานแซคชันเดียวกับการเลือก หลายฐานข้อมูลรองรับการอ่านแบบ “skip locked” เพื่อให้ scheduler หลายตัวรันโดยไม่ชนกัน

-- concept example
BEGIN;
SELECT id FROM jobs
WHERE status='queued' AND run_at \u003c= NOW()
ORDER BY run_at
LIMIT 100
FOR UPDATE SKIP LOCKED;
UPDATE jobs
SET status='running', locked_until=NOW() + INTERVAL '5 minutes'
WHERE id IN (...);
COMMIT;

เก็บขนาดแบตช์ให้เล็ก (เช่น 50–200) แบตช์ใหญ่เกินไปจะชะลอฐานข้อมูลและทำให้การชนกันอันเจ็บปวดขึ้น

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

รูปแบบสำหรับการเตือน สรุปประจำวัน และล้างข้อมูล

ทีมส่วนใหญ่ลงท้ายด้วยงานพื้นหลังสามประเภทเดียวกัน: ข้อความที่ต้องส่งตรงเวลา รายงานรันตามตาราง และงานล้างข้อมูลที่รักษาเก็บและประสิทธิภาพให้ดี ตาราง jobs และลูป worker เดียวกันสามารถจัดการทั้งหมดนี้ได้

การเตือน

สำหรับการเตือน ให้เก็บทุกอย่างที่จำเป็นในการส่งข้อความไว้ในแถวงาน: ใคร ช่องทาง (email, SMS, Telegram, in-app) เทมเพลต และเวลาส่งที่แน่นอน Worker ควรสามารถรันงานโดยไม่ต้อง “หา context เพิ่ม”

ถ้ามีการเตือนจำนวนมากครบกำหนดพร้อมกัน ให้เพิ่มการจำกัดอัตรา จำกัดข้อความต่อชั่วโมงต่อช่องทาง และให้งานส่วนเกินรอรันครั้งถัดไป

สรุปประจำวัน

สรุปประจำวันล้มเหลวเมื่อหน้าต่างเวลากำกวม เลือกเวลา cutoff ที่มั่นคง (เช่น 08:00 ตามเวลาท้องถิ่นของผู้ใช้) และกำหนดหน้าต่างอย่างชัดเจน (เช่น “เมื่อวาน 08:00 ถึงวันนี้ 08:00”) เก็บ cutoff และโซนเวลาผู้ใช้กับงานเพื่อให้การรันใหม่ให้ผลเหมือนเดิม

ให้แต่ละงานสรุปมีขนาดเล็ก หากต้องประมวลผลรายการจำนวนมาก แยกเป็นชิ้น ๆ (ตามทีม ตามบัญชี หรือช่วงไอดี) แล้วใส่ follow-up jobs

งานล้างข้อมูล

งานล้างข้อมูลปลอดภัยเมื่อแยก “ลบ” ออกจาก “เก็บถาวร” ตัดสินใจว่าสิ่งใดลบได้ถาวร (โทเค็นชั่วคราว เซสชันหมดอายุ) และอะไรควรเก็บถาวร (ล็อกตรวจสอบ ใบแจ้งหนี้) รันการล้างข้อมูลเป็นแบตช์คงที่เพื่อหลีกเลี่ยงล็อกยาวและการโหลดพุ่ง

เวลาและโซนเวลา: แหล่งบั๊กที่ซ่อนอยู่

ออกแบบงานที่กำหนดเวลาให้เชื่อถือได้
สร้างกระบวนการ Scheduler และ Worker เป็น Business Processes ที่ตรวจสอบและแก้ไขได้อย่างปลอดภัย
เริ่มสร้าง

ความล้มเหลวหลายอย่างมาจากบั๊กเวลา: การเตือนส่งผิดชั่วโมง สรุปประจำวันพลาดวันจันทร์ หรือการล้างข้อมูลรันสองครั้ง

ค่าเริ่มต้นที่ดีคือเก็บ timestamp การกำหนดเวลาเป็น UTC และเก็บโซนเวลาผู้ใช้แยกต่างหาก run_at ควรเป็นช่วงเวลาเดียวใน UTC เมื่อผู้ใช้บอกว่า “09:00 ตามเวลาฉัน” ให้แปลงเป็น UTC ตอนสร้างตาราง

Daylight saving เป็นจุดที่การตั้งค่าลวกๆ พัง “ทุกวันเวลา 09:00” ไม่เท่ากับ “ทุก 24 ชั่วโมง” ในการเปลี่ยน DST เวลา 09:00 จะแม็ปไปยัง UTC ต่างกัน และบางเวลาท้องถิ่นอาจไม่มีจริง (spring forward) หรือเกิดขึ้นสองครั้ง (fall back) วิธีที่ปลอดภัยคือคำนวณการเกิดครั้งถัดไปในเวลาท้องถิ่นทุกครั้งที่คุณรีสเค줄 แล้วแปลงเป็น UTC ใหม่

สำหรับสรุปประจำวัน ให้ตัดสินใจก่อนว่า “หนึ่งวัน” หมายถึงอะไร วันปฏิทิน (เที่ยงคืนถึงเที่ยงคืนในโซนเวลาผู้ใช้) ตรงกับความคาดหวังของมนุษย์ ช่วง 24 ชั่วโมงท้ายง่ายกว่าแต่เลื่อนและทำให้คนงงได้

ข้อมูลมาช้าคือสิ่งหลีกเลี่ยงไม่ได้: เหตุการณ์มาถึงหลัง retry บ้าง หรือโน้ตถูกเพิ่มหลังเที่ยงคืนไม่กี่นาที ตัดสินใจว่าข้อมูลช้านั้นจะเข้าข่าย “เมื่อวาน” (ด้วยช่วงเวลายกเว้น) หรือ “วันนี้” และรักษากฎนั้นให้สอดคล้อง

บัฟเฟอร์ปฏิบัติได้ช่วยป้องกันการพลาด:

  • สแกนหางานครบกำหนดย้อนหลัง 2–5 นาที
  • ทำให้งาน idempotent เพื่อให้การรันซ้ำปลอดภัย
  • บันทึกช่วงเวลาที่คลุมใน payload เพื่อให้สรุปสม่ำเสมอ

ความผิดพลาดทั่วไปที่ทำให้พลาดหรือรันซ้ำ

ความเจ็บปวดส่วนใหญ่เกิดจากสมมติฐานที่คาดเดาได้

ข้อใหญ่ที่สุดคือสมมติว่า “รันหนึ่งครั้งเท่านั้น” ในระบบจริง worker รีสตาร์ท การเรียกเครือข่ายหมดเวลา และล็อกอาจหลุด คุณมักจะได้การส่งแบบ “อย่างน้อยหนึ่งครั้ง” ซึ่งหมายความว่าการซ้ำเป็นเรื่องปกติ and โค้ดของคุณต้องทนได้

อีกข้อคือทำผลข้างเคียงก่อน (ส่งอีเมล เก็บเงิน) โดยไม่มีการตรวจ dedupe การป้องกันง่ายๆ มักแก้ได้: sent_at timestamp คีย์ไม่ซ้ำ เช่น (user_id, reminder_type, date) หรือโทเค็น dedupe เก็บไว้

การมองเห็นเป็นช่องว่างถัดมา หากคุณตอบไม่ได้ว่า “อะไรติดค้าง มาตั้งแต่เมื่อไร และทำไม” คุณจะเดา ข้อมูลขั้นต่ำที่ต้องเก็บคือ status, attempt count, next scheduled time, last error, และ worker id

ข้อผิดพลาดที่พบบ่อยที่สุด:

  • ออกแบบงานเหมือนมันจะรันเพียงครั้งเดียว แล้วแปลกใจเมื่อเกิดการซ้ำ
  • ทำผลข้างเคียงโดยไม่ตรวจ dedupe
  • รันงานใหญ่ชุดเดียวที่พยายามทำทุกอย่างแล้วหมดเวลาไปกลางทาง
  • retry ไม่มีขีดจำกัด
  • ขาดการมองเห็นคิวพื้นฐาน (ไม่มีมุมมอง backlog, failures, งานรันยาว)

ตัวอย่างที่เป็นรูปธรรม: งานสรุประหว่างวันวนผ่านผู้ใช้ 50,000 คน แล้วหมดเวลาเมื่อถึงผู้ใช้คนที่ 20,000 เมื่อ retry มันเริ่มใหม่และส่งสรุปซ้ำให้ผู้ใช้ 20,000 คนแรก เว้นแต่ว่าคุณติดตามการเสร็จแต่ละผู้ใช้หรือแยกเป็นงานต่อผู้ใช้

เช็คลิสต์ด่วนสำหรับระบบงานที่เชื่อถือได้

หลีกเลี่ยงหนี้เทคนิคในระบบงาน
รักษาความยืดหยุ่นด้วยการส่งออกรหัสหรือสร้างโค้ดใหม่เมื่อข้อกำหนดเปลี่ยนแปลง
สร้างโค้ด

ตัวรันงานถือว่า “เสร็จ” เมื่อคุณวางใจได้ตอนตี 2

ตรวจให้แน่ใจว่าคุณมี:

  • การมองเห็นคิว: จำนวน queued vs running vs failed และงาน queued ที่เก่าแก่ที่สุด
  • Idempotency เป็นค่าเริ่มต้น: คาดว่าทุกงานอาจรันสองครั้ง ใช้คีย์ไม่ซ้ำหรือเครื่องหมาย “ประมวลผลแล้ว”
  • นโยบาย retry แยกตามประเภทงาน: retry, backoff, และเงื่อนไขหยุดที่ชัดเจน
  • การเก็บเวลาอย่างสอดคล้อง: เก็บ run_at เป็น UTC; แปลงเฉพาะตอนรับเข้าและแสดงผล
  • ล็อกที่กู้คืนได้: ลีสเพื่อไม่ให้ crash ทำให้งานค้างตลอดไป

จำกัด ขนาดแบตช์ (จำนวนงานที่อ้างสิทธิ์พร้อมกัน) และ ความพร้อมทำงานพร้อมกันของ worker (จำนวนที่รันพร้อมกัน) ถ้าไม่จำกัด การพุ่งของงานอาจทำให้ฐานข้อมูลล่มหรือทำให้งานอื่นขาดแคลน

ตัวอย่างที่สมจริง: การเตือนและสรุปสำหรับทีมเล็กๆ

รันงานบนโครงสร้างพื้นฐานของคุณ
deploy scheduler และ workers ไปยังโครงสร้างพื้นฐานคลาวด์ของคุณเมื่อพร้อม
ปรับใช้แอป

เครื่องมือ SaaS ขนาดเล็กมีบัญชีลูกค้า 30 รายแต่ละบัญชีต้องการสองอย่าง: การเตือนเวลา 09:00 น. สำหรับงานที่เปิดอยู่ และสรุปเวลา 18:00 น. ของสิ่งที่เปลี่ยนแปลงวันนี้ พวกเขายังต้องการการล้างข้อมูลรายสัปดาห์เพื่อไม่ให้ฐานข้อมูลเต็มด้วยล็อกเก่าและโทเค็นหมดอายุ

พวกเขาใช้ jobs table บวก worker ที่โพลหางานครบกำหนด เมื่อมีลูกค้าใหม่ backend จะกำหนดเวลาการเตือนและการสรุปรายวันแรกตามโซนเวลาของลูกค้า

งานถูกสร้างในบางช่วงเวลาทั่วไป: ตอนสมัคร (สร้างตารางการเกิดซ้ำ), ตอนเหตุการณ์บางอย่าง (enqueue การแจ้งเตือนแบบครั้งเดียว), ตอนติ๊กตาราง (insert รอบการทำงานที่กำลังจะมาถึง), และวันบำรุงรักษา (enqueue งานล้างข้อมูล)

วันอังคารหนึ่ง ผู้ให้บริการอีเมลมีปัญหาชั่วคราวเวลา 08:59 น. worker พยายามส่งการเตือน เกิด timeout และตั้งเวลาใหม่ด้วย backoff (เช่น 2 นาที, 10 นาที, 30 นาที) เพิ่ม attempts ทุกครั้ง เนื่องจากแต่ละงานเตือนมีคีย์ idempotency เช่น account_id + date + job_type การ retry จะไม่สร้างซ้ำหากผู้ให้บริการฟื้นตัวกลางทาง

การล้างข้อมูลรันเป็นรายสัปดาห์เป็นแบตช์เล็ก ๆ ดังนั้นมันจึงไม่บล็อกงานอื่น แทนที่จะลบล้านแถวในงานเดียว มันลบสูงสุด N แถวต่อรัน แล้วตั้งเวลาเองจนเสร็จ

เมื่อมีลูกค้าร้องว่า “ฉันไม่ได้รับสรุป” ทีมตรวจสอบ jobs table สำหรับบัญชีและวันนั้น: status ของงาน จำนวน attempt ฟิลด์ล็อกปัจจุบัน และ last error จากผู้ให้บริการ นั่นเปลี่ยนจาก “มันควรส่งแล้ว” เป็น “นี่คือสิ่งที่เกิดขึ้นจริง ๆ”

ขั้นตอนถัดไป: ทำจริง สังเกต แล้วสเกล

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

เริ่มด้วยเวอร์ชันที่คุณวางใจได้:

  • สร้าง jobs table และ worker ตัวเดียวที่ประมวลผลงานประเภทเดียว
  • เพิ่มลูป scheduler ที่อ้างสิทธิ์และรันงานครบกำหนด
  • เก็บ payload ให้พอรันงานโดยไม่ต้องเดา
  • บันทึกทุก attempt และผลลัพธ์เพื่อให้คำถาม “มันรันไหม?” ตอบได้ภายใน 10 วินาที
  • เพิ่มทาง rerun แบบแมนนวลสำหรับงานที่ล้มเพื่อการกู้คืนไม่ต้อง deploy

เมื่อมันรัน ให้ทำให้สามารถสังเกตได้สำหรับมนุษย์ มุมมองแอดมินพื้นฐานก็คุ้มค่าอย่างรวดเร็ว: ค้นหางานตาม status กรองตามเวลา ตรวจ payload ยกเลิกงานติดค้าง และ rerun งาน id ที่เฉพาะเจาะจง

ถ้าคุณต้องการสร้าง flow scheduler และ worker แบบนี้ด้วยตรรกะแบ็กเอนด์แบบภาพ AppMaster (appmaster.io) สามารถโมเดล jobs table ใน PostgreSQL และนำลูป claim-process-update ไปทำเป็น Business Process ในขณะที่ยังสร้างโค้ดต้นฉบับจริงสำหรับการดีพลอย

ง่ายต่อการเริ่มต้น
สร้างบางสิ่งที่ น่าทึ่ง

ทดลองกับ AppMaster ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

เริ่ม