4 คะแนน โดย GN⁺ 2024-10-21 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • PostgreSQL กลายเป็นเหมือนตัวเลือกเริ่มต้นของอินเทอร์เน็ตในช่วงหลัง แต่จากประสบการณ์ของ Carnegie Mellon และ OtterTune การใช้งาน MVCC ของมันมีภาระด้านการปฏิบัติการมากกว่า MySQL, Oracle และ Microsoft SQL Server
  • แกนหลักของการออกแบบคือ การจัดเก็บแบบ append-only ที่เมื่ออัปเดตแถวจะไม่เขียนทับแถวเดิม แต่สร้างเวอร์ชันทางกายภาพใหม่ และมีเชน O2N ที่ไล่ตามจากเวอร์ชันเก่าไปยังเวอร์ชันใหม่
  • โครงสร้างนี้นำไปสู่การคัดลอก tuple ทั้งก้อน การสะสมของ dead tuple การอัปเดตทุกอินเด็กซ์ และการพึ่งพา autovacuum โดยจะหลีกเลี่ยงต้นทุนบางส่วนได้เฉพาะกรณีที่ทำ HOT update ได้เท่านั้น
  • จากการวิเคราะห์ PostgreSQL ของลูกค้า OtterTune พบว่าโดยเฉลี่ยมีเพียงประมาณ 46% ของการอัปเดต ที่ใช้การปรับแต่ง HOT และค่า autovacuum เริ่มต้นอาจทำให้การเก็บกวาดบนตารางขนาดใหญ่ล่าช้าไปนาน
  • PostgreSQL ยังคงเป็น DBMS ที่น่าสนใจ แต่สำหรับเวิร์กโหลดที่เขียนหนักและตารางขนาดใหญ่ ผู้ใช้ต้องจัดการพื้นที่จัดเก็บ, I/O, หน่วยความจำ, การบำรุงรักษาอินเด็กซ์ และการดำเนินงาน vacuum เอง

เหตุใด PostgreSQL MVCC จึงเป็นปัญหา

  • ตัวเลือกฐานข้อมูลมีมากมายจน ณ เดือนเมษายน 2023 มีการลงทะเบียนไว้ใน DBDB ถึง 897 รายการ แต่ในแต่ละยุคก็มักมีตัวเลือกที่ถือเป็นค่าเริ่มต้นโดยพฤตินัย
    • ในทศวรรษ 2000 MySQL ที่ Google และ Facebook ใช้เป็นตัวเลือกตามธรรมเนียม
    • ในทศวรรษ 2010 MongoDB ได้รับความสนใจพร้อมภาพลักษณ์ “webscale”
    • ในช่วง 5 ปีหลัง PostgreSQL ได้รับความนิยมอย่างกว้างขวางเพราะความเสถียร ฟีเจอร์ที่ครบถ้วน ความสามารถในการขยาย และความเหมาะสมกับเวิร์กโหลดเชิงปฏิบัติการ
  • จุดโฟกัสของบทความคือการใช้งาน multi-version concurrency control (MVCC) ของ PostgreSQL
    • จาก งานวิจัย ของ Carnegie Mellon และประสบการณ์ปรับแต่ง Amazon RDS PostgreSQL การใช้งาน MVCC ของ PostgreSQL ถูกประเมินว่าแย่กว่า MySQL, Oracle และ Microsoft SQL Server
    • Amazon Aurora PostgreSQL ก็มีปัญหาเชิงโครงสร้างแบบเดียวกัน

เป้าหมายของ MVCC และทางเลือกการออกแบบของ PostgreSQL

  • MVCC เป็นวิธีที่ทำให้หลายคิวรีอ่านและเขียนฐานข้อมูลพร้อมกันได้โดยรบกวนกันให้น้อยที่สุด
    • DBMS จะไม่เขียนทับแถวเดิม แต่คงเวอร์ชันทางกายภาพหลายเวอร์ชันไว้สำหรับแต่ละแถวเชิงตรรกะ
    • คิวรีจะอ่านเวอร์ชันที่เหมาะกับตนตามลำดับเวอร์ชัน เช่น เวลาที่สร้าง
    • ทำให้เกิด snapshot isolation ที่มองเห็นสแนปช็อตของฐานข้อมูล ณ เวลาที่เริ่มทรานแซกชันได้
  • งานอ่านสามารถลดสถานการณ์ที่ถูกบล็อกด้วย record lock แบบชัดเจนจากงานเขียนที่อัปเดตรายการเดียวกัน
  • MVCC DBMS ต้องตัดสินใจหลัก ๆ สามอย่าง
    • จะจัดเก็บการอัปเดตของแถวเดิมอย่างไร
    • จะหาเวอร์ชันของแถวที่เหมาะกับคิวรี ณ เวลารันอย่างไร
    • จะลบเวอร์ชันหมดอายุที่มองไม่เห็นอีกต่อไปอย่างไร
  • PostgreSQL ยังแบกรับภาระในสองด้านหลังมาจนถึงวันนี้ เพราะตัวเลือกแรกที่เลือกไว้ตั้งแต่ทศวรรษ 1980

การจัดเก็บเวอร์ชันแบบ append-only

  • PostgreSQL ถูกออกแบบตั้งแต่แรกให้รองรับหลายเวอร์ชัน และเมื่อมีการแก้ไขจะไม่เขียนทับแถวเดิม แต่สร้างสำเนาแล้วนำการเปลี่ยนแปลงไปใช้กับเวอร์ชันใหม่
  • วิธีนี้มองได้ว่าเป็น การจัดเก็บเวอร์ชันแบบ append-only
    • เมื่ออัปเดต tuple เดิม DBMS จะจองสล็อตว่างในตารางสำหรับเวอร์ชันแถวใหม่
    • คัดลอกเนื้อหาของแถวเวอร์ชันปัจจุบันไปยังเวอร์ชันใหม่
    • นำการเปลี่ยนแปลงไปใช้กับสล็อตเวอร์ชันที่เพิ่งจัดสรร
  • ตารางตัวอย่างคือ movies ซึ่งเก็บข้อมูลภาพยนตร์
    • id เป็นคีย์หลัก และมีคอลัมน์ name, year, director
    • มีอินเด็กซ์หลัก movies_pkey และอินเด็กซ์เสริมแบบ B+Tree คือ idx_name, idx_director
  • ในการอัปเดตที่เปลี่ยนปีเข้าฉายของ "Shaolin and Wu Tang" จากปี 1985 เป็น 1983 จะคัดลอก tuple เดิมก่อน แล้วนำปีที่เปลี่ยนไปใช้กับเวอร์ชันใหม่
    • หากหน้าเดิมไม่มีพื้นที่ เวอร์ชันใหม่อาจถูกสร้างในหน้าตารางอื่น

เชนเวอร์ชันและวิธี O2N

  • เมื่อหลายเวอร์ชันทางกายภาพแทนแถวเชิงตรรกะแถวเดียวกัน DBMS ต้องบันทึกความสัมพันธ์ระหว่างเวอร์ชัน
    • MVCC DBMS สร้าง version chain เป็นลิงก์ลิสต์ทางเดียว
    • เชนเชื่อมต่อทางเดียวเท่านั้นเพื่อลดต้นทุนด้านพื้นที่จัดเก็บและการบำรุงรักษา
  • ลำดับของ version chain มีสองแบบ
    • N2O: เวอร์ชันล่าสุดชี้ไปยังเวอร์ชันก่อนหน้า และหัวเชนเป็นเวอร์ชันล่าสุดเสมอ
    • O2N: แต่ละเวอร์ชันชี้ไปยังเวอร์ชันใหม่ และหัวเชนเป็นเวอร์ชันเก่าที่สุด
  • DBMS ส่วนใหญ่รวมถึง Oracle และ MySQL ใช้ N2O
  • PostgreSQL ใช้ O2N ซึ่งพบไม่บ่อย ยกเว้นเอนจิน In-Memory OLTP ของ Microsoft SQL Server
    • O2N สามารถลดความจำเป็นในการอัปเดตอินเด็กซ์ให้ชี้ไปยังเวอร์ชันใหม่ทุกครั้งที่ tuple ถูกแก้ไข
    • แต่ในทางกลับกัน อาจต้องไล่ตาม version chain ที่ยาวเพื่อหาเวอร์ชันล่าสุด
  • ฟิลด์ t_tcid ใน header ของแถว PostgreSQL เก็บ tuple ID ของเวอร์ชันถัดไป หรือถ้าเป็นเวอร์ชันล่าสุดจะเก็บ tuple ID ของตัวเอง
    • หากอินเด็กซ์ชี้ไปยังเวอร์ชันเก่า PostgreSQL ต้องไล่ตามเชนเพื่อหาเวอร์ชันใหม่

วิธีลดการไล่เชนด้วยอินเด็กซ์

  • นักพัฒนา PostgreSQL รู้ถึงต้นทุนสองอย่างมาตั้งแต่ช่วงแรก
    • ต้นทุนการสร้างสำเนาใหม่ของ tuple ทั้งก้อนทุกครั้งที่อัปเดตนั้นสูง
    • การที่คิวรีส่วนใหญ่ต้องสำรวจ version chain ทั้งหมดเพื่อหาเวอร์ชันล่าสุดที่ต้องการเป็นความสูญเปล่า
  • PostgreSQL เพิ่มรายการอินเด็กซ์ของตารางให้กับแต่ละเวอร์ชันทางกายภาพของแถว เพื่อหลีกเลี่ยงการไล่เชนยาว
    • หากแถวเชิงตรรกะหนึ่งแถวมีเวอร์ชันทางกายภาพ 5 เวอร์ชัน อาจมีรายการอินเด็กซ์สำหรับ tuple นั้นได้สูงสุด 5 รายการ
    • หากอินเด็กซ์ idx_name ชี้ไปยังหลายเวอร์ชันของ "Shaolin and Wu Tang" PostgreSQL ก็สามารถเข้าถึงเวอร์ชันล่าสุดได้โดยตรง
  • การเข้าถึงเวอร์ชันล่าสุดอาจเร็วขึ้น แต่อินเด็กซ์จะใหญ่ขึ้นและต้นทุนการบำรุงรักษาเพิ่มขึ้น

การปรับแต่ง HOT update

  • PostgreSQL ใช้ HOT (heap-only tuple) update เพื่อลดกรณีที่เวอร์ชันที่เกี่ยวข้องกระจัดกระจายไปหลายหน้าและเกิดรายการอินเด็กซ์หลายรายการ
  • เงื่อนไขที่ทำ HOT update ได้มีสองข้อ
    • การอัปเดตไม่แก้ไขคอลัมน์ที่ถูกอ้างอิงในอินเด็กซ์ของตาราง
    • เวอร์ชันใหม่มีพื้นที่พอที่จะจัดเก็บใน data page เดียวกับเวอร์ชันก่อนหน้า
  • เมื่อใช้ HOT อินเด็กซ์ยังคงชี้ไปยังเวอร์ชันเก่า และคิวรีจะไล่ตาม version chain เพื่อหาเวอร์ชันล่าสุด
  • PostgreSQL ยังทำการปรับแต่งระหว่างการทำงานปกติเพื่อตัดแต่ง version chain โดยลบเวอร์ชันเก่าออก

การลบ dead tuple และ vacuum

  • เนื่องจาก PostgreSQL สร้างสำเนาแถวทุกครั้งที่อัปเดต จึงต้องลบเวอร์ชันเก่าที่เป็น dead tuple
  • PostgreSQL ยุคแรกในทศวรรษ 1980 ไม่ได้ลบ dead tuple
    • แนวคิดคือหากคงเวอร์ชันเก่าไว้ ก็จะรันคิวรีแบบ time-travel เพื่อดูสถานะฐานข้อมูล ณ จุดเวลาเฉพาะได้
    • แต่หากไม่ลบ dead tuple ขนาดตารางจะไม่ลดลงแม้มีการลบ และ version chain ของ tuple ที่ถูกอัปเดตบ่อยจะยาวขึ้น
  • PostgreSQL เก็บกวาด dead tuple จากตารางด้วยกระบวนการ vacuum
    • vacuum จะสแกนตามลำดับหน้าตารางที่ถูกแก้ไขตั้งแต่การรันครั้งล่าสุด และค้นหาเวอร์ชันที่หมดอายุ
    • หากเวอร์ชันใดไม่ปรากฏต่อทรานแซกชันที่ยังทำงานอยู่ จะถือว่า expired
    • เนื่องจากทรานแซกชันปัจจุบันไม่เข้าถึงเวอร์ชันนั้น และทรานแซกชันในอนาคตจะใช้เวอร์ชัน live ล่าสุด จึงนำพื้นที่กลับมาใช้ซ้ำได้อย่างปลอดภัย
  • PostgreSQL สามารถรัน autovacuum อัตโนมัติเป็นระยะตามการตั้งค่า
    • ปรับความถี่ vacuum ของทั้งตารางได้ด้วยการตั้งค่าระดับ global
    • ตั้งค่า autovacuum แยกรายตารางได้ด้วย
    • ผู้ใช้สามารถรันเองด้วยคำสั่ง SQL VACUUM

ปัญหา 1: การคัดลอก tuple ทั้งก้อน

  • ใน append-only MVCC แม้คอลัมน์เดียวของ tuple จะเปลี่ยน ก็ต้องคัดลอกทุกคอลัมน์ไปยังเวอร์ชันใหม่
  • วิธีนี้เพิ่ม การซ้ำซ้อนของข้อมูล และความต้องการพื้นที่จัดเก็บอย่างมาก
    • PostgreSQL อาจต้องใช้หน่วยความจำและดิสก์มากกว่า DBMS อื่นเพื่อเก็บฐานข้อมูลเดียวกัน
    • ผลคือคิวรีอาจช้าลงและค่าใช้จ่ายคลาวด์อาจเพิ่มขึ้น
  • MySQL และ Oracle เก็บ delta ที่บีบอัดระหว่างเวอร์ชันใหม่กับเวอร์ชันปัจจุบัน
    • หากในตารางที่มี 1,000 คอลัมน์มีเพียงคอลัมน์เดียวเปลี่ยน ก็จะเก็บเฉพาะ delta record ของคอลัมน์ที่เปลี่ยน
    • PostgreSQL สร้างเวอร์ชันใหม่ที่มีทั้งคอลัมน์ที่เปลี่ยน 1 คอลัมน์และคอลัมน์ที่ไม่เปลี่ยนอีก 999 คอลัมน์
  • คุณสมบัติ TOAST ของ PostgreSQL ถูกจัดการต่างกัน จึงไม่นำมารวมในการเปรียบเทียบนี้
  • EnterpriseDB เริ่ม โครงการ zheap ในปี 2013 เพื่อเปลี่ยน storage engine แบบ append-only ให้เป็นแบบอิง delta version
    • อัปเดตอย่างเป็นทางการล่าสุดคือ บทความสถานะ ในปี 2021
    • หลังจากนั้นยังไม่เห็นความคืบหน้าชัดเจน

ปัญหา 2: table bloat

  • เวอร์ชันหมดอายุของ PostgreSQL หรือ dead tuple ใช้พื้นที่มากกว่า delta version
  • ในเวิร์กโหลดที่เขียนหนัก dead tuple อาจสะสมเร็วกว่า autovacuum จะตามทัน
    • ตารางอาจโตขึ้นเรื่อย ๆ
    • dead tuple และ live tuple ปะปนอยู่ในหน้าเดียวกัน ทำให้ระหว่างรันคิวรีต้องโหลด dead tuple เข้าหน่วยความจำด้วย
    • bloat ที่ไม่ถูกควบคุมทำให้ table scan ใช้ IOPS และหน่วยความจำมากขึ้น ส่งผลให้ประสิทธิภาพคิวรีลดลง
    • หาก dead tuple ทำให้สถิติของ optimizer ไม่แม่นยำ อาจนำไปสู่แผนคิวรีที่ไม่ดี
  • ตัวอย่างเช่น หากตาราง movies มี live tuple 10 ล้านรายการและ dead tuple 40 ล้านรายการ แปลว่า 80% เป็นข้อมูล obsolete
    • หากขนาด tuple เฉลี่ย 1KB live tuple จะใช้ 10GB และ dead tuple จะใช้ประมาณ 40GB
    • ขนาดตารางทั้งหมดคือ 50GB
    • เมื่อสแกนทั้งตาราง PostgreSQL ต้องอ่าน 50GB จากดิสก์และนำขึ้นหน่วยความจำ แม้ส่วนใหญ่เป็นข้อมูล obsolete
  • PostgreSQL มีกลไกป้องกันไม่ให้ sequential scan ทำให้ buffer pool cache ปนเปื้อน แต่ไม่ได้กำจัดต้นทุน I/O เอง

ความแตกต่างระหว่าง VACUUM และ VACUUM FULL

  • แม้ autovacuum จะรันเป็นประจำและตามเวิร์กโหลดทัน แต่ autovacuum ปกติไม่สามารถคืนพื้นที่จัดเก็บให้ระบบปฏิบัติการได้
  • VACUUM ปกติจะลบ dead tuple และจัดตำแหน่ง live tuple ใหม่ภายในแต่ละหน้า แต่ไม่ยึดคืนหน้าว่างบนดิสก์
  • หาก PostgreSQL สามารถทำให้หน้าสุดท้ายว่างได้ ก็อาจ truncate หน้านั้นได้ แต่หน้าอื่น ๆ จะยังคงอยู่บนดิสก์
    • แม้ลบ dead tuple 40GB ออกจากตาราง 50GB แล้ว PostgreSQL ก็ยังอาจคงพื้นที่จัดสรร 50GB จากมุมมองของระบบปฏิบัติการหรือ RDS
  • หากต้องการคืนพื้นที่ที่ไม่ได้ใช้จริง ๆ ต้องเขียนทั้งตารางใหม่ลงพื้นที่ใหม่ด้วย VACUUM FULL หรือ pg_repack
    • ทั้งสองงานใช้ทรัพยากรมากและใช้เวลานาน
    • ในฐานข้อมูล production อาจกระทบประสิทธิภาพคิวรีอย่างมาก
  • VACUUM FULL จะลบ dead tuple ในแต่ละหน้า บีบอัด live tuple ที่เหลือแล้วย้ายไปยังหน้าใหม่ จากนั้นลบหน้าที่ไม่จำเป็น

ปัญหา 3: การบำรุงรักษาอินเด็กซ์เสริม

  • PostgreSQL ต้องอัปเดตทุกอินเด็กซ์ของตารางนั้นเมื่ออัปเดต tuple หนึ่งรายการ
  • เพราะทั้งอินเด็กซ์หลักและอินเด็กซ์เสริมเก็บ ตำแหน่งทางกายภาพ ที่แน่นอนของเวอร์ชัน
    • งานนี้จำเป็นทุกครั้งที่อัปเดต เว้นแต่จะเป็น HOT update ที่เวอร์ชันใหม่ถูกเก็บในหน้าเดียวกับเวอร์ชันก่อนหน้า
  • ในตัวอย่างการอัปเดต PostgreSQL สร้างเวอร์ชันใหม่ใน Table Page #2 แล้วแทรกรายการที่ชี้ไปยังเวอร์ชันใหม่ใน movies_pkey, idx_director, idx_name
  • โครงสร้างที่ต้องแก้ไขทุกอินเด็กซ์สร้างต้นทุนด้านประสิทธิภาพหลายอย่าง
    • คิวรีอัปเดตช้าลง
    • เกิด I/O เพิ่มเติมเพื่อค้นหาแต่ละอินเด็กซ์และแทรกรายการใหม่
    • เกิดการแข่งขัน lock/latch ในโครงสร้างภายใน เช่น ตารางหน้าของอินเด็กซ์และ buffer pool
    • ต้องจ่ายต้นทุนการบำรุงรักษาแม้กับอินเด็กซ์ที่คิวรีจริงไม่ได้ใช้
    • ใน DBMS ที่คิดค่าบริการตาม IOPS เช่น Amazon Aurora การอ่านและเขียนเพิ่มเติมเป็นปัญหา
  • จากการวิเคราะห์ฐานข้อมูล PostgreSQL ของลูกค้า OtterTune พบว่าโดยเฉลี่ยประมาณ 46% ของการอัปเดต ใช้การปรับแต่ง HOT
    • การอัปเดตที่เหลือมากกว่า 50% ต้องรับต้นทุนการบำรุงรักษาอินเด็กซ์
  • บทความของ Uber ในปี 2016 เรื่องการย้ายจาก Postgres ไป MySQL เป็นกรณีตัวอย่างสำคัญที่แสดงปัญหานี้
    • เวิร์กโหลดที่เขียนหนักบนตารางที่มีอินเด็กซ์เสริมจำนวนมากประสบปัญหาประสิทธิภาพอย่างมาก
  • Oracle และ MySQL ไม่มีปัญหาเดียวกัน เพราะอินเด็กซ์เสริมไม่ได้เก็บที่อยู่ทางกายภาพของเวอร์ชันใหม่
    • อินเด็กซ์เสริมเก็บตัวระบุเชิงตรรกะ เช่น tuple ID หรือ primary key
    • DBMS ใช้ตัวระบุเชิงตรรกะนี้หา address ทางกายภาพของเวอร์ชันปัจจุบัน
    • การอ่านผ่านอินเด็กซ์เสริมอาจช้าลง แต่ก็ลด overhead ได้ด้วยข้อดีอื่นของการใช้งาน MVCC

ปัญหา 4: ความยากในการดำเนินงาน autovacuum

  • ประสิทธิภาพของ PostgreSQL พึ่งพา ประสิทธิผลของ autovacuum อย่างมากในการลบข้อมูล obsolete และทำให้พื้นที่กลับมาใช้ซ้ำได้
  • RDS, Aurora และ Aurora Serverless ล้วนเป็น PostgreSQL variants จึงมีปัญหา autovacuum แบบเดียวกัน
  • autovacuum ซับซ้อน ทำให้รันในสภาพที่เหมาะสมได้ยาก
    • ค่าเริ่มต้นไม่เหมาะกับทุกตาราง
    • โดยเฉพาะกับตารางขนาดใหญ่ ปัญหาจะยิ่งรุนแรง
  • ค่าเริ่มต้นของ autovacuum_vacuum_scale_factor คือ 20%
    • ในตารางที่มี 100 ล้าน tuple ต้องมีอย่างน้อย 20 ล้าน tuple ถูกอัปเดตก่อน autovacuum จึงจะถูกทริกเกอร์
    • ด้วยเหตุนี้ dead tuple จำนวนมากอาจค้างอยู่ในตารางเป็นเวลานาน ก่อให้เกิดต้นทุน I/O และหน่วยความจำ
  • long-running transaction อาจขัดขวาง autovacuum ได้
    • หากการเก็บกวาด expired version ล่าช้า dead tuple และสถิติเก่าจะสะสม
    • อาจเกิดวงจรเลวร้ายที่ปัญหาประสิทธิภาพสร้าง long-running transaction เพิ่ม และทรานแซกชันเหล่านั้นก็ไปขัดขวาง autovacuum อีก
    • ในกรณีเช่นนี้ คนอาจต้องยุติ long-running transaction ด้วยตนเอง

กรณีลูกค้า OtterTune

  • ในฐานข้อมูล PostgreSQL Amazon RDS หนึ่งแห่ง จำนวน dead tuple เปลี่ยนเป็นรูปฟันเลื่อยตลอด 2 สัปดาห์
    • autovacuum ทำการเก็บกวาดครั้งใหญ่ประมาณวันละครั้ง
    • วันที่ 14 กุมภาพันธ์ DBMS เก็บกวาด dead tuple ได้ 3.2 ล้านรายการ
    • เมื่อดูทั้งกราฟ จำนวน dead tuple มีแนวโน้มเพิ่มขึ้น และเป็นสถานะผิดปกติที่ autovacuum ตามไม่ทัน
  • ในอินสแตนซ์ PostgreSQL RDS หนึ่งของลูกค้า OtterTune หลังจาก bulk insertion เกิด long-running query เพราะสถิติที่ล้าสมัย
    • คิวรีนี้ขัดขวางการอัปเดตสถิติของ autovacuum
    • ส่งผลให้เกิด long-running query เพิ่มขึ้น
    • health check อัตโนมัติของ OtterTune ระบุปัญหาได้ แต่ผู้ดูแลต้องยุติคิวรีด้วยตนเองและรัน ANALYZE หลัง bulk insertion
    • เวลาในการรันของ long query ดังกล่าวลดจาก 52 นาทีเหลือ 34 วินาที

ข้อสรุปเชิงปฏิบัติ

  • การออกแบบ DBMS มีทางเลือกที่ยากเสมอ และแต่ละทางเลือกทำให้ประสิทธิภาพแตกต่างกันตามเวิร์กโหลด
  • สำหรับเวิร์กโหลดแบบ write-intensive เฉพาะของ Uber index write amplification จาก MVCC ของ PostgreSQL เป็นเหตุผลในการย้ายไป MySQL
  • การใช้งาน MVCC ของ PostgreSQL ถูกประเมินว่าเป็นแนวทางที่ไม่ควรทำตามหากจะสร้าง MVCC DBMS ใหม่
    • ปัญหาหลักคือการผสมกันของการจัดเก็บแบบ append-only และ autovacuum
    • การออกแบบนี้เป็นมรดกจากทศวรรษ 1980 และเป็นวิธีก่อนที่แพตเทิร์นระบบแบบ log-structured จะแพร่หลายหลังทศวรรษ 1990
  • PostgreSQL ยังคงเป็น DBMS ที่ได้รับความนิยม แต่ต้องยอมรับและดำเนินงานพร้อมจุดอ่อนของ MVCC
  • ทางเลี่ยงคือใช้เวลาและความพยายามจำนวนมากในการจูนด้วยตนเอง

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

 
GN⁺ 2024-10-21
ความคิดเห็นใน Hacker News
  • คิดว่าตัวเองรู้ภายในของ Postgres พอสมควรแล้ว แต่บทความนี้ยอดเยี่ยมมากและได้เรียนรู้อีกเยอะ
    หนึ่งในจุดอ่อนพื้นฐานดูเหมือนจะเป็นการที่ Postgres เลือกใช้การติดตามเวอร์ชันของแถวแบบ O2N แทนที่จะเป็น N2O แม้ว่าการเปลี่ยนเป็น N2O จะไม่แก้ทุกปัญหา เช่น ปัญหาการเก็บสำเนาทั้งแถวก็ยังคงอยู่ แต่ถ้ามองแบบ 80/20 ก็น่าจะลดข้อเสียของการติดตั้งปัจจุบันได้เกือบทั้งหมด
    ธุรกรรมส่วนใหญ่น่าจะต้องการเวอร์ชันล่าสุดของแถว ดังนั้นถ้าใช้ลำดับแบบ N2O ก็จะต้องไล่ตาม linked list เฉพาะตอนที่ต้องการเวอร์ชันเก่าเท่านั้น และก็น่าจะไม่จำเป็นต้องเก็บแต่ละเวอร์ชันของแถวไว้ในดัชนี

    • แนะนำให้ดูคลาส YouTube ของ Andy ชื่อ History of Databases (CMU Databases / Spring 2020)
      คาบแรกทั้งคาบเขาสอนอยู่บนถนนในอัมสเตอร์ดัมเพราะเข้าโรงแรมไม่ได้ คาแรกเตอร์ก็น่าสนใจและอธิบายการทำงานภายในได้เก่งมาก
  • ข้อดีใหญ่อย่างหนึ่งคือเมื่อภาระงานประกอบด้วย INSERT เป็นหลักและตามด้วยการลบตารางในภายหลัง จะไม่ต้องใช้พื้นที่เพิ่ม
    โดยทั่วไปก็ไม่จำเป็นต้องซอยธุรกรรมการแทรกด้วย เพราะแทบไม่มีข้อจำกัดจริงจังเรื่องขนาดข้อมูลที่สร้างขึ้นหรือจำนวนแถวรวมที่เปลี่ยนแปลง แม้จะมีข้อจำกัดเรื่องจำนวน statement ในธุรกรรม แต่ถ้าไม่ได้แก้ตารางถี่เกินไปก็อ้อมไปใช้ COPY FROM ได้
    ในมุมมองของ DBA ก็ไม่ต้องจัดการพื้นที่ rollback/undo แยกจากพื้นที่เก็บตาราง ทั้งนี้ขึ้นกับแอปพลิเคชัน แต่การออกแบบของ PostgreSQL ก็ไม่ได้แพ้ไปทุกด้าน ไม่ได้เป็นอะไรแบบ bubble sort

    • ตอนทำเกมช่วงต้นยุค 2000 ก็ได้เรียนรู้ว่า bubble sort ไม่ได้แพ้ทุกด้านเหมือนกัน
      มันทำงานได้ดีเมื่อรายการเกือบเรียงอยู่แล้ว และในการเรนเดอร์ 3D เวลาจัดเรียงวัตถุตามระยะจากกล้องก็มักเป็นแบบนั้น ถ้าขยับหรือหมุนกล้องเพียงเล็กน้อย การจัดเรียงใหม่โดยอิงลำดับจากเฟรมก่อนหน้า bubble sort จะทำงานได้ดีมาก
      ถ้าอยากเลี่ยงกรณีแย่ที่สุด ก็ให้นับจำนวนการเปรียบเทียบที่ล้มเหลวในรอบล่าสุดกับจำนวนรอบที่ทำไปทั้งหมด แล้วถ้าเกินค่า threshold ก็สลับไปใช้อัลกอริทึมเรียงลำดับตัวอื่น
    • Bubble sort ยอดเยี่ยมกับฮาร์ดแวร์บางแบบหรือกับชุดข้อมูลที่เกือบเรียงอยู่แล้ว
    • ในบทความบอกตรง ๆ ว่าการออกแบบ MVCC ของ pg เป็นสไตล์ยุค 90 และทุกวันนี้ไม่มีใครทำแบบนั้นแล้ว
      มันเป็นเทคโนโลยีเก่าเกิน 30 ปี ดังนั้นถึงจะไม่ได้แพ้ทุกด้าน แต่ในด้านที่สำคัญที่สุดก็มองว่าแพ้
  • คิดว่าบทความนี้ผิดโดยเฉพาะตรงส่วนต่อไปนี้
    “ในยุค 2000 ความเชื่อกระแสหลักเลือก MySQL เพราะเหล่าสตาร์ทอัปเทคโนโลยีขาขึ้นอย่าง Google และ Facebook ใช้มัน ในยุค 2010 คือ MongoDB ที่กลายเป็น ‘webscale’ ด้วยการเขียนแบบไม่คงทน ตลอด 5 ปีที่ผ่านมา PostgreSQL ได้กลายเป็น DBMS ที่อินเทอร์เน็ตรัก และก็มีเหตุผลที่ดี! มันเชื่อถือได้ ฟีเจอร์ครบ ขยายได้ และเหมาะกับภาระงาน production ส่วนใหญ่”
    วิศวกรฉลาด ๆ เลือก Postgres ไม่ใช่เพราะตรรกะผิดแบบอ้างความนิยม แต่เพราะ ความปลอดภัยของข้อมูล, ACID, ความคล้ายกับ Oracle, MVCC, การทำตามมาตรฐาน SQL, ทีม Postgres, ชุมชนที่ยอดเยี่ยมและช่วยเหลือดี, data types, ประสิทธิภาพสูง และความยืดหยุ่นของ BSD
    นี่ก็เป็นเหตุผลที่ ATT เลือก Postgres ในช่วงต้นยุค 2000 และ Oracle DBA ก็ยอมรับการย้ายมาใช้งานได้อย่างง่ายดายมาก ระหว่างที่ MySQL ผ่านการเปลี่ยนผ่านอันวุ่นวาย PG ก็ยังแข็งแกร่งขึ้นและพัฒนาต่อเนื่อง คิดว่า Bruce Momjian มีส่วนอย่างมากกับความสำเร็จนี้ และชุมชนก็ยอดเยี่ยมจริง ๆ

    • คล้ายกันคือ ในปี 2005 ความชอบเปลี่ยนจาก MySQL มาเป็น PostgreSQL
      เพราะอยากใช้ database views เพื่อสร้างชั้นความเข้ากันได้แบบ “live” ระหว่าง สคีมาฐานข้อมูล AS400 แบบเก่ากับแอป Rails สมัยใหม่
      หลังจากนั้นความชอบก็ยิ่งมากขึ้นจากเรื่องอย่างความปลอดภัยของข้อมูล และ DDL ภายในธุรกรรม
  • “MySQL และ Oracle เก็บเดลตาที่บีบอัดระหว่างเวอร์ชันใหม่กับเวอร์ชันปัจจุบัน (ให้นึกเหมือน git diff)”
    Git ขึ้นชื่ออยู่แล้วว่าไม่ได้เก็บ diff แต่เก็บ อ็อบเจ็กต์ใหม่และอ็อบเจ็กต์เก่าทั้งก้อน เหมือนวิธีที่ Postgres ใช้ไม่ใช่หรือ?

    • ประโยคที่ยกมานั้นไม่ได้บอกว่า Git เก็บ diff แต่บอกว่าสิ่งที่ MySQL และ Oracle เก็บ คล้ายกับ git diff
    • ใช่ แต่ละเวอร์ชันของไฟล์คือ blob แยกกัน
      มันมีการบีบอัดด้วย packing เพื่อให้ clone ได้เร็วขึ้น แต่รูปแบบดิบที่ Git จัดการก็คือ blob พวกนี้
    • สิ่งที่เปรียบเทียบไม่ใช่วิธีเก็บของ Git แต่เป็นการเปรียบเปรยว่าวิธีเก็บของ MySQL และ Oracle คล้ายฟอร์แมตของ git diff
      Git repository ก็มีการบีบอัด และการบีบอัดนั้นก็อิง diff อยู่บ้าง แต่ไม่ใช่อิงประวัติ commit แบบตรงไปตรงมาอย่างที่คนอาจคาดกัน
    • มีคนอื่นพูดถึง “git diffs” แต่จริง ๆ แล้ว Git ใช้ delta เป็นการเพิ่มประสิทธิภาพระดับล่างใน pack file ซึ่งก็คล้ายกับการเปรียบเทียบกับ MySQL
      ใน SQL query ก็ไม่ได้ส่งคืน diff ตรง ๆ อยู่แล้ว
    • Git diff ถูกสร้างขึ้นตอนใช้งาน แต่ diff ก็ยังเป็น diff อยู่ดี
  • “ความจำเป็นที่ PostgreSQL ต้องแก้ไขดัชนีทั้งหมดของตารางทุกครั้งที่มีการอัปเดต ก่อให้เกิดผลกระทบด้านประสิทธิภาพหลายอย่าง ตามปกติแล้วระบบต้องทำงานมากขึ้น ดังนั้นคำสั่ง update จึงช้าลง”
    สงสัยเรื่อง write amplification นี้อยู่เหมือนกัน MySQL อาจไม่ต้องอัปเดตดัชนีแบบนั้นจริง แต่การ replication ของ MySQL พึ่งพา binlog และทุกการเปลี่ยนแปลงต้องถูกบันทึกเพิ่มอีกชั้น นอกเหนือจากที่ฐานข้อมูลเองบันทึกอยู่แล้ว (เช่น InnoDB redo log)
    เพราะงั้น MySQL ที่ใช้ในคลัสเตอร์ก็ดูเหมือนจะมี write amplification อีกแบบหนึ่ง ส่วน PostgreSQL นำ WAL มาใช้ซ้ำสำหรับ replication จึงไม่มี amplification แบบนั้น
    แถมฝั่งผู้รับ MySQL ยังต้องเขียน binlog ที่เข้ามาลง relay log ก่อน แล้วให้ applier thread มาอ่านต่อ ซึ่งก่อให้เกิดการเขียน InnoDB เพิ่มและโดยค่าเริ่มต้นยังสร้าง binlog เพิ่มอีกด้วย

  • หัวข้อนี้คุยโดยไม่พูดถึง ดิสก์ ไม่ได้
    SSD เขียนทีละหน้า 4KB กล่าวคือ ต่อให้อัปเดตแค่ 1 บิต ดิสก์ก็ต้องอ่าน 4KB เปลี่ยนบิต แล้วเขียนหน้า 4KB กลับไปยังสล็อตใหม่อีกครั้ง ดังนั้นบทลงโทษจากการคัดลอกจึงแตกต่างกันไปตามชนิดของดิสก์

    • น่าสนใจ อยากรู้ว่าสิ่งนี้สะท้อนอยู่ใน การคิดค่าบริการของ AWS อย่างไร
      AWS คิดค่าบริการแบบเหมาจ่ายตาม MBps ของ IO แต่ไม่แน่ใจว่ามีกฎปัดขึ้นเป็น 4KB ที่ใกล้ที่สุดหรือไม่ หรือเขาติดตามปริมาณการเขียนของไดรฟ์เองแทนที่จะดูจากปริมาณคำขอ แล้วคิดเงินตามปริมาณ IO จริงในระดับ implementation ของสตอเรจ
    • หน้าใน Postgres มีขนาด 8KB ดังนั้นประเด็นนั้นจึงไม่ได้สำคัญมากนัก
  • “Oracle และ MySQL ไม่มีปัญหานี้ในการทำ MVCC เพราะดัชนีรองไม่ได้เก็บที่อยู่จริงของเวอร์ชันใหม่ แต่เก็บตัวระบุเชิงตรรกะ (เช่น tuple ID, primary key) และ DBMS จะใช้สิ่งนั้นเพื่อหา physical address ของเวอร์ชันปัจจุบัน ด้วยเหตุนี้การอ่านดัชนีรองอาจช้าลงได้ แต่ DBMS เหล่านี้ก็มีข้อดีอื่นในเชิง implementation ของ MVCC ที่ช่วยลด overhead”
    มีพฤติกรรมที่น่าสนใจอย่างหนึ่งที่สังเกตได้ใน MySQL ฐานข้อมูลขนาดราว 500GB และสคีมามีลักษณะใกล้เคียงแบบ document-oriented มากกว่า relational โดย SELECT id WHERE something; UPDATE what WHERE id=id เร็วกว่าแบบคนละหลายหลัก เมื่อเทียบกับ UPDATE what WHERE something
    คิดว่าเหตุผลของพฤติกรรมนี้น่าจะอยู่ตรงนี้ แต่อย่างไรก็ตาม เวิร์กโหลดปกติไม่ได้ทำแบบนี้ และช้าก็เฉพาะ DML ชั่วคราวที่ใช้แก้ความไม่สอดคล้องกันเท่านั้น

    • SELECT เป็นงานแบบอ่านอย่างเดียว จึงทำงานแบบขนานได้
      ส่วน UPDATE มีการเขียนจริงและอาจล็อกตารางด้วย UPDATE id=id ทำให้ใช้ row-level locking ได้ แต่ก็มีความเสี่ยงว่าจะพลาดเรคอร์ดที่ถูกแทรกเข้ามาใหม่ระหว่าง SELECT กับ UPDATE
    • ผมดูแล Postgres หลายอินสแตนซ์ที่ขนาดเกิน 2TB และเน้นอ่านเป็นหลัก ซึ่งพวกนี้ก็มีลักษณะใกล้เคียง document-oriented เช่นกัน
      เห็นด้วยว่าการอัปเดตจำนวนมากอาจช้าเกินไป สุดท้ายจึงมักต้อง ค่อย ๆ ทำเป็นชุด หรือบางครั้งถึงขั้นใช้ COPY
  • “ในยุค 2010 มันคือ MongoDB เพราะมันเป็น ‘webscale’ ได้ด้วยการเขียนแบบไม่ทนทาน”
    แม้จะนอกประเด็น แต่ตั้งแต่ต้นจนจบมันคือ การตลาด: https://news.ycombinator.com/item?id=15124306

    • MongoDB ถูกออกแบบเป็นฐานข้อมูลแบบ DIY ที่ทำขึ้นโดยวิศวกรอดีต DoubleClick เพราะไม่มี DB ที่ตอบโจทย์ความต้องการของบริการอื่นได้พอดี
      เวอร์ชัน 4.2.8 (2020) ค่อนข้างแข็งแรงแล้ว และเป็นที่เข้าใจกันว่าไม่มี dirty write: https://en.wikipedia.org/wiki/MongoDB#Technical_criticisms
  • สิ่งที่เกลียดที่สุดคือแม้แต่ในปี 2024 ก็ยังต้องวาง connection pooler (เช่น pgbouncer) ไว้ข้างหน้าอยู่ดีถึงจะใช้งานได้ดี

  • OrioleDB พยายามแก้ปัญหานี้ด้วย storage engine ตัวใหม่: https://github.com/orioledb/orioledb

    • อยู่ในทีม Supabase
      ตอนนี้ Oriole เข้าร่วมกับ Supabase แล้ว และ Alexander กับทีมของเขากำลังทำงานกับมันแบบเต็มเวลา ชุดแพตช์อยู่ที่นี่: https://www.orioledb.com/docs#patch-set
      น่าจะได้ลองใช้บนแพลตฟอร์ม Supabase ภายในปลายปีนี้