- Oban.py คือ เวอร์ชันที่พอร์ต Oban ซึ่งเป็นเฟรมเวิร์กประมวลผลงานของ Elixir มาสู่ Python บนพื้นฐานของ PostgreSQL โดยสามารถแทรกและประมวลผลงานได้ด้วยฐานข้อมูลเพียงอย่างเดียว
- งานถูกสร้างและโรลแบ็กภายในทรานแซกชันของฐานข้อมูล และรองรับฟีเจอร์หลากหลาย เช่น การจัดการคิว การเก็บผลลัพธ์ และการตั้งเวลาแบบ cron
- เวอร์ชันโอเพนซอร์สมีข้อจำกัด เช่น การรัน asyncio แบบเธรดเดียว และ การแทรก/ยืนยันผลแบบทีละรายการ แต่เวอร์ชัน Pro ให้ การประมวลผลแบบขนาน เวิร์กโฟลว์ และ smart concurrency
- การทำงานภายในประกอบด้วย 5 ขั้นตอนคือ
Insert → Notify → Fetch → Execute → Ack และใช้ FOR UPDATE SKIP LOCKED ของ PostgreSQL เพื่อป้องกันการชนกันด้าน concurrency
- การเลือกผู้นำ การกู้คืนงานกำพร้า และการ retry แบบ backoff ก็ทำบนฐานข้อมูลเช่นกัน จึงทำให้ การประมวลผลแบบกระจายที่เสถียรโดยไม่ต้องมี external broker เป็นไปได้
ภาพรวมของ Oban.py
- Oban.py คือ เฟรมเวิร์กประมวลผลงานแบบอิงฐานข้อมูล ที่พอร์ต Oban ของ Elixir มาสู่ Python
- แทรกและประมวลผลงานภายในทรานแซกชันของฐานข้อมูล และหากล้มเหลวทรานแซกชันทั้งหมดจะถูกโรลแบ็ก
- มีฟีเจอร์ควบคุมหลากหลาย เช่น การจำกัดคิว การเก็บงานที่เสร็จแล้ว การคงผลลัพธ์ และการตั้งเวลาแบบ cron
- มีให้เลือก 2 เวอร์ชัน
- โอเพนซอร์ส (OSS) : รัน asyncio แบบเธรดเดียว, แทรก/ยืนยันผลทีละรายการ, กู้คืนแบบเรียบง่าย
- เวอร์ชัน Pro: การประมวลผลแบบขนานบน process pool, รองรับ workflow, relay, unique jobs และ smart concurrency
- OSS เหมาะกับโปรเจ็กต์ส่วนตัวหรือการประเมินทดลองใช้ ส่วนสภาพแวดล้อมขนาดใหญ่แนะนำเวอร์ชัน Pro
เส้นทางการประมวลผลงาน
- หลังแทรกงาน จะถูกบันทึกลงตาราง
oban_jobs ด้วย state='available' และส่งการแจ้งเตือนไปยังแต่ละโหนดผ่าน NOTIFY ของ PostgreSQL
- Stager ของแต่ละโหนดจะตรวจจับคิวดังกล่าวแล้วปลุก Producer ขึ้นมา จากนั้น Producer จะดึงงานไปประมวลผล
- ตอนเลือกงาน ใช้
FOR UPDATE SKIP LOCKED ของ SQL เพื่อให้ ประมวลผลแบบขนานได้โดยไม่รันงานซ้ำ
- แถวที่ถูกล็อกอยู่แล้วจะถูกข้าม ทำให้โปรดิวเซอร์อื่นดึงงานอื่นไปได้ทันที
- งานจะถูก dispatch เป็น async task และเมื่อเสร็จสิ้นจะจัดการ acknowledgement ผ่าน callback
- เวอร์ชัน Pro ใช้ dispatcher แบบ process pool แทน asyncio เพื่อรองรับการรันแบบขนานหลายคอร์
โปรเซสเบื้องหลัง
- การเลือกผู้นำ (Leader Election)
- ใช้
INSERT ... ON CONFLICT ของ PostgreSQL และ lease แบบอิง TTL เพื่อกำหนดผู้นำ
- โดยไม่ต้องมี consensus protocol แยกต่างหาก ผู้นำเพียงตัวเดียวจะรับหน้าที่ ล้างและกู้คืนงาน
- Lifeline (กู้คืนงานกำพร้า)
- หากงานที่กำลังรันอยู่นานเกินเวลาที่กำหนด (
rescue_after, ค่าเริ่มต้น 5 นาที) จะถูก กู้คืนกลับเป็นสถานะ available
- เวอร์ชัน Pro ตรวจสอบการมีชีวิตอยู่ของ producer แต่ OSS ตัดสินจากเวลาเพียงอย่างเดียว
- Pruner (ล้างงาน)
- ลบงานที่เสร็จสิ้น ยกเลิก หรือถูกทิ้ง ซึ่งมีอายุมากกว่า
max_age (ค่าเริ่มต้น 1 วัน)
- จำกัดช่วงการลบด้วย
LIMIT เพื่อป้องกันภาระโหลดบนฐานข้อมูล
การ retry และ backoff
- หากงานเกิด exception ขึ้น Executor จะเป็นผู้ตัดสินว่าจะ retry หรือไม่
- หากยังไม่ถึงจำนวนครั้งสูงสุด (
max_attempts) ก็จะ retry และหากเกินจะถูกทิ้ง
- backoff เริ่มต้นเป็นแบบ เพิ่มขึ้นเชิงเลขชี้กำลัง พร้อม jitter
- ลดปัญหาโหลดพุ่งจากการ retry พร้อมกันจำนวนมาก (Thundering Herd) เมื่อเกิดความล้มเหลวจำนวนมาก
- ตัวอย่าง: ครั้งที่ 1 รอประมาณ 17 วินาที, ครั้งที่ 5 รอประมาณ 47 วินาที, ครั้งที่ 10 รอประมาณ 17 นาที
- คลาส worker สามารถใช้เมธอด
backoff() เพื่อสร้าง ลอจิก backoff แบบกำหนดเอง ได้
คุณสมบัติเด่นและการประเมิน
- PostgreSQL ทำหน้าที่หลัก
- ผ่าน
FOR UPDATE SKIP LOCKED, LISTEN/NOTIFY, ON CONFLICT เพื่อจัดการทั้ง การควบคุม concurrency การส่งสัญญาณ และการเลือกผู้นำ
- สร้างชั้นการประสานงานด้วย ฐานข้อมูลเดียวโดยไม่ต้องใช้ Redis หรือ external broker
- ไม่ขนานเต็มรูปแบบแต่รองรับ concurrency
- ด้วย asyncio จึงเหมาะกับงานแบบ I/O-bound ส่วนงานแบบ CPU-bound แนะนำเวอร์ชัน Pro
- โครงสร้างโค้ดชัดเจน
- ด้วยการตั้งชื่อที่สม่ำเสมอและการแยกความรับผิดชอบที่ชัด ทำให้เป็น โค้ดเบสที่อ่านง่าย
- การแบ่งบทบาทของ OSS และ Pro ชัดเจน
- OSS สำหรับการทดลองและงานขนาดเล็ก ส่วน Pro สำหรับสภาพแวดล้อมขนาดใหญ่และต้องการประสิทธิภาพสูง
- สรุป: เป็น Python port ที่สะอาดและมีโครงสร้างดี ซึ่งสร้าง job queue ที่สมบูรณ์ได้ด้วย PostgreSQL เพียงอย่างเดียว เหมาะกับผู้ใช้ Elixir หรือผู้พัฒนาที่ต้องการระบบงานแบบไม่มีโครงสร้างพื้นฐานภายนอก
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ฉันคือคนที่สร้าง Sidekiq ขอแสดงความยินดีกับสิ่งที่ Shannon และ Parker เพิ่งเปิดตัว
เมื่อก่อนฉันก็เคยลังเลเรื่องเดียวกัน — จะโฟกัสที่ Ruby ต่อไป หรือจะขยาย Sidekiq ไปยังภาษาอื่นดี สุดท้ายก็พบว่าไม่มีทางเป็นผู้เชี่ยวชาญได้ทุกภาษา ก็เลยสร้าง Faktory แทน มันเป็นสถาปัตยกรรมที่ให้เซิร์ฟเวอร์กลางจัดการวงจรชีวิตของคิว ส่วนไคลเอนต์ของแต่ละภาษาก็เรียบง่าย เช่นมีไคลเอนต์อย่าง faktory-rs ข้อเสียคือมันไม่ได้โฟกัสที่คอมมูนิตี้ของภาษาใดภาษาหนึ่ง ทำให้ยากที่จะมีตัวอย่างที่เหมาะกับภาษานั้น
แนวทางที่โฟกัสคอมมูนิตี้เดียวอาจให้ผลลัพธ์ที่ดีกว่าก็ได้ เดี๋ยวเวลาก็จะบอกเอง
แก่นสำคัญของ Oban คือมันสามารถใส่งานและประมวลผลงานได้ด้วยฐานข้อมูลเพียงอย่างเดียว คุณสามารถเพิ่มงานส่งอีเมลไว้ในทรานแซ็กชันการสร้างผู้ใช้ แล้วถ้าล้มเหลวก็ rollback ทั้งหมดได้
หลายคนบอกว่าไม่ควรใช้ relational DB เป็น job queue แต่พวกเขามองข้ามความสำคัญของทรานแซ็กชัน บทความของ Brandur Leach ชื่อ Job Drain ก็อธิบายแนวคิดนี้ไว้ดีมาก
แต่ตอนนี้ไม่มีใครจำความลำบากนั้นได้แล้ว “รูปแบบ transactional outbox” เป็นสิ่งจำเป็น และผมชอบวิธีที่ได้ การรับประกันแบบ ACID เหมือนกับข้อมูลของผมเอง
ต่อให้ไม่รู้ภายในของ DB มากนัก แค่ลงทุนเรียนเรื่อง isolation level กับลำดับการ commit สักหนึ่งสัปดาห์ ก็อาจช่วยประหยัดเวลาการ debug ระบบกระจายได้เป็นปี
ในยุคที่มี AI process ยาว ๆ เยอะมาก ความ ทนทาน แบบนี้เป็นสิ่งจำเป็น ใน ecosystem ภาษาอื่นฟีเจอร์แบบนี้ต้องจ่ายเงินเพิ่ม แต่ใน Oban มีมาให้เลย
ทีม Oban ขึ้นชื่อใน ecosystem ของ Elixir เรื่อง วิศวกรรมที่ประณีต แต่การล็อก process pool ไว้ในเวอร์ชันโปรทำให้สับสน
ตัวอย่างเช่น แพ็กเกจ $135/เดือนมีการรันหลายโปรเซส, workflow, global limits, unique jobs, bulk operations, encrypted sources และซัพพอร์ตเฉพาะทาง
โปรเจกต์ของผม Chancy ฟรีทั้งหมด และสามารถผสม asyncio, process, thread และ sub-interpreter ได้อย่างอิสระ
ผมเลยคิดว่าการย้ายฟีเจอร์เหล่านี้ไปไว้ใน OSS แล้วเก็บเงินจาก enterprise support น่าจะดีกว่า เพราะ ecosystem ของ Python มีคู่แข่งมากกว่ามาก
โมเดลที่ขายแค่ซัพพอร์ตอย่างเดียวดูจะไม่ค่อยเวิร์ก แต่ใน Python อาจต่างออกไป
ecosystem ของ Python นี่มี ทุกอย่างเยอะมาก จริง ๆ
ถ้าเพิ่มการรองรับ Django Tasks ให้ Chancy หรือทำแพ็กเกจ
django-chancyขึ้นมาเอง น่าจะถูกนำไปใช้ได้เร็วOban เวอร์ชัน OSS รองรับแค่การรัน asyncio แบบ single-threaded ทำให้งานที่เป็น CPU-bound ไปบล็อก event loop
เพราะงั้นผมเลยรู้สึกว่ามันยังไม่น่าลอง อินเทอร์เฟซของ Celery อาจไม่ดีนัก แต่คุ้นเคยและขยายได้ไม่จำกัดทั้งแนวตั้งและแนวนอน
แต่พอรู้ว่าสามารถรัน worker node ได้หลายตัว ก็เริ่มเปลี่ยนความคิดนิดหน่อย
การแบ่งฟีเจอร์ OSS/Pro ก็พอเข้าใจได้ แต่พอเห็นว่า “เวอร์ชัน Pro ใช้ heartbeat ที่ฉลาดกว่าเพื่อติดตามว่า producer ยังมีชีวิตอยู่” ก็รู้สึกเสียดาย
การที่ ฟีเจอร์ด้านความน่าเชื่อถือ ต้องเสียเงิน ทำให้โปรเจกต์ OSS ตัดสินใจนำไปใช้ยาก
เวอร์ชันพื้นฐานควรต้องดีที่สุด แล้วค่อยเก็บเงินกับฟีเจอร์เสริม ตอนนี้เส้นแบ่งมันดูอยู่ใน ตำแหน่งที่แปลก ๆ
ประโยคที่ยกมานั้นไม่ค่อยแม่นเท่าไร — การติดตามว่า producer ยังอยู่เหมือนกัน ความต่างมีแค่ วิธีการกู้งานกำพร้า เท่านั้น
อยากให้ workflow ด้าน BI/ML/DS ใน Python ย้ายมาอยู่บน Elixir
ผมคิดว่า Elixir ที่มีความเป็น functional, fault-tolerant และ concurrent สูง เป็นฐานที่ธรรมชาติกว่าสำหรับงานแบบนี้มาก
วิดีโอนี้ กับ คู่มือ Elixir Genius เป็นแหล่งอ้างอิงที่ดี
บริษัทเราก็ใช้ Celery อยู่ แต่มันไม่ได้ดีเท่าไร Temporal ก็ดูหนักเกินไป ส่วน Oban ดูเบาและน่าสนใจ
อยากรู้ว่าคนที่เคยใช้ทั้งสองอย่างเปรียบเทียบกันอย่างไร
Temporal เหมาะกับองค์กรที่ต้องการการรับประกันของ workflow และยอมรับความซับซ้อนได้ เช่น ธนาคาร
ส่วน Oban เป็นคิวแบบอิงฐานข้อมูล ซึ่งคุณต้องเสริมความน่าเชื่อถือเอง
ผมคิดว่าถ้ามีทั้งคู่ร่วมอยู่ในระบบเดียวกันก็ดี
ตอนนี้เราใช้ทั้ง ProcessWorker แบบเรียบง่ายและ worker บน ECS ร่วมกัน
เลยสงสัยว่าเดี๋ยวนี้ Celery เสถียรน้อยลงหรือจัดการยากขึ้นหรือเปล่า
เป็นโปรเจกต์ที่น่าสนใจ แต่ก็สังเกตได้ว่าฟีเจอร์หลักบางส่วนมีเฉพาะใน Pro
โปรเจกต์ที่มาก่อนและทำ durable workflow บน Postgres แบบ OSS ได้แก่ DBOS และ Absurd
เป็นเรื่องดีที่เห็นแนวทางแบบยึดฐานข้อมูลเป็นศูนย์กลางเพิ่มขึ้น
โมเดลโอเพนซอร์สเต็มตัวและอยู่ได้ด้วยการขายซัพพอร์ตอย่างเดียวเป็น โมเดลในฝัน หวังว่าสักวันจะทำได้
สงสัยว่า Postgres จะ แรงพอ สำหรับการประมวลผลงานระดับหลายร้อยล้านรายการไหม ก่อนหน้านี้ผมย้ายไป Redis + Sidekiq แล้วเห็นประสิทธิภาพดีขึ้นมาก
เวอร์ชัน OSS บอกว่าถ้ามีงานยาว ๆ มันอาจถูกกู้คืนผิดพลาดได้แม้ producer จะยังทำงานอยู่
งั้นแปลว่าควรใช้กับงานสั้น ๆ เท่านั้นหรือ?
จังหวะการกู้คืนจะต่างกันเฉพาะในกรณีที่ไม่สามารถรอให้ปิดตัวอย่างปกติได้