PostgreSQL ใน Production: เข้าใจ MVCC, VACUUM, Bloat และการเฝ้าระวัง
PostgreSQL อาจช้าลงจาก dead tuples และ bloat ที่สะสมอยู่ภายใน table แม้ query จะไม่ได้เปลี่ยนไป บทความนี้อธิบายการทำงานของ MVCC, VACUUM, ANALYZE และสัญญาณสำคัญที่ควรเฝ้าระวังในระบบ production

PostgreSQL ใน Production: เข้าใจ MVCC, VACUUM, Bloat และการเฝ้าระวัง
เมื่อ PostgreSQL ในระบบ production เริ่มทำงานช้าลง เรามักสงสัย query หรือ index เป็นอันดับแรก แต่บางครั้งต้นเหตุไม่ได้อยู่ที่คำสั่ง SQL เพียงอย่างเดียว แต่อยู่ที่ข้อมูลเวอร์ชันเก่าซึ่งสะสมอยู่ภายใน table อย่างเงียบ ๆ
ระบบอาจยังอ่านและเขียนข้อมูลได้ตามปกติ ขณะที่ขนาดของ table, index และ backup เพิ่มขึ้นเรื่อย ๆ หากปล่อยไว้นาน ปัญหานี้อาจกลายเป็นทั้ง query ที่ช้าลง การใช้พื้นที่มากขึ้น และ disk เต็มโดยไม่มีเหตุการณ์ผิดปกติที่เห็นได้ชัดเจน
MVCC และจุดกำเนิดของ Dead Tuples
PostgreSQL ใช้แนวคิด Multi-Version Concurrency Control หรือ MVCC เพื่อให้หลาย transaction อ่านและแก้ไขข้อมูลพร้อมกันได้อย่างถูกต้อง เมื่อมีการ UPDATE ระบบมักสร้าง row version ใหม่แทนการเขียนทับข้อมูลเดิมทันที ส่วนการ DELETE ก็ยังไม่ลบข้อมูลทางกายภาพออกในทันที
ข้อมูลเวอร์ชันเก่าเหล่านี้ช่วยให้ transaction ซึ่งเริ่มทำงานก่อนหน้าเห็น snapshot ที่สอดคล้องกับช่วงเวลาของตัวเอง เมื่อไม่มี transaction ใดต้องใช้เวอร์ชันเก่าอีกแล้ว ข้อมูลเหล่านั้นจึงกลายเป็น dead tuples ที่สามารถนำพื้นที่กลับมาใช้ใหม่ได้
หากมี transaction เปิดค้างเป็นเวลานาน PostgreSQL อาจยังไม่สามารถจัดการ dead tuples บางส่วนได้ เพราะต้องรักษาข้อมูลไว้เผื่อ snapshot เก่ายังต้องอ่าน นี่คือเหตุผลที่ long-running transaction ไม่ได้สร้างปัญหาเฉพาะเรื่อง lock แต่ยังทำให้การทำความสะอาดข้อมูลตามหลังระบบได้ด้วย
เมื่อ Dead Tuples กลายเป็น Bloat
Dead tuples จำนวนหนึ่งถือเป็นเรื่องปกติของ PostgreSQL แต่หากระบบสร้างข้อมูลเก่าเร็วกว่าที่จะจัดการได้ table และ index จะค่อย ๆ ใช้พื้นที่มากกว่าข้อมูลจริง อาการสะสมนี้เรียกว่า bloat
เมื่อเกิด bloat ระบบอาจต้องอ่าน page มากขึ้นเพื่อค้นหาข้อมูลจำนวนเท่าเดิม ข้อมูลที่ไม่จำเป็นยังแย่งพื้นที่ cache เพิ่มภาระ I/O และทำให้การสำรองข้อมูลใช้เวลานานขึ้น ผลกระทบจึงไม่ได้จำกัดอยู่ที่ขนาดไฟล์ แต่สามารถสะท้อนกลับมาเป็น latency ที่ผู้ใช้สัมผัสได้
VACUUM ทำความสะอาดอย่างไร
VACUUM มีหน้าที่ตรวจสอบ dead tuples และทำเครื่องหมายให้พื้นที่เหล่านั้นกลับมาใช้ซ้ำได้เมื่อปลอดภัย การทำงานนี้ช่วยจำกัดการขยายตัวของ table และเป็นส่วนสำคัญของการดูแล PostgreSQL ในระยะยาว
อย่างไรก็ตาม VACUUM แบบปกติโดยทั่วไปไม่ได้ทำให้ไฟล์บน disk เล็กลงหรือคืนพื้นที่ให้ระบบปฏิบัติการทันที พื้นที่จะยังอยู่ภายในไฟล์ของ PostgreSQL แต่พร้อมให้การเขียนข้อมูลครั้งต่อไปนำกลับมาใช้ จึงควรมอง VACUUM เป็นการจัดระเบียบพื้นที่ภายในมากกว่าคำสั่งลดขนาดไฟล์
VACUUM FULL สามารถคืนพื้นที่ให้ระบบปฏิบัติการได้มากกว่า เพราะจะเขียน table ขึ้นใหม่ในรูปแบบที่กระชับกว่า แต่กระบวนการนี้ต้องใช้ lock ที่เข้มงวดและอาจบล็อกการใช้งาน table ระหว่างทำงาน ใน production จึงไม่ควรเรียกใช้แบบสุ่มขณะมีผู้ใช้งาน และควรวาง maintenance window รวมถึงประเมินพื้นที่ชั่วคราวให้เหมาะสมก่อนเสมอ
Autovacuum คือผู้ดูแลเบื้องหลัง
PostgreSQL มี autovacuum คอยตรวจสอบ table ที่มีการเปลี่ยนแปลง และเริ่มทำ VACUUM หรือ ANALYZE เมื่อปริมาณการเปลี่ยนแปลงถึงเกณฑ์ที่กำหนด ระบบจำนวนมากทำงานได้ต่อเนื่องเพราะกลไกนี้ดูแลฐานข้อมูลอยู่เบื้องหลังทุกวัน
ถึงอย่างนั้น autovacuum ไม่ใช่ระบบที่ตามทุก workload ได้โดยอัตโนมัติเสมอไป หากมีการเขียนข้อมูลหนักมาก table มีขนาดใหญ่ transaction เปิดค้างนาน หรือค่ากำหนดไม่เหมาะกับรูปแบบการใช้งาน autovacuum อาจจัดการ dead tuples ไม่ทัน เมื่อเกิดเหตุการณ์นี้ table และ index จะโตขึ้น ขณะที่ query, backup และงานดูแลระบบค่อย ๆ ใช้เวลามากกว่าเดิม
การเห็น autovacuum กำลังทำงานไม่ใช่สัญญาณอันตรายโดยตัวมันเอง สิ่งที่ควรสนใจคือมันเริ่มทำงานทันหรือไม่ ทำงานเสร็จหรือไม่ และจำนวน dead tuples มีแนวโน้มลดลงหลังจากนั้นหรือยัง
ANALYZE ช่วยให้ Planner เลือกแผนได้ถูกต้อง
ANALYZE เก็บสถิติเกี่ยวกับข้อมูล เช่น จำนวนแถวโดยประมาณ การกระจายตัวของค่า และสัดส่วนค่าที่ซ้ำกัน Query planner ใช้ข้อมูลเหล่านี้ตัดสินใจว่าจะใช้ index scan, sequential scan รวมถึงลำดับและวิธีการ join แบบใด
หากสถิติเก่า planner อาจประเมินจำนวนแถวคลาดเคลื่อนและเลือก execution plan ที่ไม่เหมาะสม ตัวอย่างเช่น เลือก sequential scan ทั้งที่ index มีประโยชน์ หรือเลือกวิธี join ที่มีต้นทุนสูงกว่าสำหรับข้อมูลจริง ดังนั้น VACUUM และ ANALYZE จึงควรถูกมองควบคู่กัน แม้ทั้งสองกระบวนการจะมีหน้าที่ต่างกัน
สัญญาณที่ Backend Developer ควรติดตาม
การเฝ้าดูเฉพาะ CPU และ memory ไม่เพียงพอสำหรับการประเมินสุขภาพ PostgreSQL ฐานข้อมูลอาจติด lock, รอ connection หรือสะสม bloat ขณะที่ CPU ยังอยู่ในระดับปกติ Dashboard ที่ดีจึงควรแสดงทั้งประสิทธิภาพ query และสภาพภายในของฐานข้อมูล
Slow Queries และภาระรวมของระบบ
pg_stat_statements ช่วยให้เห็นว่า query ใดใช้เวลารวมสูง ถูกเรียกบ่อย หรือมีเวลาเฉลี่ยผิดปกติ การแก้ปัญหาไม่ควรเลือกเฉพาะ query ที่เคยช้าที่สุดหนึ่งครั้ง แต่ควรพิจารณาผลกระทบสะสมต่อระบบด้วย
สำหรับค่า latency แบบ p95 และ p99 ควรเก็บข้อมูลผ่านระบบ monitoring หรือ APM ที่รองรับข้อมูลตามช่วงเวลา ค่า percentile เหล่านี้ช่วยเปิดเผยความช้าที่กระทบผู้ใช้บางกลุ่ม แม้ค่าเฉลี่ยโดยรวมจะยังดูปกติ
Connections และ Connection Pool
ควรติดตามจำนวน connection ทั้งหมด สถานะ active, idle และ idle in transaction พร้อมเปรียบเทียบกับ max_connections หากจำนวน connection เข้าใกล้เพดาน แอปอาจไม่มี connection pool ที่เหมาะสม มี request ค้าง หรือคืน connection กลับ pool ไม่ครบ
แอป Node.js ที่สร้าง connection ใหม่อย่างต่อเนื่องสามารถทำให้ฐานข้อมูลเริ่มเหนื่อยก่อนโค้ดจะแสดง error อย่างชัดเจน การเตือนตั้งแต่ใช้ connection ราว 80% ของ limit ช่วยให้ทีมมีเวลาแยกสาเหตุก่อนผู้ใช้เริ่มเชื่อมต่อไม่ได้
Locks และ Transaction ที่เปิดค้าง
เมื่อ query รอ lock ผู้ใช้อาจรู้สึกว่าเว็บค้างทั้งที่ CPU ไม่ได้สูง ตารางสถิติอย่าง pg_locks และ pg_stat_activity ช่วยบอกได้ว่า session ใดกำลังรอ ใครเป็นตัวบล็อก และ query นั้นเปิดค้างมานานเท่าใด
Long-running transaction ควรได้รับความสนใจเป็นพิเศษ เพราะนอกจากจะถือ lock ได้นานแล้ว ยังอาจรักษา snapshot เก่าจน dead tuples ไม่สามารถถูกจัดการได้ การตั้ง alert จากอายุ transaction จึงช่วยป้องกันทั้งปัญหา latency และ bloat
Replication Lag
หากระบบใช้ read replica ควรติดตาม replication lag ตามระดับความสดใหม่ของข้อมูลที่ธุรกิจยอมรับได้ Replica ที่ตาม primary ไม่ทันอาจทำให้ผู้ใช้บันทึกข้อมูลสำเร็จแล้วกลับมาอ่านไม่พบข้อมูลล่าสุด
Lag ยังส่งผลต่อ backup, analytics และกระบวนการอื่นที่อ้างอิงข้อมูลจาก replica ค่า threshold จึงควรกำหนดจากผลกระทบที่ระบบรับได้ ไม่ใช่ใช้ตัวเลขเดียวกับทุกบริการ
Disk Usage และการเติบโตของ Table
PostgreSQL ไม่ควรถูกปล่อยให้ disk ใกล้เต็ม เพราะทั้งการเขียนข้อมูล งาน maintenance และไฟล์ชั่วคราวล้วนต้องการพื้นที่ ควรตั้งระดับเตือนล่วงหน้า เช่น 80%, 90% และ 95% พร้อมพิจารณาอัตราการเติบโต ไม่ใช่ดูเฉพาะพื้นที่คงเหลือ ณ เวลาหนึ่ง
นอกจากพื้นที่รวม ควรติดตาม table และ index ที่โตผิดปกติ รวมถึงแนวโน้มของ dead tuples ด้วย Bloat มักสะสมอย่างค่อยเป็นค่อยไป การเห็นกราฟขนาดย้อนหลังจึงมีประโยชน์กว่าการตรวจสอบเมื่อ disk ใกล้เต็มแล้ว
Alert ที่ควรมีในระบบ Production
Alert ที่ดีควรแจ้งก่อนปัญหากระทบผู้ใช้ และต้องมีข้อมูลเพียงพอให้ทีมเริ่มวิเคราะห์สาเหตุได้ทันที อย่างน้อยควรครอบคลุมสัญญาณต่อไปนี้
- พื้นที่ disk ที่เหลือต่ำกว่าระดับกำหนด หรือมีอัตราการเติบโตเร็วกว่าปกติ
- Table มี dead tuples สูงผิดปกติ หรือ autovacuum ตาม workload ไม่ทัน
- จำนวน connection ใช้เกินประมาณ 80% ของ limit
- Query latency แบบ p95 หรือ p99 สูงกว่า baseline ของระบบ
- Lock wait นานเกินช่วงเวลาที่บริการยอมรับได้
- Replication lag สูงกว่า threshold ของงานที่อ่านจาก replica
- Transaction เปิดค้างนานผิดปกติ โดยเฉพาะสถานะ idle in transaction
- Table หรือ index เติบโตอย่างรวดเร็วโดยปริมาณข้อมูลเชิงธุรกิจไม่ได้เพิ่มในสัดส่วนเดียวกัน
ตัวเลข threshold ไม่ควรถูกใช้เหมือนกันทุกระบบ เพราะฐานข้อมูลแต่ละแห่งมีความจุและรูปแบบ workload ต่างกัน ควรเริ่มจาก baseline ในช่วงที่ระบบทำงานปกติ แล้วกำหนดระดับเตือนให้ทีมมีเวลาตอบสนองก่อนถึงจุดวิกฤต
ปรับ Autovacuum จากข้อมูล ไม่ใช่การคาดเดา
ก่อนปรับ autovacuum ควรเริ่มจาก table ที่มีการเขียนมากหรือโตเร็วกว่าปกติ ตรวจสอบค่าอย่าง n_dead_tup, last_autovacuum และ last_autoanalyze ควบคู่กับขนาด table อัตรา UPDATE หรือ DELETE และระยะเวลาที่แต่ละรอบใช้
ลำดับการตรวจสอบที่ใช้งานได้จริงมีดังนี้
- ระบุ table ที่สร้าง dead tuples หรือใช้พื้นที่เพิ่มเร็วที่สุด
- ตรวจสอบว่า autovacuum และ autoanalyze ทำงานครั้งล่าสุดเมื่อใด
- ค้นหา long-running transaction หรือ session ที่ขัดขวางการทำความสะอาด
- เปรียบเทียบอัตราการสร้าง dead tuples กับความถี่และระยะเวลาของ autovacuum
- ปรับ
autovacuum_vacuum_scale_factorหรือ threshold เฉพาะ table ที่มีปัญหา - ติดตามผลหลังปรับว่าจำนวน dead tuples, latency และการใช้ I/O ดีขึ้นจริงหรือไม่
การลด scale factor อาจทำให้ table ถูก vacuum เร็วขึ้น โดยเฉพาะ table ขนาดใหญ่ที่เกณฑ์แบบเปอร์เซ็นต์รอให้เกิดการเปลี่ยนแปลงจำนวนมากเกินไป แต่การปรับแรงเกินความจำเป็นก็เพิ่มภาระ I/O ได้เช่นกัน หากต้นเหตุคือ transaction เปิดค้าง การเร่ง autovacuum อย่างเดียวก็ไม่สามารถนำ tuple ที่ยังถูก snapshot เก่าอ้างอิงกลับมาใช้ได้
Mental Model สำหรับผู้เริ่มดูแล PostgreSQL
แนวคิดทั้งหมดสามารถย่อให้จำง่ายได้ดังนี้
UPDATEและDELETEทำให้เกิด row version เก่าหรือ dead tuplesVACUUMทำให้พื้นที่จาก dead tuples กลับมาใช้ซ้ำได้VACUUM FULLช่วยลดขนาดไฟล์ได้มากกว่า แต่แลกกับ lock ที่กระทบการใช้งานANALYZEอัปเดตสถิติให้ query planner ประเมินข้อมูลได้แม่นยำขึ้น- Autovacuum ทำงานอัตโนมัติ แต่ยังต้องตรวจสอบว่าตาม workload ทันหรือไม่
- Bloat คืออาการสะสมเมื่อพื้นที่ที่ไม่จำเป็นเพิ่มขึ้นเร็วกว่าการจัดการ
- Long-running transaction อาจขวางทั้งการคืนพื้นที่และการทำงานของระบบส่วนอื่น
สรุป
PostgreSQL ไม่ได้ต้องการให้ทีมเรียก VACUUM ทุกวันเหมือนเป็นพิธีกรรม สิ่งสำคัญกว่าคือการเข้าใจวงจรของ MVCC รู้ว่า dead tuples เกิดขึ้นอย่างไร และมองเห็นเมื่อ autovacuum เริ่มตาม workload ไม่ทัน
เริ่มต้นด้วย dashboard ที่มี slow queries, connections, locks, replication lag, disk usage และ dead tuples เพียงเท่านี้ทีมก็จะเห็นสุขภาพของฐานข้อมูลได้ชัดกว่าการรอให้เว็บช้าหรือ disk เต็มแล้วค่อยคาดเดา เป้าหมายของ monitoring ที่ดีคือให้ alert เป็นผู้แจ้งปัญหาก่อนที่ผู้ใช้จะเป็นคนแจ้งทีมเอง