ยังจำเป็นต้องใช้ Redis ไหม? PostgreSQL มีคิว, ล็อก และ Pub/Sub ให้แล้ว (2021)
(spin.atomicobject.com)- ชุดที่พบได้บ่อยในเว็บเซอร์วิสอย่าง PostgreSQL+Redis นั้นสะดวก แต่บางส่วนของคิวงานเบื้องหลัง, distributed lock และ Pub/Sub สามารถจัดการได้ด้วย PostgreSQL เพียงอย่างเดียว
FOR UPDATE SKIP LOCKEDใน PostgreSQL 9.5 ใช้ในการทำคิวที่ป้องกันไม่ให้หลาย worker หยิบงานเดียวกันไป โดยข้ามแถวที่ถูกล็อกโดยไม่ต้องรอ- distributed lock ระดับแอปพลิเคชันสามารถสร้างได้ด้วย advisory locks ของ PostgreSQL โดยนำเอนจินล็อกภายในมาใช้ซ้ำเพื่อวัตถุประสงค์ที่แอปพลิเคชันกำหนด
LISTEN/NOTIFYใน PostgreSQL 9 รองรับการ subscribe ช่องสตริงใด ๆ และส่ง notification จึงใช้เป็นเลเยอร์ Pub/Sub ได้ และ Rails ActionCable ก็รองรับการใช้ PostgreSQL เป็นค่าเริ่มต้น- Redis ยังคงมีจุดแข็งด้านการทำ TTL caching และการจัดการข้อมูลชั่วคราว แต่บางระบบสามารถลดการพึ่งพา Redis เพื่อลด ต้นทุนการปฏิบัติการและความซับซ้อนในการพัฒนา ได้
บทบาทของ Redis ที่ PostgreSQL สามารถรับมาแทนได้
- เว็บเซอร์วิสทั่วไปใช้ PostgreSQL เป็นที่เก็บข้อมูล และใช้ Redis สำหรับประสานคิวงานเบื้องหลังหรือการดำเนินการแบบ atomic ที่จำกัดบางอย่าง
- ตัว Redis เองมีประโยชน์ แต่ในชุดการใช้งานนี้ บางบทบาทที่ Redis รับอยู่สามารถแทนได้ด้วยความสามารถของ PostgreSQL เพียงอย่างเดียว
คิวงานเบื้องหลัง
- Redis มักถูกใช้ในเว็บเซอร์วิสเพื่อ ประสานคิวงาน สำหรับส่งงานไปยังพูลของ background worker
- บันทึกงานเบื้องหลังที่ต้องรันและข้อมูลอินพุต
- ต้องรับประกันว่าในบรรดา worker หลายตัว จะมีเพียงตัวเดียวที่หยิบงานนั้นไป
- Redis เหมาะกับงานนี้เพราะมีการดำเนินการแบบ atomic บนโครงสร้างข้อมูลจำนวนมาก
- ตั้งแต่ PostgreSQL 9.5 เป็นต้นมา สามารถใช้ตัวเลือก
SKIP LOCKEDในคำสั่งSELECT ... FOR ...ได้- เมื่อระบุตัวเลือกนี้ PostgreSQL จะไม่สนใจแถวที่ต้องรอให้ปลดล็อก
- เมื่อระบุ
FOR UPDATE SKIP LOCKEDจะมีการได้ ล็อกระดับแถว สำหรับแถวที่ถูกส่งกลับมาโดยปริยาย - ด้วย
SKIP LOCKEDจึงไม่มีโอกาสถูกบล็อกโดยล็อกของ transaction อื่น - หากมีงานอื่นที่ต้องประมวลผล งานนั้นจะถูกส่งกลับมา
- แม้ worker หลายตัวจะรันคำสั่งเดียวกัน ก็จะไม่ได้แถวเดียวกันเพราะล็อกระดับแถว
- โฟลว์พื้นฐานคือเลือกงานหนึ่งรายการที่มีสถานะ
pendingภายใน transaction เปลี่ยนงานนั้นเป็นrunningแล้วส่งคืนBEGINSELECT ... FOR UPDATE SKIP LOCKEDUPDATE jobs SET status = 'running' ... RETURNING jobs.*COMMIT
- ข้อควรระวังคือ เมื่อมีจำนวน worker และงานมากทั้งคู่ ต้นทุนในการไล่ดูคิวและพยายามขอล็อกอาจสูงขึ้น
- แอปส่วนใหญ่ที่เคยจัดการจริงมี background worker น้อยกว่า 12 ตัว ดังนั้นต้นทุนนี้จึงมีแนวโน้มไม่สูงนัก
ล็อกระดับแอปพลิเคชัน
- มีบางกรณี เช่น รูทีนที่ซิงก์กับบริการ third-party ซึ่งต้องทำให้สำหรับผู้ใช้คนหนึ่ง มีเพียงอินสแตนซ์เดียวเท่านั้นที่รันอยู่ในทุก server process
- distributed lock ลักษณะนี้เป็นอีก use case ที่พบบ่อยของ Redis
- PostgreSQL สามารถบรรลุวัตถุประสงค์เดียวกันได้ด้วย
advisory locks- advisory locks ช่วยให้นำเอนจินล็อกที่ PostgreSQL ใช้ภายในมาใช้เพื่อวัตถุประสงค์ที่แอปพลิเคชันกำหนดเองได้
Pub/Sub
- Redis มักถูกใช้เมื่อต้อง push event ไปยัง client ที่ active อยู่ด้วย
- แจ้งผู้ใช้ว่าสามารถอ่านข้อความใหม่ได้
- สตรีมข้อมูลไปยัง client ทันทีที่พร้อม
- โดยทั่วไป WebSocket เป็นเลเยอร์ส่ง event และ Redis ทำหน้าที่เป็น เอนจิน Pub/Sub
- ตั้งแต่ PostgreSQL 9 เป็นต้นมา มีฟังก์ชัน Pub/Sub ผ่านคำสั่ง
LISTENและNOTIFY- client ของ PostgreSQL สามารถ subscribe ช่องข้อความเฉพาะที่เป็นสตริงใด ๆ ด้วย
LISTENได้ - เมื่อ client อื่นส่ง
NOTIFYไปยังช่องนั้น client ทั้งหมดที่ subscribe อยู่จะได้รับ notification - สามารถแนบข้อความขนาดเล็กไปด้วยได้ตามต้องการ
- client ของ PostgreSQL สามารถ subscribe ช่องข้อความเฉพาะที่เป็นสตริงใด ๆ ด้วย
- หากใช้ Rails และ ActionCable อยู่ ก็รองรับการใช้ PostgreSQL เป็นค่าเริ่มต้น
พื้นที่ที่ยังน่าเก็บ Redis ไว้
- Redis รับผิดชอบคนละพื้นที่กับ PostgreSQL และแข็งแกร่งในงานที่ PostgreSQL ไม่ได้ตั้งเป้าไว้
- การแคชข้อมูล ที่มี TTL
- การเก็บและจัดการข้อมูลชั่วคราว
- PostgreSQL มีขอบเขตความสามารถกว้างเกินกว่าจะมองว่าเป็นเพียงฐานข้อมูล SQL ธรรมดาหรือองค์ประกอบทึบที่อยู่หลัง ORM
- งานบางส่วนที่เคยมอบให้ Redis อาจเหมาะกับ PostgreSQL ด้วยเช่นกัน
- การละ Redis ออกอาจเป็นทางเลือกที่ช่วยลด ต้นทุนการปฏิบัติการ และความซับซ้อนในการพัฒนาอันเกิดจากการพึ่งพาบริการข้อมูลหลายตัวได้
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เพราะทุกคนยืนกรานจะใช้ สถาปัตยกรรมที่กระจายตัวเกินไป กันหมด จึงมักมองไม่เห็นข้อดีที่แท้จริงของ Redis ถ้ารันบนเครื่องเดียวกับแอปพลิเคชัน มันตอบสนองได้เร็วกว่ามากเมื่อเทียบกับ 1 มิลลิวินาที และทำให้แอปพลิเคชันทำสิ่งที่ทำด้วย Postgres ได้ยาก
Postgres ยอดเยี่ยมก็จริง แต่ไม่ได้รันอยู่ในหน่วยความจำบนเครื่องเดียวกับแอปพลิเคชัน ถ้าต้องการแค่สิ่งอย่างคิว ก็อาจไม่จำเป็นต้องมีที่เก็บคีย์-ค่าแบบ in-memory แก่นสำคัญของที่เก็บคีย์-ค่าแบบ in-memory คือการทำงานที่ต้องใช้ ลักษณะสมรรถนะของ RAM และคุณจะไม่ได้ลักษณะนั้นผ่านการเชื่อมต่อเครือข่าย
Postgres ไม่ได้มีเวทมนตร์พิเศษอะไร มันก็เป็นแค่โปรแกรมที่รันในโปรเซสอื่นเหมือน Redis สำหรับการเชื่อมต่อโลคัลก็ใช้ pipe ที่เร็วเพื่อลด latency และยังใช้วิธีส่งข้อมูลปริมาณมากที่เร็วกว่าได้ด้วย ผมเคยใช้แบบนี้มาหลายครั้งแล้ว
แต่สำหรับเครื่องมือทำงานภายใน อาจขยายใช้งาน instance เดียวไปได้นาน และแคชแบบ in-memory นี้ทำให้เร็วมาก django-cachalot เป็นไลบรารีที่จัดการ cache invalidation อัตโนมัติทุกครั้งที่มีการเขียนลงตาราง วิธีค่อนข้างหยาบ แต่ช่วยเพิ่มประสิทธิภาพแทบไม่ต้องลงแรง และแอปงานภายในที่อัปเดตไม่บ่อยก็แทบจะรันจาก RAM จริง ๆ แล้วค่อยกลับไปใช้ query ฐานข้อมูลปกติเมื่อไม่มีข้อมูลในแคช
https://github.com/noripyt/django-cachalot
การอภิปรายตรงนี้มีการปกป้องจากมุมของ Redis ค่อนข้างมาก แต่แน่นอนว่า Redis มี บางขอบเขตเฉพาะ ที่ทำได้ดีกว่า
อย่างไรก็ดี ผมคิดว่านั่นไม่ใช่ประเด็นหลักของบทความ ประโยคสำคัญสรุปได้ว่า “PostgreSQL มีความสามารถมากกว่าที่คุณคาดไว้มาก เมื่อคุณมองมันเป็นแค่ฐานข้อมูล SQL ธรรมดา หรือเป็นตัวตนลึกลับที่ซ่อนอยู่หลัง ORM” ถ้าคุณใช้ฐานข้อมูล ผ่าน ORM เท่านั้น ก็มีโอกาสสูงที่จะพลาดความสามารถของฐานข้อมูล ไม่ว่าจะเป็นฐานข้อมูลใดก็ตาม ถ้าจำเป็นต้องเพิ่มบริการอื่นอย่าง Redis บางทีการใช้ฐานข้อมูลที่ตั้งค่าไว้แล้วอาจดีกว่าการเพิ่ม dependency ใหม่
การเข้าใจว่า Postgres ทำอะไรได้บ้างเป็นเรื่องดี มันเป็นฐานข้อมูลที่ทรงพลัง
ข้อโต้แย้งคืออุปสรรคในการใช้ Redis ต่ำมาก และสิ่งที่ได้กลับมาคือประสิทธิภาพสูง การรองรับจากไลบรารีมากมาย และช่วยลดโหลดของฐานข้อมูลหลักได้ ตัวอย่างเช่น คุณทำ แคชคำตอบ API ด้วย Postgres ได้ TTL ก็ใช้ cron job กวาดค่าข้อมูลแคชเก่า ๆ ออกไป หรือจะใช้ Redis ไปเลยก็ได้
advisory lock นั้นเจ๋งและมีประโยชน์ แต่ถ้าใช้สิ่งอย่าง PgBouncer อาจเกิดปัญหาระหว่าง session advisory lock กับ transaction interleaving ได้ ระบบแยกต่างหากมีข้อเสียอย่าง network call, availability, และ domain knowledge แต่ trade-off ระดับ Redis นั้นถือว่าค่อนข้างต่ำ
เป็นบทความที่ค่อนข้างเก่าแล้ว แต่ตอนนี้กลายเป็น pattern ที่พบได้บ่อยมาก 90% ของโปรเจกต์ที่ต้องการแค่ คิวงาน สำหรับส่งอีเมลหรือสร้างรายงานไม่ได้ประมวลผลข้อความหลายล้านรายการต่อวินาที ดังนั้นแนวทางที่ทำให้ stack เรียบง่ายลงจึงคุ้มค่าที่จะพิจารณา
ผมใช้ pattern แบบนี้บ่อยเพื่อเลี่ยงปัญหาที่เจอกับ Celery แล้วแยกออกมาเป็นเฟรมเวิร์กต่างหาก: https://github.com/TkTech/chancy ยินดีรับ feedback
เครื่องมือแนวนี้มีเยอะ และบางตัวเป็นบริการเชิงพาณิชย์ แปลว่ามีความต้องการชัดเจน
https://worker.graphile.org/ (Node.js)
https://riverqueue.com/ (Go)
https://github.com/acaloiaro/neoq (Go)
https://github.com/contribsys/faktory (Go)
https://github.com/sorentwo/oban (Elixir)
https://github.com/procrastinate-org/procrastinate (Python)
PGQueuer ใช้ FOR UPDATE SKIP LOCKED และ LISTEN/NOTIFY ของ PostgreSQL เพื่อให้บริการคิวงาน การล็อก และการแจ้งเตือนแบบเรียลไทม์
หากใช้ PostgreSQL อยู่แล้ว นี่เป็นทางเลือกแบบมินิมัลที่ไม่ต้องใช้ Redis
https://github.com/janbjorge/PGQueuer
เผื่อข้อมูลไว้ ผมเป็นคนทำเอง
ชอบ Postgres แต่ก็มีข้อจำกัดอยู่บ้าง
ถ้าต้องการที่เก็บแบบคีย์-ค่า ต้องพิจารณาว่ารู้จัก autovacuum หรือไม่ รู้ข้อจำกัดของ connection pool หรือไม่ และต้องการอะไรระหว่าง throughput กับความปลอดภัย ถ้าต้องการคิว ต้องดูว่าเป็นการประมวลผลตามลำดับหรือไม่ มีการจำกัดความเร็วหรือไม่ เป็น fan-out หรือไม่ หรือแยกตามหัวข้อหรือไม่ ถ้าต้องการ publish/subscribe ต้องพิจารณาว่ากังวลเรื่องการรับซ้ำหรือไม่ กังวลเรื่องข้อความสูญหายหรือไม่ และต้องการ replay หรือไม่ ถ้าต้องการล็อก ต้องรู้ข้อจำกัดของ connection pool และ statement_timeout ปัญหาส่วนใหญ่ข้างต้นแก้ได้ แต่ไม่ได้ง่ายขนาดนั้น
อุปสรรคใหญ่ใน publish/subscribe ของ Postgres คือขนาดข้อความสูงสุดอยู่ที่ 8000 ไบต์
วิธีเลี่ยงที่แนะนำคือใส่ข้อมูลไว้ในตารางแล้วส่งเฉพาะ ID แต่ถ้าไม่อยากเก็บข้อมูลนั้นไว้ตลอดไป ก็ต้องทำ garbage collection ให้ข้อมูลนั้น และเพิ่มงานต่อข้อความทุกครั้ง แน่นอนว่าบางกรณีก็ไม่เป็นไร แต่ใน use case จำนวนมากของ Redis ข้อจำกัดนี้ทำให้มองว่าเทียบเท่ากันได้ยาก
มาดูกันว่า pgsql จัดการ การเชื่อมต่อจากไคลเอนต์ 15000 รายการ ได้หรือไม่
คนจาก Planetscale เคยพูดในพอดแคสต์ว่าอินสแตนซ์ MySQL ของ GitHub แต่ละตัวรองรับ การเชื่อมต่อมากกว่า 50,000 รายการ ขณะที่ถ้าใน Postgres ต้องการการเชื่อมต่อเกิน 100 รายการ ก็เริ่มต้องใช้ PgBouncer แล้ว
คิว ล็อก และ publish/subscribe ทำได้ แต่ แคช ซึ่งเป็นการใช้งานที่สำคัญที่สุดของ Redis กลับไม่มี
การอัปเดตใน Postgres ขึ้นชื่อว่าแพงกว่าการแทรก สร้างขยะ และต้องใช้ vacuum การเขียนก็ช้าลงมากเพราะการรับประกัน durability ที่ไม่สำคัญสำหรับแคช การหมดอายุอัตโนมัติ สะดวกมากและช่วยลดข้อผิดพลาด
synchronous commit, autovacuum ฯลฯ ก็ปิดได้เช่นกัน แน่นอนว่า Redis ยังน่าจะเร็วกว่าอยู่ แต่ความต่างอาจไม่ได้มากพอให้บริษัททั่วไปต้องใส่ใจ
ถ้าพูดแก่นของบทความให้ชัดขึ้นคือ ให้ เริ่มด้วย Postgres แล้วค่อยย้ายไป Redis เมื่อมีความจำเป็น
ควรรักษาจำนวนชิ้นส่วนที่เคลื่อนไหวให้มีน้อยที่สุดเท่าที่ทำได้