PlusMagi's Blog By Pitt Phunsanit PostgreSQL วิธีปรับจูน gin_pending_list_limit เพื่อลด Write Amplification

วิธีปรับจูน gin_pending_list_limit เพื่อลด Write Amplification

การทำงานของ fastupdate ใน GIN Index จะนำข้อมูลแถวใหม่ไปพักไว้ใน Pending List ก่อน แล้วค่อยนำไปควบรวม (Merge/Clean) เข้าสู่โครงสร้างหลัก การปรับจูนพารามิเตอร์ gin_pending_list_limit จึงเป็นหัวใจสำคัญในการควบคุมสมดุลระหว่างความเร็วในการเขียน (Write Performance), ปัญหา Write Amplification และความเร็วในการค้นหา (Read Performance)


1. ทำความเข้าใจปัญหา: เมื่อใดที่เกิด Write Amplification?

ค่าเริ่มต้นของ gin_pending_list_limit ใน PostgreSQL อยู่ที่ 4MB

  • หากตั้งค่าน้อยเกินไป (เช่น ค่า default 4MB ในระบบ Write-heavy)
    Pending List จะเต็มเร็วมาก ส่งผลให้คำสั่ง INSERT หรือ UPDATE ธรรมดาของ Application ต้องกลายเป็นผู้รับภาระ (Victim Worker) ในการสั่งหยุดและทำการ Flush/Clean โครงสร้าง Index หลักแบบ Synchronous ทันที ทำให้เกิด Write Latency Spikes และ I/O กระชาก
  • หากตั้งค่าใหญ่เกินไป (เช่น 64MB – 128MB)
    ลดความถี่ในการ Clean ได้ดี แต่ Query (SELECT) จะช้าลงอย่างหนัก เพราะทุกครั้งที่มีการอ่าน ข้อมูลใน Pending List ยังไม่ได้ถูกจัดทำ Index แบบ Inverted โครงสร้าง จึงต้องสแกน Pending List ทั้งหมดแบบ Sequential Scan ทีละแถวก่อนที่จะไปรวมผลกับ Index หลัก

2. แนวทางการปรับขนาด gin_pending_list_limit

แทนที่จะปรับแบบ Global ทั้งเซิร์ฟเวอร์ ให้กำหนดเฉพาะเจาะจงระดับ ราย Index (Per-Index Storage Parameter) ตามลักษณะเวิร์กโหลด

-- ปรับเพิ่มขนาด Pending List เป็น 16MB หรือ 32MB สำหรับ Index ที่มีการเขียนต่อเนื่อง
ALTER INDEX idx_products_details SET (gin_pending_list_limit = '32MB');

เกณฑ์การพิจารณาขนาด

  • 8MB – 16MB (แนะนำเริ่มต้น)
    สำหรับตาราง JSONB / Array ที่มีอัตราการ Insert ปานกลางถึงสูง ช่วยลดความถี่ในการแย่ง Clean ระหว่างเขียนได้ดีโดยไม่กระทบความเร็ว SELECT มากนัก
  • 32MB – 64MB
    สำหรับระบบที่เน้น Batch Ingestion ปริมาณสูง และไม่ได้มีการรัน Query ตรวจสอบผลแบบ Real-time ทันทีหลังจากเขียน

3. ย้ายภาระการ Clean ออกจาก Application ไปให้ Autovacuum

เป้าหมายสูงสุดเพื่อลด Write Latency Spikes คือ “อย่าให้คำสั่ง INSERT ทั่วไปต้องเป็นคน Clean Pending List เอง” แต่ให้กระบวนการเบื้องหลัง (Background Worker) จัดการแทน

  1. ปรับแต่ง Autovacuum ให้เข้ามาช่วยบ่อยขึ้น
    ALTER TABLE products SET (
        -- บังคับให้ Autovacuum ตรวจจับและเข้ามา Clean Pending List เร็วขึ้น
        autovacuum_vacuum_scale_factor = 0.05,
        autovacuum_vacuum_cost_limit = 1000
    );
    
  2. สั่ง Clean ด้วยตนเองแบบ Background Job (Manual Flush)
    สำหรับระบบที่มีรอบการเขียนข้อมูลต่อเนื่องสูง ให้เขียน Schedule Task (เช่น ผ่าน pg_cron) สั่ง Flush เป็นระยะตามเวลาที่กำหนด เพื่อไม่ให้ขนาดสะสมไปแตะลิมิต
    -- รันคลีน Pending List โดยตรง
    SELECT gin_clean_pending_list('idx_products_details');
    
  3. ย้ายภาระการ Clean ออกจาก Application ไปให้ Autovacuum
    เป้าหมายสูงสุดเพื่อลด Write Latency Spikes คือ “อย่าให้คำสั่ง INSERT ทั่วไปต้องเป็นคน Clean Pending List เอง” แต่ให้กระบวนการเบื้องหลัง (Background Worker) จัดการแทน
    1. ปรับแต่ง Autovacuum ให้เข้ามาช่วยบ่อยขึ้น
      ALTER TABLE products SET (
          -- บังคับให้ Autovacuum ตรวจจับและเข้ามา Clean Pending List เร็วขึ้น
          autovacuum_vacuum_scale_factor = 0.05,
          autovacuum_vacuum_cost_limit = 1000
      );
      
    2. สั่ง Clean ด้วยตนเองแบบ Background Job (Manual Flush)
      สำหรับระบบที่มีรอบการเขียนข้อมูลต่อเนื่องสูง ให้เขียน Schedule Task (เช่น ผ่าน pg_cron) สั่ง Flush เป็นระยะตามเวลาที่กำหนด เพื่อไม่ให้ขนาดสะสมไปแตะลิมิต
      -- รันคลีน Pending List โดยตรง
      SELECT gin_clean_pending_list('idx_products_details');
      
  4. กลยุทธ์สำหรับ Bulk Ingestion (โหลดข้อมูลชุดใหญ่)
    หากต้องนำเข้าข้อมูลจำนวนหลายแสนหรือหลักล้านแถวพร้อมกัน การพึ่งพา gin_pending_list_limit เพียงอย่างเดียวจะไม่เพียงพอ ให้เลือกใช้ 2 แนวทางนี้
    • ปิด fastupdate ชั่วคราวสำหรับการโหลดแบบ Batch
      เมื่อข้อมูลเข้ามาเป็นก้อนใหญ่ การพักใน Pending List แล้วทยอยย้ายเข้า Tree ทีละรอบจะเปลือง I/O รวมมากกว่าการให้เขียนลง Tree โดยตรง
      -- ปิด fastupdate ก่อน Bulk Load
      ALTER INDEX idx_products_details SET (fastupdate = off);
      
      -- [ ทำกระบวนการ COPY หรือ Bulk INSERT ตรงนี้ ]
      
      -- เปิดกลับคืนหลังโหลดเสร็จ
      ALTER INDEX idx_products_details SET (fastupdate = on);
      
    • Drop Index แล้วสร้างใหม่ (เร็วที่สุดสำหรับ Initial Load) หากเป็นการ Migrate ข้อมูลหรือ ETL ประจำวันขนาดใหญ่ การลบ GIN Index ทิ้ง รัน COPY ข้อมูลเข้าตาราง แล้วสั่ง CREATE INDEX ... USING GIN ใหม่ทีเดียว จะใช้เวลาและ Disk I/O น้อยกว่าการทยอยอัปเดตหลายเท่า
  5. วิธีตรวจสอบสุขภาพของ Pending List
    ตรวจสอบว่าปัจจุบัน Index ดังกล่าวเปิดใช้ fastupdate หรือไม่ และมี Pending List ตกค้างอยู่เท่าใด
    -- ตรวจสอบการตั้งค่าของ Index
    SELECT relname, reloptions 
    FROM pg_class 
    WHERE relname = 'idx_products_details';
    
    -- ดูจำนวนหน้า (Pages) และ Tuples ใน Pending List ด้วย pageinspect
    CREATE EXTENSION IF NOT EXISTS pageinspect;
    
    SELECT * FROM gin_metapage_info(get_raw_page('idx_products_details', 0));
    
    • จุดสังเกตใน gin_metapage_info
      nPendingPages: จำนวน Pages ที่ยังรอการ Clean หากค่านี้สูงต่อเนื่องใกล้เคียงกับลิมิตที่ตั้งไว้ แปลว่า Autovacuum ทำงานไม่ทัน หรือมีการเขียนเข้ามาเร็วกว่าอัตราการ Clean