Postmortem: วันที่ Postgres ทำเว็บล่ม
Index ที่หายไปตัวเดียว รวมกับ cron job ที่รันตอน peak ทำให้ database ค้าง 47 นาที timeline ครบและบทเรียน
Postmortem: วันที่ Postgres ทำเว็บล่ม
สรุปสั้น: Index ที่หายไปใน foreign key column รวมกับ cron job ที่รัน ตอน peak hours ทำให้ billing API ล่ม 47 นาที
Timeline (เวลา UTC ทั้งหมด)
- 14:00 — Cron job เริ่ม:
cleanup_old_invoices.sh - 14:02 — ลูกค้าแจ้ง checkout ช้า
- 14:05 — PagerDuty alert: API latency p99 > 5s
- 14:08 — On-call engineer (ผม) เปิด laptop
- 14:14 — ระบุได้: Postgres CPU 100% มี
DELETEqueries รอหลายร้อยตัว - 14:22 — Kill cron job แบบ manual → queries drain ใน ~30s
- 14:25 — Service เริ่มฟื้น
- 14:47 — ฟื้นเต็มที่ replication catch up แล้ว
ผลกระทบ: 47 นาทีที่ service ช้า ~120 transactions fail
ต้นเหตุ
Cleanup script รัน DELETE FROM invoices WHERE created_at < NOW() - INTERVAL '90 days'
มี 2 อย่างผิด:
created_atไม่มี index — full table scan สำหรับทุก DELETE batch- Foreign key จาก
invoice_items.invoice_id— Postgres ต้อง verify ว่าไม่มี referencing rows ในทุก DELETE ก็เป็น full scan
ทุก DELETE กลายเป็น:
SCAN invoices (~3M rows) — ไม่มี index
→ สำหรับแต่ละ row ที่ลบ:
SCAN invoice_items (~50M rows) — ไม่มี index บน FK
มี 10,000 rows ที่จะลบ นั่นคือ ~3×10⁹ row checks ที่ ~2 นาทีต่อ batch cron job จะรัน 2+ ชั่วโมง locking ทั้งตาราง
แก้ไข
- เพิ่ม index ที่หายไป:
CREATE INDEX CONCURRENTLY idx_invoices_created_at ON invoices (created_at); CREATE INDEX CONCURRENTLY idx_invoice_items_invoice_id ON invoice_items (invoice_id); - ย้าย cron job ไป off-peak (3 AM ไม่ใช่ 2 PM)
- เพิ่ม statement timeout:
SET statement_timeout = '5min'ใน postgresql.conf - เพิ่ม pre-check: ถ้ามี long-running query อยู่แล้ว ข้าม cleanup
บทเรียน
- Index foreign keys เสมอ Postgres ไม่ทำให้คุณ
- Cron jobs ไม่ควรรันตอน peak เว้นแต่ต้องการจริง ๆ
- Statement timeouts ช่วยคุณ ถ้าไม่มี query แย่ ๆ จะรันไปตลอด
- Run load test bug นี้อยู่มา 6 เดือน — เพิ่งเห็นตอน table โตเกิน threshold
- Document incident ของคุณ เพื่อคนถัดไปไม่ต้องเรียนรู้ใหม่
Action items
- เพิ่ม indexes ที่หายไป
- ย้าย cron ไป off-peak
- เพิ่ม statement_timeout
- รัน EXPLAIN กับ DELETE/UPDATE queries ที่ >10K rows ทั้งหมด
- ทุกไตรมาส: ดู slow query log