2 คะแนน โดย GN⁺ 2024-11-04 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ชุดที่พบได้บ่อยในเว็บเซอร์วิสอย่าง 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 แล้วส่งคืน
    • BEGIN
    • SELECT ... FOR UPDATE SKIP LOCKED
    • UPDATE 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
    • สามารถแนบข้อความขนาดเล็กไปด้วยได้ตามต้องการ
  • หากใช้ Rails และ ActionCable อยู่ ก็รองรับการใช้ PostgreSQL เป็นค่าเริ่มต้น

พื้นที่ที่ยังน่าเก็บ Redis ไว้

  • Redis รับผิดชอบคนละพื้นที่กับ PostgreSQL และแข็งแกร่งในงานที่ PostgreSQL ไม่ได้ตั้งเป้าไว้
    • การแคชข้อมูล ที่มี TTL
    • การเก็บและจัดการข้อมูลชั่วคราว
  • PostgreSQL มีขอบเขตความสามารถกว้างเกินกว่าจะมองว่าเป็นเพียงฐานข้อมูล SQL ธรรมดาหรือองค์ประกอบทึบที่อยู่หลัง ORM
  • งานบางส่วนที่เคยมอบให้ Redis อาจเหมาะกับ PostgreSQL ด้วยเช่นกัน
  • การละ Redis ออกอาจเป็นทางเลือกที่ช่วยลด ต้นทุนการปฏิบัติการ และความซับซ้อนในการพัฒนาอันเกิดจากการพึ่งพาบริการข้อมูลหลายตัวได้

1 ความคิดเห็น

 
GN⁺ 2024-11-04
ความคิดเห็นจาก Hacker News
  • เพราะทุกคนยืนกรานจะใช้ สถาปัตยกรรมที่กระจายตัวเกินไป กันหมด จึงมักมองไม่เห็นข้อดีที่แท้จริงของ Redis ถ้ารันบนเครื่องเดียวกับแอปพลิเคชัน มันตอบสนองได้เร็วกว่ามากเมื่อเทียบกับ 1 มิลลิวินาที และทำให้แอปพลิเคชันทำสิ่งที่ทำด้วย Postgres ได้ยาก
    Postgres ยอดเยี่ยมก็จริง แต่ไม่ได้รันอยู่ในหน่วยความจำบนเครื่องเดียวกับแอปพลิเคชัน ถ้าต้องการแค่สิ่งอย่างคิว ก็อาจไม่จำเป็นต้องมีที่เก็บคีย์-ค่าแบบ in-memory แก่นสำคัญของที่เก็บคีย์-ค่าแบบ in-memory คือการทำงานที่ต้องใช้ ลักษณะสมรรถนะของ RAM และคุณจะไม่ได้ลักษณะนั้นผ่านการเชื่อมต่อเครือข่าย

    • ถ้ามีแค่โปรเซสโลคัลตัวเดียวที่จะใช้ Redis ในเครื่องเป็น แคชแบบ in-memory ก็ควรใช้โครงสร้างข้อมูลของภาษาโปรแกรมที่ใช้อยู่ไปเลยดีกว่า
    • สงสัยว่าเมื่อรันแบบโลคัล overhead ของ Postgres กับ Redis ต่างกันแค่ไหน และก็ไม่เข้าใจว่าทำไมถึงมองว่า Postgres รันแบบโลคัลไม่ได้
      Postgres ไม่ได้มีเวทมนตร์พิเศษอะไร มันก็เป็นแค่โปรแกรมที่รันในโปรเซสอื่นเหมือน Redis สำหรับการเชื่อมต่อโลคัลก็ใช้ pipe ที่เร็วเพื่อลด latency และยังใช้วิธีส่งข้อมูลปริมาณมากที่เร็วกว่าได้ด้วย ผมเคยใช้แบบนี้มาหลายครั้งแล้ว
    • Django มีแคชที่รองรับ Redis มาในตัว และมีตัวเลือก แคชแบบ in-memory ด้วย แต่ถูกระบุว่า “ไม่เหมาะสำหรับ production” เพราะถ้ามี Django หลาย instance แคชแบบ in-memory แต่ละตัวจะไม่เหมือนกัน
      แต่สำหรับเครื่องมือทำงานภายใน อาจขยายใช้งาน instance เดียวไปได้นาน และแคชแบบ in-memory นี้ทำให้เร็วมาก django-cachalot เป็นไลบรารีที่จัดการ cache invalidation อัตโนมัติทุกครั้งที่มีการเขียนลงตาราง วิธีค่อนข้างหยาบ แต่ช่วยเพิ่มประสิทธิภาพแทบไม่ต้องลงแรง และแอปงานภายในที่อัปเดตไม่บ่อยก็แทบจะรันจาก RAM จริง ๆ แล้วค่อยกลับไปใช้ query ฐานข้อมูลปกติเมื่อไม่มีข้อมูลในแคช
      https://github.com/noripyt/django-cachalot
    • สิ่งที่เร็วกว่า Redis ในการตั้งค่านี้คือ hashmap และไม่มีเหตุผลที่ Postgres จะรันบนเซิร์ฟเวอร์เดียวกับแอปไม่ได้ จริง ๆ แล้วเป็นการตั้งค่าที่พบได้ค่อนข้างบ่อย
    • การออกแบบเกินจำเป็นและการรีบทำให้เป็นระบบกระจายเป็นปัญหาจริง แต่ Redis ย่อมาจาก “Remote Dictionary Server” จุดประสงค์เดิมไม่ใช่การรันแบบโลคัลเลย แน่นอนว่าการรันแบบโลคัลก็อาจเป็นตัวเลือกการออกแบบที่สมเหตุสมผลได้ เช่น กรณีที่ dictionary พื้นฐานของภาษาไม่รองรับ range query
  • การอภิปรายตรงนี้มีการปกป้องจากมุมของ 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 นั้นถือว่าค่อนข้างต่ำ

    • ข้อดีคือมีสิ่งที่ต้องดูแลใน production น้อยลงหนึ่งอย่าง เริ่มด้วย PostgreSQL แล้วค่อยเพิ่มระบบเฉพาะทางเมื่อจำเป็นเพราะประสิทธิภาพ การขยายระบบ หรือค่าใช้จ่าย
  • เป็นบทความที่ค่อนข้างเก่าแล้ว แต่ตอนนี้กลายเป็น 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)

    • กำลังมองหาคิวงานง่าย ๆ อยู่ Huey ก็ใช้ได้ แต่เราใช้ Postgres อยู่แล้ว และงานที่ใส่คิวมีประมาณชั่วโมงละครั้ง การใช้แบบ อิง Redis จึงรู้สึกเกินจำเป็นเสมอ
    • ฝั่ง Node ก็มี PG-Boss ด้วย: https://github.com/timgit/pg-boss
  • 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 ปัญหาส่วนใหญ่ข้างต้นแก้ได้ แต่ไม่ได้ง่ายขนาดนั้น

    • ต้องดูด้วยว่า implementation จะไม่พังเมื่อทรานแซกชันที่รันนานขัดขวาง การลบทูเพิล ของ vacuum
  • อุปสรรคใหญ่ใน publish/subscribe ของ Postgres คือขนาดข้อความสูงสุดอยู่ที่ 8000 ไบต์
    วิธีเลี่ยงที่แนะนำคือใส่ข้อมูลไว้ในตารางแล้วส่งเฉพาะ ID แต่ถ้าไม่อยากเก็บข้อมูลนั้นไว้ตลอดไป ก็ต้องทำ garbage collection ให้ข้อมูลนั้น และเพิ่มงานต่อข้อความทุกครั้ง แน่นอนว่าบางกรณีก็ไม่เป็นไร แต่ใน use case จำนวนมากของ Redis ข้อจำกัดนี้ทำให้มองว่าเทียบเท่ากันได้ยาก

  • มาดูกันว่า pgsql จัดการ การเชื่อมต่อจากไคลเอนต์ 15000 รายการ ได้หรือไม่

    • ความแตกต่างระหว่าง Postgres กับ MySQL ในจุดนี้ใหญ่จนน่าทึ่งจริง ๆ
      คนจาก Planetscale เคยพูดในพอดแคสต์ว่าอินสแตนซ์ MySQL ของ GitHub แต่ละตัวรองรับ การเชื่อมต่อมากกว่า 50,000 รายการ ขณะที่ถ้าใน Postgres ต้องการการเชื่อมต่อเกิน 100 รายการ ก็เริ่มต้องใช้ PgBouncer แล้ว
    • ใช้ connection pooling ก็ได้ ใช้ได้ด้วยการคลิกไม่กี่ครั้งเหมือน AWS RDS Proxy
  • คิว ล็อก และ publish/subscribe ทำได้ แต่ แคช ซึ่งเป็นการใช้งานที่สำคัญที่สุดของ Redis กลับไม่มี
    การอัปเดตใน Postgres ขึ้นชื่อว่าแพงกว่าการแทรก สร้างขยะ และต้องใช้ vacuum การเขียนก็ช้าลงมากเพราะการรับประกัน durability ที่ไม่สำคัญสำหรับแคช การหมดอายุอัตโนมัติ สะดวกมากและช่วยลดข้อผิดพลาด

    • ฟีเจอร์ที่ไม่จำเป็นส่วนใหญ่ปิดได้ เช่น ทำตารางเป็น unlogged ได้: https://www.postgresql.org/docs/current/sql-createtable.html...
      synchronous commit, autovacuum ฯลฯ ก็ปิดได้เช่นกัน แน่นอนว่า Redis ยังน่าจะเร็วกว่าอยู่ แต่ความต่างอาจไม่ได้มากพอให้บริษัททั่วไปต้องใส่ใจ
  • ถ้าพูดแก่นของบทความให้ชัดขึ้นคือ ให้ เริ่มด้วย Postgres แล้วค่อยย้ายไป Redis เมื่อมีความจำเป็น
    ควรรักษาจำนวนชิ้นส่วนที่เคลื่อนไหวให้มีน้อยที่สุดเท่าที่ทำได้

    • Redis ไม่ได้ deploy ยากเป็นพิเศษ จะเลือกใช้ Redis ตั้งแต่แรกเลยก็ได้ แทนที่จะวางแผนการเปลี่ยนผ่านที่ภายหลังอาจเกิดผลข้างเคียงไม่คาดคิด