กลับไปยังบทความ

Backend

ดูแล Database บน Production: Connection Pool, Maintenance, Partitioning และ Backup

Query ที่เร็วเป็นเพียงส่วนหนึ่งของฐานข้อมูลที่แข็งแรง คู่มือนี้พูดถึงงานฝั่ง operation ได้แก่ การกำหนดขนาด connection pool การดูแล vacuum และ statistics ให้พร้อมใช้งาน การทำ partition ตารางขนาดใหญ่ การวางแผน capacity และการออกแบบ backup ที่ทดสอบ restore มาแล้วจริง

24 กันยายน 25696 นาทีในการอ่าน

Running Databases in Production: Pooling, Maintenance, Partitioning, and Backups

Query ที่ปรับแต่งมาอย่างดีบนฐานข้อมูลที่ดูแลไม่ดี ก็ยังเป็น query ที่ช้าอยู่ดี Connection ไม่พอ ตารางบวม disk โตไม่หยุด และ backup ที่ไม่เคยทดสอบ ก่อให้เกิด incident บน production มากกว่า index ที่ขาดไปตัวใดตัวหนึ่งเสียอีก บทความนี้รวบรวมนิสัยด้าน operation ที่ช่วยให้ relational database (ในตัวอย่างใช้ PostgreSQL) แข็งแรงไปอีกหลายปี

การจัดการ connection

ทำไม connection ถึงแพง

แต่ละ connection ของ PostgreSQL คือ process แยกของระบบปฏิบัติการที่มี memory ของตัวเอง Connection หลายร้อยตัวที่ส่วนใหญ่อยู่ในสถานะ idle จะกิน RAM เพิ่ม context switching และทำให้การจัดการ lock แพงขึ้น ฐานข้อมูลที่รับได้ 10,000 query ต่อวินาทีผ่าน 50 connection อาจเริ่มหอบเมื่อมี 2,000 connection ทำงานปริมาณเท่าเดิม

ทำ pool ในแอปพลิเคชัน

ทุก service ควรใช้ connection pool ที่กำหนดขีดจำกัดไว้ชัดเจน:

go
db.SetMaxOpenConns(20)                  // hard ceiling per instance
db.SetMaxIdleConns(10)                  // keep some warm
db.SetConnMaxLifetime(30 * time.Minute) // recycle to rebalance after failovers
db.SetConnMaxIdleTime(5 * time.Minute)

ลองคูณดู: 30 pod × 20 connection = 600 connection ซึ่งมักมากเกินกว่าที่ฐานข้อมูลจะรับได้อย่างมีประสิทธิภาพ Autoscaling ยิ่งทำให้แย่ลง เพราะ pod ที่เพิ่มขึ้นหมายถึง connection ที่เพิ่มขึ้นในจังหวะที่โหลดพีกพอดี

กำหนดขนาด pool

จุดเริ่มต้นที่ดีคือจำไว้ว่าฐานข้อมูลทำงานได้ดีที่สุดเมื่อมี connection ที่ active เพียงไม่กี่ตัวต่อ CPU core การเพิ่ม concurrency เกินกว่านั้นจะเพิ่มการแย่งทรัพยากรโดยไม่ได้ throughput เพิ่ม ให้เริ่มจากค่าน้อย ๆ วัดเวลาที่รอ connection เทียบกับ latency ของ query และเพิ่มขนาดเฉพาะเมื่อ request ต้องต่อคิวรอ connection ในขณะที่ฐานข้อมูลยังมี capacity ว่างอยู่

ทำ pool ไว้หน้าฐานข้อมูล

เมื่อมีหลาย service หรือมี serverless function ให้เพิ่ม connection pooler อย่าง PgBouncer คั่นระหว่างแอปกับฐานข้อมูล ในโหมด transaction pooling client connection นับพันจะใช้ server connection ชุดเล็ก ๆ ร่วมกัน แต่ต้องรู้ด้วยว่าโหมด transaction ทำให้อะไรใช้ไม่ได้บ้าง ได้แก่ ฟีเจอร์ระดับ session อย่าง SET ที่ไม่มี LOCAL, advisory lock ที่ถือข้าม transaction และ (ขึ้นอยู่กับเวอร์ชันและการตั้งค่า driver) server-side prepared statement

Vacuum, bloat และ statistics

ทำไมต้องมี vacuum

PostgreSQL ใช้ MVCC โดย UPDATE จะเขียนเวอร์ชันใหม่ของแถว ส่วน DELETE แค่ทำเครื่องหมายไว้ เวอร์ชันเก่า ("dead tuple") จะยังคงอยู่จนกว่า VACUUM จะเรียกคืนพื้นที่ให้นำกลับมาใช้ใหม่ได้ ถ้าไม่มี vacuum ตารางและ index จะบวม และ query ต้องอ่าน page มากขึ้นเพื่อหาข้อมูลที่ยังใช้งานอยู่ชุดเดิม

Autovacuum ทำงานนี้ให้อัตโนมัติ แต่ค่าเริ่มต้นถูกตั้งไว้สำหรับตารางขนาดปานกลาง บนตาราง 200 ล้านแถว threshold เริ่มต้น (ประมาณ 20% ของแถวที่เปลี่ยนแปลง) หมายความว่าต้องรอให้มี dead row ถึง 40 ล้านแถวก่อนจะเริ่มเก็บกวาด

ให้ปรับแต่งเป็นรายตารางสำหรับตารางใหญ่ที่มีงานหนาแน่น:

sql
ALTER TABLE events SET (
    autovacuum_vacuum_scale_factor = 0.01,
    autovacuum_analyze_scale_factor = 0.02,
    autovacuum_vacuum_cost_limit = 2000
);

สิ่งที่ต้องเฝ้าดู

sql
SELECT relname, n_live_tup, n_dead_tup,
       last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;
  • Dead tuple เพิ่มเร็วกว่าที่ถูกเก็บกวาด แปลว่า autovacuum ต้องตั้งค่าให้ดุขึ้นหรือเพิ่มจำนวน worker
  • Transaction ที่รันนาน จะขวางการเก็บกวาด เพราะ vacuum ลบเวอร์ชันของแถวที่ transaction ที่ยังเปิดอยู่อาจต้องใช้ไม่ได้ BEGIN ที่ถูกลืมไว้ตัวเดียวใน session ที่ idle อาจทำให้ทั้งฐานข้อมูลบวมได้ ให้ตั้งค่า idle_in_transaction_session_timeout
  • Transaction ID wraparound คือขีดจำกัดตายตัวของ PostgreSQL ปกติ autovacuum จะป้องกันไว้ แต่ควร monitor age(datfrozenxid) และตั้ง alert ไว้ตั้งแต่เนิ่น ๆ ก่อนจะเข้าใกล้ขีดจำกัด

Statistics ที่สดใหม่

พี่น้องของ vacuum อย่าง ANALYZE มีหน้าที่รีเฟรช statistics ที่ query planner ใช้ หลัง bulk load หรือการลบข้อมูลจำนวนมาก ให้รัน ANALYZE เองแทนที่จะรอ autovacuum

การทำ partition ตารางขนาดใหญ่

เมื่อตารางโตถึงหลายร้อยล้านแถว โดยเฉพาะข้อมูลแบบ time-series อย่าง event, log หรือธุรกรรม declarative partitioning จะแบ่งตารางเป็นตารางทางกายภาพย่อย ๆ ที่อยู่เบื้องหลังตารางเชิงตรรกะตัวเดียว:

sql
CREATE TABLE events (
    id          BIGINT GENERATED ALWAYS AS IDENTITY,
    occurred_at TIMESTAMPTZ NOT NULL,
    tenant_id   BIGINT NOT NULL,
    payload     JSONB
) PARTITION BY RANGE (occurred_at);

CREATE TABLE events_2026_09 PARTITION OF events
    FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');

สิ่งที่ partitioning ให้คุณ

  • Partition pruning Query ที่มี WHERE occurred_at >= '2026-09-15' จะ scan เฉพาะ partition ที่เกี่ยวข้อง
  • การจัดการ retention ที่ถูก การลบข้อมูลเก่ากลายเป็น DROP TABLE events_2025_09 ซึ่งเสร็จทันที แทนที่จะเป็น DELETE ก้อนใหญ่ที่สร้าง dead tuple หลายกิกะไบต์
  • หน่วยการ maintenance ที่เล็กลง Vacuum และ reindex ทำงานทีละ partition

สิ่งที่ partitioning ไม่ได้ให้

  • ไม่ได้ช่วยให้ query ที่ไม่กรองด้วย partition key เร็วขึ้น query เหล่านั้นจะ scan ทุก partition และอาจช้ากว่าเดิมด้วยซ้ำ
  • Unique constraint ต้องรวม partition key ไว้ด้วย
  • Partition ที่มากเกินไป (หลักพัน) จะเพิ่ม overhead ในการวาง plan ให้เลือกความละเอียด เช่น รายเดือนหรือรายสัปดาห์ ที่ทำให้แต่ละ partition ยังมีขนาดใหญ่พอสมควร

ให้สร้าง partition ล่วงหน้าแบบอัตโนมัติด้วยเครื่องมืออย่าง pg_partman หรือ scheduled job เพื่อไม่ให้การ insert ล้มเหลวเพราะไม่มี partition รองรับ

การวางแผน capacity

ติดตามแนวโน้มก่อนที่มันจะกลายเป็น incident:

  • Disk: อัตราการโตต่อสัปดาห์ และจำนวนวันที่คาดว่าจะเต็ม ให้ alert ที่ 70–80% ไม่ใช่ 95%
  • Memory: buffer cache hit ratio ของตารางที่ใช้บ่อย ถ้าอัตราส่วนลดลง มักแปลว่า working set โตเกินขนาด RAM แล้ว
  • CPU และ I/O: การใช้งานต่อเนื่องในช่วงพีก ไม่ใช่ค่าเฉลี่ย
  • Connections: จำนวน connection ที่ active สูงสุดเทียบกับขีดจำกัด
  • Replication lag: replica ที่ให้บริการข้อมูลเก่าจะก่อให้เกิดบั๊กที่สังเกตยาก

ขยายฝั่งการอ่านด้วย read replica สำหรับงานรายงานและ endpoint ที่อ่านเป็นหลัก และส่งงานเขียนกับเส้นทางที่ต้องอ่านหลังเขียนทันทีไปยัง primary ก่อนจะทำ sharding ซึ่งเพิ่มความซับซ้อนอย่างมาก ให้ตรวจดูก่อนว่า partitioning การ archive ข้อมูลที่ไม่ค่อยถูกใช้ index ที่ดีขึ้น หรือ instance ที่ใหญ่ขึ้น จะแก้ปัญหาได้หรือไม่

เปลี่ยน schema โดยไม่มี downtime

การดำเนินการ ALTER TABLE บางอย่างจะถือ lock ที่ block traffic ทั้งหมด รูปแบบที่ปลอดภัยกว่ามีดังนี้

  • การเพิ่มคอลัมน์ ที่ไม่มีค่า default หรือมีค่า default เป็นค่าคงที่ใน PostgreSQL เวอร์ชันใหม่ ๆ ทำได้เร็ว แต่การเพิ่มคอลัมน์ที่มี default แบบ volatile จะเขียนตารางใหม่ทั้งตาราง
  • การเพิ่ม NOT NULL constraint: ให้เพิ่มแบบ NOT VALID ก่อน แล้วค่อย VALIDATE CONSTRAINT แยกอีกครั้ง เพื่อให้การตรวจสอบทั้งหมดทำงานได้โดยไม่ต้องถือ lock ที่ block งานอื่น
  • การเปลี่ยนชื่อหรือลบคอลัมน์: ใช้แนวทาง expand and contract คือเพิ่มคอลัมน์ใหม่ เขียนลงทั้งสองคอลัมน์ backfill เป็น batch เปลี่ยนการอ่านไปที่คอลัมน์ใหม่ แล้วค่อยลบคอลัมน์เก่าใน release ถัดไป
  • ตั้งค่า lock_timeout สำหรับ migration เพื่อให้ migration ที่ต้องรอหลัง query ยาว ๆ ล้มเหลวอย่างรวดเร็ว แทนที่จะทำให้ traffic ทั้งหมดต้องต่อคิวรออยู่ข้างหลัง

Backup ที่ไว้ใจได้

Backup ที่ไม่เคย restore เลยเป็นแค่ความหวัง ไม่ใช่ backup

ชั้นของการป้องกัน

  • Continuous WAL archiving + base backup ทำให้ทำ point-in-time recovery (PITR) ได้ คุณสามารถ restore กลับไปยังช่วงเวลาใดก็ได้ เช่น หนึ่งวินาทีก่อนที่ใครบางคนจะรัน DELETE โดยไม่มี WHERE
  • Logical dump (pg_dump) พกพาไปใช้ที่อื่นได้ และสะดวกสำหรับตารางเดี่ยวหรือการ migrate แต่ช้าสำหรับฐานข้อมูลขนาดใหญ่และใช้แทน PITR ไม่ได้
  • Replica ไม่ใช่ backup มันจะ replicate ความผิดพลาดของคุณไปทันที

กำหนดเป้าหมาย

  • RPO (Recovery Point Objective): คุณยอมให้ข้อมูลหายได้มากแค่ไหน? WAL archiving ช่วยลดตัวเลขนี้ลงเหลือระดับวินาที
  • RTO (Recovery Time Objective): คุณยอมให้ระบบล่มได้นานแค่ไหน? ตัวเลขนี้ขึ้นกับขนาดฐานข้อมูลและความเร็วในการ restore ดังนั้นต้องวัดจริง

ทดสอบ restore อย่างสม่ำเสมอ

ทำ scheduled job แบบอัตโนมัติที่:

  1. Restore backup ล่าสุดลงใน instance ที่แยกออกมา
  2. รัน sanity check เช่น จำนวนแถวและ timestamp ล่าสุด
  3. บันทึกระยะเวลาที่ใช้ restore
  4. แจ้ง alert เมื่อล้มเหลว

เก็บ backup ไว้ใน account หรือ region แยกต่างหากที่จำกัดสิทธิ์การลบ เพื่อไม่ให้ incident ที่เจาะเข้า production ได้ทำลาย backup ไปด้วย

Checklist ตรวจสุขภาพประจำเดือน

  • ทบทวน query อันดับต้น ๆ ตามเวลารวมแล้ว
  • ตรวจสอบ index ที่ไม่ได้ใช้และ index ที่ซ้ำกันแล้ว
  • ตรวจแนวโน้ม bloat และ dead tuple แล้ว
  • อายุของ transaction ID ที่เก่าที่สุดยังอยู่ในระยะปลอดภัย
  • อัปเดตการคาดการณ์การโตของ disk แล้ว
  • ทบทวนจำนวน connection สูงสุดเทียบกับขีดจำกัดแล้ว
  • ทดสอบ restore ผ่าน พร้อมบันทึกระยะเวลาแล้ว

สรุป

ทำ connection pool และจำกัดจำนวน connection ดูแล vacuum และ statistics ให้พร้อมใช้งาน ทำ partition ตารางขนาดใหญ่ที่อิงตามเวลา เฝ้าดูแนวโน้ม capacity เปลี่ยน schema ทีละขั้น และทดสอบ restore อย่างสม่ำเสมอ ไม่มีอะไรในนี้ที่หวือหวา แต่ทั้งหมดนี้คือสิ่งที่ทำให้ฐานข้อมูลเชื่อถือได้ไปอีกหลายปี

พร้อมสร้างสิ่งที่ยิ่งใหญ่หรือยัง?

เล่าโปรเจคของคุณให้เราฟัง — เราจะพาคุณไปถึง

เลือกคุกกี้ที่เราใช้ได้ คุกกี้ที่จำเป็นช่วยให้เว็บไซต์ทำงานได้และเปิดใช้งานเสมอ