การเลือกเทคโนโลยีคิวด้วย Postgres
(adriano.fyi)- ก่อนจะนำระบบคิวแยกต่างหากเข้ามาใช้ หากสามารถสร้าง คิวงานเบื้องหลัง ด้วย Postgres ที่ใช้อยู่แล้วได้ ก็จะใช้ประโยชน์จากความเรียบง่ายในการดูแลระบบและความคุ้นเคยของทีมได้อย่างมาก
NOTIFY/LISTENและFOR UPDATE SKIP LOCKEDที่มีมาตั้งแต่ Postgres 9.5 ช่วยแก้ทั้งการแจ้งเตือนงานใหม่และ การป้องกันไม่ให้คนงานหลายตัวประมวลผลงานซ้ำกัน- Redis, Kafka, RabbitMQ และ Amazon SQS เป็นตัวเลือกที่ทรงพลัง แต่การเพิ่ม dependency ใหม่จะเพิ่มทั้ง รูปแบบความล้มเหลวและต้นทุนการเรียนรู้ ให้กับสภาพแวดล้อมการพัฒนา การทดสอบ และการปฏิบัติการ
- คิวบน Postgres ก็ไม่ใช่คำตอบสำหรับทุกอย่าง เพราะการ push/pop อาจช้ากว่า Redis และอาจมี ต้นทุนด้านปฏิบัติการ เพิ่มขึ้น เช่น ต้องมีฐานข้อมูลหรือเซิร์ฟเวอร์คิวแยกต่างหาก และต้อง
VACUUMบ่อยขึ้น - ควรออกแบบโค้ดงานเบื้องหลังให้ abstraction แยกจากคิวแบ็กเอนด์ และพิจารณาเทคโนโลยีคิวอื่นก็ต่อเมื่อเทคโนโลยีที่คุ้นเคยไม่สามารถตอบโจทย์ได้เท่านั้น
คิวบน Postgres ทำงานอย่างไร
- เทคโนโลยีคิวบน Postgres ประกอบด้วย 2 องค์ประกอบ
- pub/sub สำหรับแจ้งและรับงานใหม่
- การล็อกแถว เพื่อไม่ให้คนงานหลายตัวประมวลผลงานเดียวกัน
- ทั้งสองความสามารถมีมาให้เป็นพื้นฐานตั้งแต่ Postgres 9.5 ที่ออกในปี 2016
- หากใช้
NOTIFYและLISTENร่วมกัน ก็สามารถเพิ่ม pub/sub ให้กับแอปพลิเคชันได้ FOR UPDATE SKIP LOCKEDจะทำการล็อกแถวกับเรคอร์ดที่ตรงเงื่อนไข และข้ามเรคอร์ดที่ถูกล็อกอยู่แล้ว- เมื่อนำไปใช้กับเรคอร์ดงาน ก็สามารถสร้างคิวรีสำหรับประมวลผลคิวอย่าง
SELECT * FROM jobs ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1
- เมื่อนำไปใช้กับเรคอร์ดงาน ก็สามารถสร้างคิวรีสำหรับประมวลผลคิวอย่าง
SKIP LOCKEDให้มุมมองข้อมูลที่ “ไม่สอดคล้องกัน” แต่ในคิวคุณสมบัตินี้กลับมีประโยชน์- งานที่กำลังถูกประมวลผลอยู่จะมองไม่เห็นสำหรับคนงานตัวอื่นเพราะมีการล็อกแถว
- พฤติกรรมนี้ทำให้เกิด distributed mutual exclusion ได้
- หากใช้
NOTIFYเพื่อแจ้งคนงานที่กำลังLISTENอยู่ว่ามีงานใหม่ ก็สามารถสร้างได้ทั้งการประมวลผลแบบแบตช์ตามรอบเวลาและการประมวลผลแบบเรียลไทม์
เส้นทางที่ทำให้ Redis กลายเป็นค่าเริ่มต้นของงานเบื้องหลัง
- วิธีจัดการงานที่ใช้เวลารันนานเป็นการเลือกเทคโนโลยีที่มี trade-off หลายด้าน
- ตัวเลือกคิวและ message broker ที่ใช้กันแพร่หลายมีดังนี้
- Redis: ที่เก็บข้อมูลในหน่วยความจำและเป็นแบ็กเอนด์ของไลบรารีงานเบื้องหลังหลายตัว
- Apache Kafka: แพลตฟอร์ม distributed event streaming ที่ Apache Foundation ดูแล
- RabbitMQ: message broker ที่ถูกใช้งานอย่างแพร่หลาย
- Amazon SQS: SaaS ของ Amazon ที่ให้บริการคิวที่ขยายขนาดได้สูง
- ใน background jobs topic ของ GitHub ไลบรารียอดนิยม 5 อันดับแรกเป็น Redis-based ทั้งหมด
- Redis เก็บข้อมูลไว้ในหน่วยความจำ จึงทำการแทรกและค้นหาได้รวดเร็ว และมีทั้ง pub/sub API กับโครงสร้างข้อมูล
list,setที่เหมาะกับการทำคิว - ความสามารถในการขยายขนาด ของ Redis ทำให้มันกลายเป็นตัวเลือกตั้งต้นสำหรับนักพัฒนาจำนวนมาก และค่าเริ่มต้นนั้นมีอิทธิพลอย่างมากในการเลือกเทคโนโลยี
เกณฑ์ที่ควรมองก่อนเรื่องการขยายขนาด
- ความหมกมุ่นกับ “scale” ในอุตสาหกรรมเทคโนโลยีอาจผลักเรื่องความเรียบง่าย ความง่ายในการบำรุงรักษา และการลดภาระทางความคิดของนักพัฒนาให้ถอยไปอยู่ข้างหลัง
- แอปพลิเคชันจำนวนมากไม่ได้ต้องการการขยายขนาดระดับ Google, Facebook หรือ Uber
- เมื่อเริ่มต้นโปรเจกต์หรือธุรกิจ ควรพิจารณาเกณฑ์ต่อไปนี้ก่อน แทนที่จะ optimize เพื่อ scalability ตั้งแต่แรก
- เทคโนโลยีที่ทีมเชี่ยวชาญ
- เทคโนโลยีที่ดีพอ สำหรับตอบโจทย์ผู้ใช้
- วิธีแก้ปัญหาที่เหมาะกับความสามารถของทีมและใช้แรงน้อยที่สุด
- Postgres ก็ขยายขนาดได้ดีในทางปฏิบัติ แต่สำหรับ use case ด้านคิว มันไม่ใช่ระบบคิวเฉพาะทางที่ขยายได้มากที่สุด
- ในฐานะซอฟต์แวร์อเนกประสงค์ Postgres สามารถจัดการงานหลายประเภทได้ค่อนข้างดี และคิวก็เป็นหนึ่งในนั้น
ตัดสินด้วย “เทคโนโลยีที่น่าเบื่อ”
- เวลาจะเลือกเทคโนโลยีคิว คำถามที่สำคัญที่สุดคือ ตอนนี้กำลังใช้และเข้าใจเทคโนโลยีอะไรอยู่ดี
- เทคโนโลยีที่ใช้อยู่แล้วและเข้าใจดี จะเพิ่มภาระให้กับ software stack น้อยกว่า
- หากใช้งานฐานข้อมูลเชิงสัมพันธ์อยู่แล้ว และฐานข้อมูลนั้นคือ Postgres ก็อาจพิจารณาคิวบน Postgres ก่อนซอฟต์แวร์อื่น
- “เทคโนโลยีที่น่าเบื่อ” ไม่ใช่รายการตายตัว แต่เปลี่ยนไปตามเทคโนโลยีที่ใช้อยู่ในปัจจุบัน
- สำหรับแอปพลิเคชันที่เน้นการส่งข้อความ RabbitMQ อาจเป็นเทคโนโลยีที่น่าเบื่อ
- สำหรับแอปพลิเคชันที่เน้นแคช Redis อาจเป็นเทคโนโลยีที่น่าเบื่อ
- สำหรับแอปพลิเคชันที่มีข้อมูลเชิงสัมพันธ์จำนวนมาก Postgres อาจเป็นเทคโนโลยีที่น่าเบื่อ
- หากนำ Redis, Kafka, RabbitMQ หรือ SQS เข้ามาใหม่เพื่อใช้กับงานเบื้องหลังเพียงอย่างเดียว ต้นทุนจะสูงขึ้น
- มีการเพิ่ม dependency ของระบบใหม่ในสภาพแวดล้อมพัฒนา ทดสอบ และ production
- ในอนาคต Developer, DBA และ SRE ต้องเข้าใจรูปแบบความล้มเหลวและการตั้งค่าของระบบใหม่
- ต้องมีความรู้เรื่องการกู้คืนจากเหตุขัดข้อง การวิเคราะห์ปัญหา และการติดตามประสิทธิภาพ
- unknown unknowns ที่ทีมยังไม่รู้จักยังคงเป็นความเสี่ยง
ต้นทุนและเกณฑ์การเลือกคิวบน Postgres
- เทคโนโลยีที่น่าเบื่อก็ไม่ใช่คำตอบสารพัด และ Postgres ก็ไม่ใช่ข้อยกเว้น
- หากเลือกคิวบน Postgres คุณจะได้ข้อดีเรื่องความคุ้นเคย รูปแบบความล้มเหลวที่รู้จัก และการกระจายต้นทุน แต่ก็อาจต้องจ่ายด้วยสิ่งต่อไปนี้
- การ push/pop ของคิวบน Postgres ช้ากว่า Redis อย่างเห็นได้ชัด
- แทนที่จะมีฐานข้อมูลเชิงสัมพันธ์ตัวเดียว อาจต้องมีทั้งฐานข้อมูลแอปพลิเคชันและฐานข้อมูลคิว
- อาจต้องมีเซิร์ฟเวอร์ฐานข้อมูลแยกต่างหากเพื่อขยายงานเบื้องหลังอย่างอิสระ
- อาจต้อง
VACUUMบ่อยขึ้นจนเกิดภาระด้านประสิทธิภาพ
- ไม่มีตัวเลือกใดใน Postgres, Redis, Kafka, RabbitMQ หรือ SQS ที่ควรถูกมองเป็นค่าเริ่มต้นโดยอัตโนมัติ
- การเลือกเทคโนโลยีคือ trade-off ที่ดำเนินต่อเนื่อง และต้องพิจารณาให้เหมาะกับทีมและความต้องการของแอปพลิเคชัน
- เมื่อไม่แน่ใจ อาจใช้เกณฑ์ว่า “พิจารณาทางเลือกอื่นก็ต่อเมื่อพิสูจน์ได้ว่าเทคโนโลยีที่น่าเบื่อตอบโจทย์ไม่ได้”
โครงสร้างงานเบื้องหลังที่สามารถเปลี่ยนคิวได้
- โค้ดประมวลผลงานเบื้องหลังไม่ควรผูกติดกับเทคโนโลยีคิวใดคิวหนึ่ง
- เมื่อแอปพลิเคชันเติบโตขึ้น อาจมีการเพิ่มเทคโนโลยีอย่าง memcached หรือ Redis ตามความจำเป็น และเมื่อเวลาผ่านไป เทคโนโลยีนั้นก็อาจกลายเป็น “เทคโนโลยีที่น่าเบื่อ” ที่ทีมคุ้นเคย
- หากทำ abstraction ของเทคโนโลยีคิว ผู้ใช้ก็จะเลือกคิวที่เหมาะกับงานได้
- ในบรรดาไลบรารีงานเบื้องหลังยอดนิยมบน GitHub ยกเว้น Hangfire แล้ว ไม่มีตัวไหนเปิดทางให้ออกจาก Redis ไปใช้เทคโนโลยีคิวอื่นได้
- ในโครงสร้างแบบนี้ หากต้องการเปลี่ยนคิว ก็จำเป็นต้องเขียนโค้ดแอปพลิเคชันใหม่
- Neoq เป็นไลบรารีสำหรับ Go ที่สร้างขึ้นตามแนวคิดนี้
- คิวแบ็กเอนด์สามารถใช้ได้ทั้ง in-memory, Postgres และ Redis
- ผู้ใช้สามารถ initialize ด้วยคิวแบ็กเอนด์อื่นได้โดยไม่ต้องแก้โค้ดแอปพลิเคชัน
- implementation ของ in-memory และ Postgres เป็นของที่ให้มาโดยตรง ส่วน implementation ของ Redis ใช้ asynq
- Redis, Kafka, RabbitMQ และ SQS อาจเป็นเทคโนโลยีที่ยอดเยี่ยม แต่ไม่ได้หมายความว่ามันจะเป็นคิวที่เหมาะกับงานนั้นเสมอไป หรือมีระดับความซับซ้อนที่เหมาะสมเสมอไป
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
จนถึงตอนนี้ผมเคยสร้าง ระบบงานแบบกระจาย มา 3 ครั้ง และกฎคร่าว ๆ จากประสบการณ์ที่แนะนำมาหลายปีคือ “ออกแบบโดยยึดขนาด 10 เท่าของปัจจุบันเป็นเกณฑ์”
ถ้าตอนนี้มี 70 requests ต่อวินาที ก็ออกแบบเผื่อ 700 requests; ถ้าต้องใช้เซิร์ฟเวอร์งานแบบ batch 20 เครื่อง ก็ให้ตั้งเกณฑ์ที่ 200 เครื่อง แม้แต่สตาร์ทอัพที่โต 100% ทุกปี อีก 3 ปีข้างหน้าก็จะใหญ่ขึ้น 8 เท่า ดังนั้นยังมีเวลาเขียนใหม่ระหว่างที่เติบโตได้
ระบบแรกที่สร้างหลีกเลี่ยง SQL เพราะเรื่อง “scalability” แต่ก็ลำบากมากเมื่อเจอสถานการณ์ยกเว้นจำนวนมากที่ต้องการความถูกต้องสมบูรณ์ของ transaction
ระบบงานแบบกระจายอีกสองระบบที่สร้างภายหลังใช้ PostgreSQL เป็น coordinator และจัดโครงสร้างโดยมี
SELECT FOR UPDATE SKIP LOCKEDเป็นแกนหลักจริง ๆ ระบบหนึ่งควบคุม worker โดยทั่วไป 350 ตัว ส่วนอีกระบบใช้ priority ที่ซับซ้อนกับงานหลายพันรายการทั้งสองระบบน่าจะรับไหวโดยไม่มีปัญหาจนถึงระดับรายได้ต่อปีหลายล้านดอลลาร์ และเช่น ระบบที่มี worker 350 ตัวนั้น ถ้าปรับเล็กน้อยก็ขยายได้ถึง CPU ราว 2,000 ตัว
เทคโนโลยีระดับ hyperscale ดูเหมือนจะค่อนข้างถูก จนกว่าคุณจะรู้ตัวว่าต้องมี transaction แต่ตั้งแต่วินาทีนั้นเป็นต้นไป การเลียนแบบ semantics ของ transaction บน storage ที่มี eventual consistency จะกลายเป็นฝันร้ายด้านวิศวกรรม
ดังนั้นควรนั่งลงแล้วคำนวณดูว่า ถ้าบริษัททำรายได้ปีละ 100 ล้านดอลลาร์ ระบบกระจายต้องใหญ่แค่ไหน จะหา instance ของ PostgreSQL ที่รับโหลดนั้นได้ง่ายหรือไม่ หรือการ shard ตาม client ก็เพียงพอหรือเปล่า และถ้าเป็นไปได้ PostgreSQL ก็ควรค่าแก่การพิจารณาอย่างจริงจัง เพราะเรื่องจุกจิกนับร้อยอย่างจะง่ายขึ้น
วิธีแก้ด้วย SQL มักเรียบง่ายกว่า ใช้ทรัพยากรคำนวณน้อยกว่า และดูแลง่ายกว่า
แต่ถ้าจะทำให้ใช้งานได้ ก็ต้องรู้จักฐานข้อมูลดีพอที่จะรู้ว่าฟีเจอร์อย่าง
SELECT FOR UPDATE SKIP LOCKEDมีอยู่ตั้งแต่แรก ทุกวันนี้ความรู้นี้ค่อนข้างหายากขึ้น เพราะมีวิศวกรจำนวนมากเติบโตมาในสภาพแวดล้อมที่ ORM หนัก ๆ ไปบังความสามารถจริงของ DBMSระบบต้องรับ peak load ได้ และถ้าไม่รู้ว่า peak คือเท่าไร ก็ต้องเผื่อ margin ความปลอดภัย หรือใส่วิธีทิ้งงาน/เลื่อนงานออกไปเมื่อจำเป็น
ทุกอย่างคือ trade-off และควร optimize เฉพาะสิ่งที่จำเป็นต้อง optimize เท่านั้น ความสามารถในการตัดสินว่าอะไรคือสิ่งนั้นเป็นคุณลักษณะของวิศวกรที่ดี
ถ้านี่เป็นเรื่องใหม่สำหรับคุณ ก็ถือว่าคุณได้ความคุ้มค่าจากการอ่าน HN ระหว่างเวลางานไปหนึ่งสัปดาห์แล้ว ควรจดใส่กระดาษแล้วแปะไว้บนบัตร ATM เลย
ในหลายโปรเจกต์ ผมเลือกแนวทางที่เรียบง่ายกว่า คือใช้ตารางทั่วไปกับ
SELECT FOR UPDATE SKIP LOCKEDซึ่งทำได้ทันทีใน ORM/เฟรมเวิร์ก query DSL ของทุกภาษาhttps://www.pgcasts.com/episodes/the-skip-locked-feature-in-...
ไม่ใช่ “web scale” แต่จากประสบการณ์ของผม มันขยายไปถึง background jobs หลายพันรายการ ได้ง่าย
ใช้วิธีนี้กับงานในองค์กรขนาดใหญ่ได้โดยไม่มีปัญหา ไม่ต้อง deploy อะไรพิเศษหรือเพิ่ม infrastructure ใหม่ แค่รัน worker thread ไม่กี่ตัวใน app ก็พอ จะมี thread สำหรับ reset งานที่ถูกทิ้งไว้ก็ได้
แต่ตลอด 3 ปีไม่เคยเกิดสถานการณ์นั้นจริง ๆ และทุกอย่างอยู่ใน
try/catchถ้าล้มเหลวก็ใส่กลับเข้า queue อีกครั้ง อีกทั้งแอป Java ก็เสถียรมากdelete from taskwhere task_id in( select task_idfrom taskorder by random() -- use tablesample for better performancefor updateskip lockedlimit 1)returning task_id, task_type, params::jsonb as params[1] https://taylor.town/pg-task
แม้จะพูดว่า “background jobs หลายพันรายการ” แต่โดยปกติ queue ควรดูตาม Little's law และควรพูดเป็นอัตรา เช่น อัตราเฉลี่ยของงานที่เข้าต่อวินาทีและระยะเวลาเฉลี่ยของงาน จำนวนดิบ ๆ เพียงอย่างเดียวไม่มีความหมายมากนัก
ตอนแรก
UPDATE ... SETแบบตรงไปตรงมาก็พอใช้ได้ แต่จะจับ lock มากเกินไป แม้จะใช้ subquerySELECTในUPDATEหรือใช้SELECT FOR UPDATE SKIP LOCKEDเพื่อทำให้ lock ตอนดึงงานมีประสิทธิภาพขึ้นได้ แต่สุดท้าย query ที่ดึงงานก็อาจไปบล็อก lock ของกันและกันจน queue หยุดชะงักตอนนั้นคุณอาจปิดการรับงานเข้าเพื่อให้ DB ได้หายใจ แต่ input ที่สูญหายจะทำให้ข้อมูลสูญหาย และโดยมากปัญหาหลักคือกระบวนการดึงงานบล็อกกันเอง
อาจรีบ shard ตารางงานเพื่อหลีกเลี่ยง lock ก็ได้ แต่เปราะบางเมื่อต้อง deploy ข้าม worker หลายตัว และอาจนำไปสู่ข้อมูลสูญหาย จะทิ้งงานบางส่วนแบบสุ่มก็ได้ แต่แน่นอนว่านั่นคือข้อมูลสูญหาย
ตัวเลือกเหล่านี้สร้างความเครียดอย่างยิ่งในระหว่างปฏิบัติการจริง และกู้คืนได้ยากมากหากไม่ออกแบบใหม่ตั้งแต่ฐานราก เว้นแต่คุณจะมีลูกค้าเพียงไม่กี่รายและมั่นใจว่าจะอยู่ที่ระดับงานไม่กี่สิบรายการต่อวินาทีตลอดไป หลังจากเจอสถานการณ์แบบนี้ด้วยตัวเอง ผมจึงเลือกเทคโนโลยี queue จริง ๆ มากกว่าฐานข้อมูลเมื่อทำได้
มักเห็นคำวิจารณ์ทำนองว่า “วิศวกรลอกโครงสร้างพื้นฐานของ FAANG เพื่อให้ดูเท่ ทั้งที่ไม่ตรงกับความต้องการของตัวเองเลย” แต่ผมมองว่าส่วนใหญ่เป็นปัญหาเรื่อง ความรู้และการทำเอกสาร
ถ้าอยากทำสถาปัตยกรรมแบบคิวที่ขยายได้ไม่จำกัดตาม FAANG หรือสตาร์ทอัพอื่น ๆ ก็มีไกด์ บทสอน และไวต์เปเปอร์คุณภาพสูงเป็นสิบ ๆ ชิ้น ที่ช่วยให้ตั้งค่าโซลูชันที่ขยายได้อย่าง Redis, SQS ได้ภายในไม่กี่ชั่วโมง
แม้ต้นทุนการบำรุงรักษาจะสูงกว่า แต่ก็เริ่มได้ด้วยการคัดลอกคำสั่ง โค้ด และการตั้งค่าจากแหล่งที่มีชื่อเสียง
ในทางกลับกัน ถ้าอยากใช้
NOTIFYของ PostgreSQL แล้วค้นหา “SQLALchemy notify listen postgres” ก็เจอแค่คำถาม Stack Overflow ไม่กี่อันที่ไม่มีคำตอบ กับ GitHub Gist ที่แทบไม่มีบริบทผมอยากลองใช้วิธีนี้จริง ๆ ในโปรเจกต์ส่วนตัว แต่ไม่มีเวลาว่าง 2–3 วันไปนั่งหาคำตอบคนเดียว
มองเผิน ๆ เหมือนเป็นตัวเลือกระหว่าง “เรียบง่ายแต่ขยายไม่ได้ คือใช้ PostgreSQL อย่างเดียว” กับ “ซับซ้อนแต่ขยายได้ คือ Redis/SQS ฯลฯ” และนั่นทำให้ดูเหมือนว่าผมตาบอดเพราะเทคโนโลยีเท่ ๆ กับ FAANG จนเลือก scalability ที่ไม่จำเป็น
แต่ถ้านับรวมไกด์และทรัพยากรต่าง ๆ แล้ว ตัวเลือกจริงใกล้เคียงกับ “ซับซ้อนและ scalability ก็คาดเดาไม่ได้ เพราะผมไม่รู้วิธี implement และกับดักต่าง ๆ” กับ “เรียบง่ายและขยายได้ เป็นวิธีที่ทุกคนทำจริง” มากกว่า แบบนี้ตัวเลือกของวิศวกรที่ทำตาม FAANG จึงดูสมเหตุสมผลขึ้นมาก
NOTIFYก็แค่อ่าน https://www.postgresql.org/docs/current/sql-notify.html ไม่ต้องมีทักษะค้นหาอะไรเป็นพิเศษเช่น ถ้าใช้ PostgreSQL ที่โหลดต่ำ การทำ schema migration, เพิ่ม constraint ใหม่, วิเคราะห์ข้อมูล ฯลฯ แทบจะเป็นเรื่องเล็กน้อย
ถ้าใช้ SQS, Cassandra ฯลฯ ก็ได้ scalability/availability มา แต่เมื่อพบว่าดีไซน์เดิมไม่ตรงแล้ว การเปลี่ยนจะใช้เวลานานกว่ามาก เช่นตอนที่ฝั่งธุรกิจขอให้เพิ่ม constraint แบบ “ผู้ใช้ประเภท foo ต้องห้ามผสมค่า bar พร้อมกัน”
แม้จะทำได้โดยไม่ใช้ PostgreSQL แต่โดยเฉพาะเมื่อจำเป็นต้องเปลี่ยน มันไม่ง่ายหรือเรียบง่ายเลย
ดังนั้นถ้าต้องการคงความยืดหยุ่นไว้ ผมมองว่าควรใช้ PostgreSQL และค่อยใช้สิ่งอื่นร่วมกับ PostgreSQL เมื่อรู้แล้วว่ามันจะไม่เปลี่ยน แน่นอนว่ากรณีนี้จะมี overhead ด้านโครงสร้างพื้นฐานและการบำรุงรักษาเพิ่มขึ้น
สุดท้ายมันคือการ trade-off เสมอ และต้องรู้ว่าเมื่อไหร่ควรแลกอะไรกับอะไร
ผมไม่คิดว่าเป็นอุดมคติที่ทุกคนจะ implement background job worker แบบกำหนดเองในแอปพลิเคชันเอง ไม่ว่าจะใช้ backend PostgreSQL หรือคิวแบบอื่น
งานเบื้องหลังมีรายละเอียดปลีกย่อยและรายละเอียดการ implement ที่พลาดได้ง่ายอยู่มาก ปกติจึงควรให้ไลบรารีหรือเฟรมเวิร์กเฉพาะทางที่ครอบคลุมกว่ารับหน้าที่
ถ้าแอป Rails ทุกตัวไม่มี Sidekiq/Active Jobs แล้วแต่ละทีม implement background worker แบบกำหนดเองกันเอง แอป Rails น่าจะมีชื่อเสียงด้านความน่าเชื่อถือแย่กว่าปัจจุบันมาก
เลือกวิธีที่ให้ประโยชน์สูงสุดโดยไม่พาออกห่างจากเป้าหมายหลักมากเกินไปก็พอ
ผมเคยเห็นสตาร์ทอัพที่นักพัฒนาใช้เวลาราว 80% ไปกับการต่อสู้กับเครื่องมือและโครงสร้างพื้นฐาน runway เหลือ 3 เดือน และตอนนี้ปลาย runway ก็เหลือแต่หลุมใหญ่ คิดถึงทีไรก็ยังขนลุก
ดูเหมือนยัง เคารพความซับซ้อน ไม่พอ
ข้อเสียที่ใหญ่ที่สุดเมื่อใช้ PostgreSQL เป็นบัส publish/subscribe ร่วมกับ
LISTEN/NOTIFYคือLISTENเป็นฟีเจอร์ระดับ session จึงไม่เข้ากับ connection pooling แบบระดับ statementถ้าจะใช้วิธีนี้ แนะนำให้ใช้ advisory locks lock แบบชัดเจนอื่น ๆ ทำให้ฐานข้อมูลรับภาระมากกว่า แต่ advisory locks ถูกออกแบบมาให้เบามากโดยตั้งใจ
ตัวอย่าง implementation ที่ชอบคือ que ซึ่งถูก port ไปหลายภาษา
NOTIFY/LISTENคือเมื่อเกิดปัญหาแล้ว วินิจฉัยได้ยากเมื่อไม่นานมานี้ก็มีปัญหาที่ผ่านไปสักพัก
NOTIFY/LISTENทั้งหมดจะหยุดทำงาน และแก้ได้เฉพาะต้องรีสตาร์ตฐานข้อมูล จนต้องเลิกใช้ https://dba.stackexchange.com/questions/325104/error-could-n...เมื่อหลายปีก่อน Skype ใช้ PostgreSQL เป็นคิว ร่วมกับปลั๊กอินเล็ก ๆ เพื่อประมวลผล CDR ทั้งหมด ไม่รู้ว่าตอนนี้ยังใช้อยู่ไหม แต่เมื่อเทียบกับเมื่อ 10 ปีก่อนก็ถือว่าเป็น “ระดับเว็บสเกล” และในขณะที่คนบนอินเทอร์เน็ตถกเถียงกันว่าการใช้ฐานข้อมูลเป็นคิวเป็นแอนตี้แพตเทิร์น มันก็ทำงานได้ดีเฉย ๆ
การมีทรานแซกชันนั้นค่อนข้างสะดวก
https://wiki.postgresql.org/wiki/SkyTools
เพราะเคยใช้ในงานค่อนข้างมาก เลยเคยพูดเรื่องนี้ที่ Sydpy อยู่หลายครั้ง ถ้าคุณดูแลและซัพพอร์ต PostgreSQL ได้ดีอยู่แล้ว มันก็มีประโยชน์
แต่ถ้าเป็นสมัยนี้คงใช้คิวเฉพาะทาง และจะหลีกเลี่ยง RabbitMQ เป็นพิเศษ
แม้จะกังวลเรื่องการผูกติดกับผู้ขาย แต่การทำงานพื้นฐานของคิวก็เล็กมากในทางปฏิบัติ ประมาณ push/pop ดังนั้นการเขียนให้ย้ายออกได้ง่ายเมื่อจำเป็นก็ค่อนข้างง่าย
ถ้าคุณเอาคิวใส่ไว้ในฐานข้อมูลเพียงเพราะมีฐานข้อมูลรันอยู่แล้ว ตรรกะนั้นก็เป็นดาบสองคม แม้จะไม่น่ายินดีนัก แต่ความวุ่นวายของคิวตอนนี้อาจลาก ที่เก็บข้อมูลหลัก ของคุณให้ล่มไปด้วยได้
ข้อดีที่ใหญ่ที่สุดอย่างหนึ่งของการใช้ PostgreSQL เป็นคิวของแอปพลิเคชัน คือทุกงานอะซิงโครนัสที่จองไว้จะได้ประโยชน์จาก ความเป็นทรานแซกชัน
ตัวอย่างเช่น มีงานเปลี่ยนแปลงแบ็กเอนด์ที่ซับซ้อนหลังสมัครสมาชิกแล้วต้องส่งอีเมล หากใส่งานส่งอีเมลเข้าไปแล้วส่วนถัดมาของทรานแซกชันเดียวกันล้มเหลวจน rollback งานอีเมลนั้นก็จะไม่ถูกใส่เข้าคิวตั้งแต่แรก
ในกรณีอีเมล หากรอการยืนยันการส่งแล้วล้มเหลว ก็ต้องเลือกว่าให้ทรานแซกชันล้มเหลวและยอมรับความเป็นไปได้ที่อีเมลจะซ้ำ หรือให้ดำเนินต่อและยอมรับความเป็นไปได้ที่อีเมลจะสูญหาย
ข้อดีใหญ่จริง ๆ มาจาก code path ที่แก้ไข DB แบบอะซิงโครนัส เพราะการ consume งานและการอัปเดต DB อยู่ในทรานแซกชันเดียวกัน จึงจัดการ semantics แบบ exactly once ได้ทั้งหมดในเชิงทรานแซกชัน
ใน PostgreSQL จะมีตารางทรานแซกชันล็อกที่เก็บ event ที่ถูก trigger และ
pg_current_xact_id()ของทรานแซกชันนั้น จะใช้xminที่มีอยู่ในตัวของแถวก็ได้ แต่ต้องคอยระวัง transaction wraparound เมื่อ insert ลงแถวนี้จะเกิดNOTIFYโปรเซสเบื้องหลังจะรันซ้ำ ๆ โดยเลือกทุกแถวจากตารางทรานแซกชันที่มี transaction ID อยู่ระหว่าง
xminของการรันครั้งก่อนกับpg_snapshot_xmin(pg_current_snapshot())ในปัจจุบัน จากนั้นแมป event เหล่านั้นเป็นงาน ส่งเข้า SQS บันทึกxminแล้วใช้LISTENรอNOTIFYถัดไปในคิว PostgreSQL ถ้าใส่งานส่งอีเมลแล้วภายหลังทรานแซกชันล้มเหลว อีเมลจะไม่เข้าคิว ซึ่งถูกต้อง
โค้ดฐานข้อมูลที่ต้องแยกจากกันโดยสิ้นเชิงก็ควรแยกเป็นคนละทรานแซกชัน ในทางกลับกัน ถ้าผู้ใช้ยังไม่ได้ถูกสร้างใน DB คุณอาจไม่อยากส่งอีเมลสมัครสมาชิกเช่นกัน ดังนั้นขึ้นอยู่กับสถานการณ์
ในคิวที่ใช้ Redis หรือแทบจะคิวอื่นใดก็ตาม คุณจะเจอสถานการณ์ได้ไม่นานที่งานซึ่งสมมติว่าเรคอร์ดในฐานข้อมูลมีอยู่แล้วถูกรันก่อนทรานแซกชันจะ commit โค้ดที่แก้เรื่องนี้มักจะดูฝืนและซับซ้อน
สิ่งที่ชอบใน Kafka คือมันเป็นแค่ ล็อกแบบ append-only และโดยแก่นแล้วไคลเอนต์ถือไว้แค่ออฟเซ็ตเท่านั้น
ในเชิงแนวคิดเข้าใจง่ายมาก มีความคงทน และค่อนข้างทนต่อความขัดข้อง เพราะสามารถย้อนกลับไปอ่านใหม่จากออฟเซ็ตใดก็ได้
น่าเสียดายที่ Kafka มาพร้อมความซับซ้อนพอสมควรจากลักษณะการกระจายระบบ ทำให้สุดท้ายไม่คุ้มค่าสำหรับ use case ส่วนใหญ่
โดยส่วนตัวอยากให้มีอะไรคล้าย ๆ กันแต่ดูแลง่ายกว่า โหนดเดียวคงจัดการ event ได้หลักหลายร้อย หรืออาจหลายพันต่อวินาที และถ้าไม่มีความซับซ้อนแบบ distributed ก็น่าจะดีมาก
ในทางทฤษฎี PostgreSQL ก็ทำได้ แค่ไม่ลบแถวเลย บางทีนั่นอาจเป็นคำตอบก็ได้
ถ้าไม่ต้องการฟีเจอร์ครบชุดอย่าง consumer group หรือ partition การตั้งค่าฝั่ง consumer ก็น่าจะง่ายกว่ามาก
ที่ Windmill ใช้วิธีนี้อยู่พอดี Windmill เป็นโอเพนซอร์สทางเลือกของ Retool และเป็นเครื่องมือที่ใกล้กับ Airflow สมัยใหม่ และรัน benchmark ทุกวัน
เมื่อรัน Windmill worker หนึ่งตัวกับ PostgreSQL เป็นคอนเทนเนอร์บน GitHub CI instance ระดับพอใช้ benchmark จะประมวลผลได้ 1,200 งานต่อวินาที ถ้าเพิ่ม worker จะ scale ได้เสถียรถึง 5,000 งานต่อวินาที
กำลังพิจารณาใช้ Citus เพื่อทะลุกำแพง 5,000 งานต่อวินาทีบนอินสแตนซ์แบบ multi-tenant
https://github.com/windmill-labs/windmill/tree/benchmarks
ตอนที่ปริมาณอยู่ราว ๆ 10 ข้อความต่อวินาที เราเคยใช้ PostgreSQL กับคิวบางส่วน และมันก็ขยายได้พอสมควร แต่พูดตามตรง การตั้งค่า SQS หรือสแตกคิวอื่น ๆ บน AWS, GCP, Azure นั้นง่ายมากอยู่แล้ว ถูกสร้างมาเพื่อวัตถุประสงค์นี้โดยเฉพาะ และมีของอย่าง DLQ มาให้ในตัวด้วย
ผมไม่เห็นเหตุผลว่าทำไมต้องเลือกทางนี้ ทั้งที่ต้องกังวลว่าระบบนั้นจะล่มแล้วกระทบไปถึงสถานะของ DB ส่วนอื่น ๆ
ผมชอบหลักการ “ใช้เครื่องมือที่ง่ายที่สุด” แต่บางครั้งดูเหมือนวิศวกรจะไปไกลเกินไป จนสุดท้ายเหลือแต่เครื่องมือที่โง่ที่สุดพร้อมข้อควรระวังคลุมเครือ ทั้งที่ทางเลือกกระแสหลักนั้นค่อนข้างถูกและเรียบง่าย
เมื่อเก็บสถานะงานไว้ใน DB ก็สามารถ query สถานะเพื่อดูได้สะดวก ถึงแม้มันอาจไม่ได้แสดงสถานะจริงได้ถูกต้องเป๊ะ ๆ แต่ก็ช่วยเวลา分析เหตุขัดข้องระหว่างปฏิบัติการ โดยเฉพาะอย่างยิ่งคิวงานส่วนใหญ่มักลบเรคอร์ดที่ประมวลผลแล้วทิ้งไปเลย
และถ้าทำให้ background job ทั้งหมดเป็นแบบ idempotent วิธีอย่าง “ส่งข้อความไปยังคิวงานเพื่อบอกให้ประมวลผลงานนี้” ก็แทบจะปลอดภัยเสมอ
ถ้าพึ่งพาแค่ message queue อย่างเดียว เวลามีปัญหาด้านประสิทธิภาพ มักยากที่จะรู้ว่าเกิดอะไรขึ้น ตัวอย่างเช่น RabbitMQ อาจบอกขนาดคิวได้ แต่แทบไม่มีหรือมีความสามารถจำกัดมากในการตรวจสอบข้อมูลภายใน
ซอฟต์แวร์บางตัวอาจคาดได้ว่าจะไม่มีวันเกินขีดความสามารถของ PostgreSQL และแม้จะเกิน ก็สามารถออกแบบให้ย้ายไปบริการอื่นได้ง่าย
และอาจต้องการสร้างระบบงานที่มี transaction ภายใน PostgreSQL แบบเรียบง่ายก็ได้
เราใช้ Oban ของ Elixir เพื่อประมวลผล งานตั้งแต่หลายแสนถึงหลายล้านงาน ต่อวันภายใน PostgreSQL
การมี semantics ของ transaction รอบ ๆ งานเบื้องหลังนั้นสะดวกมาก เช่น จองการส่งอีเมลเฉพาะเมื่อ transaction สำเร็จเท่านั้น
ต้องปรับจูน
autovacuumนิดหน่อย แต่พอจัดการเรียบร้อยแล้ว มันก็เหมาะกับเรามาก