PostgreSQL Monitoring: Performance Tuning สำหรับ Postgres

PostgreSQL ขึ้นชื่อเรื่องความเสถียรและประสิทธิภาพสูง แต่การทำให้ระบบทำงานได้อย่างเต็มศักยภาพนั้นต้องอาศัยการ Monitoring ที่ครอบคลุมและการ Tuning ที่เหมาะสมกับ Workload จริง การปล่อยให้ฐานข้อมูลทำงานด้วยค่า Default ที่ติดตั้งมาตั้งแต่เริ่มต้นมักนำไปสู่ปัญหา Slow Query, Connection Exhaustion, หรือ Disk I/O พุ่งสูงในวันที่มี Traffic เยอะ

บทความนี้จะอธิบาย Metrics สำคัญที่ต้องติดตาม Extension ที่จำเป็น เช่น pg_stat_statements และแนวทางปรับ Parameter หลัก เช่น shared_buffers, work_mem, effective_cache_size ให้เหมาะกับ Workload โดยยึดหลัก Data-driven Tuning ไม่ใช่เดาสุ่ม

Metrics สำคัญที่ต้องติดตาม

การ Monitoring ฐานข้อมูลที่ดีไม่ใช่เก็บทุกอย่าง แต่เลือกเก็บ metric ที่สามารถนำไปปรับปรุงได้จริง (actionable) แบ่งเป็น 4 กลุ่มหลักตามลำดับความสำคัญดังนี้

กลุ่มMetricความสำคัญ
Availabilitypg_up, connection success rateรู้ทันทีเมื่อระบบล่ม
Performancequery latency, slow queries, cache hit ratioจุดเริ่มต้นของการ tuning
Resourceconnection count, transaction rate, lock waitsตัดสินใจเรื่อง capacity
Replicationlag, WAL sender/receiver statusHA/DR readiness

ติดตั้ง pg_stat_statements

pg_stat_statements เป็น Extension ที่ทำให้เห็นรายละเอียดของทุก query ที่รัน ตั้งแต่ execution time, rows affected, buffer hits, shared blocks read ถือเป็นเครื่องมือพื้นฐานสำหรับ DBA ที่ต้องการหา slow query และ query pattern ที่ใช้งานบ่อย

# แก้ไขไฟล์ postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.max = 10000
pg_stat_statements.track = all
pg_stat_statements.track_utility = on
pg_stat_statements.save = on

# restart PostgreSQL
sudo systemctl restart postgresql

# สร้าง extension ในฐานข้อมูลที่ต้องการ monitor
psql -U postgres -d mydb -c "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"

Query ที่ใช้บ่อย

-- Top 10 slowest queries โดยเวลาเฉลี่ยต่อครั้ง
SELECT
    substring(query, 1, 80) AS short_query,
    calls,
    round(mean_exec_time::numeric, 2) AS avg_ms,
    round(total_exec_time::numeric, 2) AS total_ms,
    round((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 2) AS pct
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

-- Query ที่กิน I/O มากที่สุด
SELECT
    substring(query, 1, 80),
    shared_blks_read + shared_blks_hit AS total_blks,
    shared_blks_read,
    round(100.0 * shared_blks_hit /
          nullif(shared_blks_hit + shared_blks_read, 0), 2) AS hit_pct
FROM pg_stat_statements
ORDER BY shared_blks_read DESC
LIMIT 10;

-- Reset สถิติเพื่อเริ่มเก็บข้อมูลใหม่
SELECT pg_stat_statements_reset();

Cache Hit Ratio

Cache Hit Ratio เป็นตัวชี้วัดว่า Postgres อ่านข้อมูลจาก shared_buffers (memory) ได้กี่เปอร์เซ็นต์ เทียบกับอ่านจาก disk ค่าที่ควรได้คือ 99% ขึ้นไปสำหรับ OLTP หากต่ำกว่า 95% เป็นสัญญาณว่าควรเพิ่ม shared_buffers หรือ RAM

-- Cache Hit Ratio ระดับฐานข้อมูล
SELECT
    datname,
    round(100.0 * blks_hit / nullif(blks_hit + blks_read, 0), 2) AS cache_hit_ratio
FROM pg_stat_database
WHERE datname NOT IN ('template0', 'template1');

-- Cache Hit Ratio ระดับ table (หา table ที่ควรเพิ่ม index หรือ memory)
SELECT
    schemaname,
    relname,
    heap_blks_read,
    heap_blks_hit,
    round(100.0 * heap_blks_hit /
          nullif(heap_blks_hit + heap_blks_read, 0), 2) AS hit_pct
FROM pg_statio_user_tables
WHERE heap_blks_read + heap_blks_hit > 0
ORDER BY heap_blks_read DESC
LIMIT 10;

Connection และ Transaction Monitoring

การติดตาม connection จะช่วยให้เห็นภาพ pattern การใช้งาน ทั้งจำนวน connection ที่ active, idle, และ idle in transaction ซึ่งเป็นปัญหาที่พบบ่อยเพราะ connection ค้างทำให้ vacuum ไม่สามารถล้าง dead tuples ได้

-- Connection count แยกตาม state
SELECT
    state,
    count(*) AS connections,
    count(*) FILTER (WHERE query_start < now() - interval '5 min') AS long_running
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY state
ORDER BY connections DESC;

-- หา idle in transaction ที่ค้างนาน (ต้องฆ่า)
SELECT
    pid,
    usename,
    application_name,
    state,
    now() - xact_start AS xact_duration,
    query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
  AND xact_start < now() - interval '10 minutes'
ORDER BY xact_duration DESC;

-- ฆ่า connection ที่ค้าง
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
  AND xact_start < now() - interval '30 minutes';

Lock Monitoring

Lock contention เป็นสาเหตุหลักที่ทำให้ query ช้าแม้ว่า resource ของเซิร์ฟเวอร์ยังเหลือ การเห็น lock waits ทันทีช่วยให้ debug ได้ตรงจุด

-- หา query ที่รอ lock และ query ที่ถือ lock
SELECT
    blocked.pid AS blocked_pid,
    blocked.query AS blocked_query,
    blocking.pid AS blocking_pid,
    blocking.query AS blocking_query,
    now() - blocked.query_start AS wait_duration
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
     ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock'
ORDER BY wait_duration DESC;

-- Deadlock count
SELECT datname, deadlocks
FROM pg_stat_database
WHERE deadlocks > 0
ORDER BY deadlocks DESC;

Replication Lag

การ monitor replication lag มีหลายมุมมอง ทั้งในฝั่ง primary (byte lag ของ WAL sender) และฝั่ง replica (apply delay) การมี lag สูงผิดปกติมักเกิดจาก network saturation หรือ long-running transaction บน primary

-- บน Primary: ดู replica ที่เชื่อมต่อและ lag
SELECT
    application_name,
    state,
    sync_state,
    pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS sent_lag_bytes,
    pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes,
    now() - reply_time AS last_reply_ago
FROM pg_stat_replication;

-- บน Replica: ดู lag เป็นเวลา
SELECT
    now() - pg_last_xact_replay_timestamp() AS replication_lag_seconds,
    pg_is_in_recovery() AS is_replica;

Parameter Tuning ที่สำคัญ

การ tuning ที่ถูกต้องต้องอ้างอิงข้อมูลจริงจาก Monitoring ไม่ใช่ copy ค่าจาก blog ใด ๆ มาใช้ ต่อไปนี้คือ parameter หลักที่ส่งผลต่อประสิทธิภาพมากที่สุด พร้อมแนวทางกำหนดค่าเบื้องต้นสำหรับเซิร์ฟเวอร์ทั่วไป

Parameterคำแนะนำเบื้องต้นหมายเหตุ
shared_buffers25% ของ RAMCache หลักสำหรับ data page
effective_cache_size50-75% ของ RAMบอก planner ว่ามี RAM ทั้งระบบเท่าไร
work_memRAM/max_connections/4สำหรับ sort และ hash — ต้องระวัง OOM
maintenance_work_mem1-2GB หรือ 5% RAMใช้ตอน VACUUM และ CREATE INDEX
max_connections100-300สูงเกินไปใช้ connection pooler แทน
wal_buffers16MB (auto-tuned)ค่า default มักเหมาะสม
checkpoint_timeout15minลด write spike ของ checkpoint
random_page_cost1.1 (SSD) / 4.0 (HDD)ช่วย planner เลือก index scan vs seq scan

ตัวอย่าง Config สำหรับเซิร์ฟเวอร์ 16GB RAM

# postgresql.conf — Server 16GB RAM, 4 vCPU, SSD, OLTP workload
shared_buffers = 4GB
effective_cache_size = 12GB
work_mem = 20MB
maintenance_work_mem = 1GB
max_connections = 200
wal_buffers = 16MB
checkpoint_timeout = 15min
checkpoint_completion_target = 0.9
random_page_cost = 1.1
effective_io_concurrency = 200
min_wal_size = 1GB
max_wal_size = 4GB

# Logging สำหรับ slow query
log_min_duration_statement = 1000
log_checkpoints = on
log_lock_waits = on
log_temp_files = 0

Autovacuum Tuning

Autovacuum เป็นกระบวนการสำคัญที่ป้องกัน table bloat และ transaction ID wraparound การ tuning ให้เหมาะสมเป็นหนึ่งในงานที่สร้างผลกระทบมากที่สุดต่อประสิทธิภาพระยะยาว

-- ดูสถิติ autovacuum ของแต่ละ table
SELECT
    schemaname,
    relname,
    n_live_tup,
    n_dead_tup,
    round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
    last_autovacuum,
    autovacuum_count
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY dead_pct DESC NULLS LAST
LIMIT 10;

-- ปรับ autovacuum สำหรับ table ที่มี update บ่อย
ALTER TABLE hot_table SET (
    autovacuum_vacuum_scale_factor = 0.05,  -- default 0.2
    autovacuum_analyze_scale_factor = 0.02, -- default 0.1
    autovacuum_vacuum_cost_limit = 2000     -- default 200
);

ใช้ postgres_exporter กับ Prometheus

สำหรับระบบ Production ที่ต้องมี alerting และ historical data การใช้ postgres_exporter ร่วมกับ Prometheus และ Grafana เป็นวิธีมาตรฐาน

# docker-compose.yml
services:
  postgres_exporter:
    image: prometheuscommunity/postgres-exporter:v0.15.0
    container_name: postgres_exporter
    restart: unless-stopped
    ports:
      - "9187:9187"
    environment:
      DATA_SOURCE_NAME: "postgresql://exporter:pass@db:5432/postgres?sslmode=disable"
      PG_EXPORTER_EXCLUDE_DATABASES: "template0,template1"
    networks:
      - monitoring
-- สร้าง user สิทธิ์จำกัดใน Postgres
CREATE USER exporter WITH PASSWORD 'pass';
GRANT pg_monitor TO exporter;
-- pg_monitor เป็น role ที่มีสิทธิ์อ่าน pg_stat_* ทั้งหมด (PostgreSQL 10+)

Alert Rules ที่แนะนำ

groups:
  - name: postgres_alerts
    interval: 30s
    rules:
      - alert: PostgresDown
        expr: pg_up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Postgres instance down ({{ $labels.instance }})"

      - alert: PostgresHighConnections
        expr: |
          sum by (instance) (pg_stat_activity_count)
          / on(instance) pg_settings_max_connections > 0.8
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Postgres connections > 80%"

      - alert: PostgresReplicationLag
        expr: pg_replication_lag_seconds > 30
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Replication lag {{ $value }}s"

      - alert: PostgresDeadlocks
        expr: rate(pg_stat_database_deadlocks[5m]) > 0.1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Deadlock rate เพิ่มขึ้น"

      - alert: PostgresLowCacheHit
        expr: |
          rate(pg_stat_database_blks_hit[5m])
          / (rate(pg_stat_database_blks_hit[5m])
             + rate(pg_stat_database_blks_read[5m])) < 0.95
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "Cache hit ratio < 95%"

Best Practices สำหรับ Production

  • เปิด pg_stat_statements เสมอตั้งแต่วันแรก — ข้อมูลย้อนหลังวิเคราะห์ปัญหาได้มาก
  • ใช้ Connection Pooler เช่น PgBouncer เมื่อ application เปิด connection เยอะ
  • ตั้ง log_min_duration_statement = 1000 เพื่อ log query ที่ใช้เวลาเกิน 1 วินาที
  • ตรวจสอบ bloat ทุกไตรมาสด้วย pgstattuple หรือ pg_repack หาก bloat เกิน 20%
  • Backup พร้อม WAL archiving ให้สามารถ Point-in-time Recovery (PITR) ได้
  • ทดสอบ restore จาก backup เป็นระยะ — ไม่ใช่แค่สำรองไว้เฉย ๆ
  • แยก read query ไปยัง replica หาก read workload สูง ลดภาระ primary
  • Upgrade minor version สม่ำเสมอ (15.5 -> 15.6) โดยไม่ต้อง dump/restore

สรุป

การทำให้ PostgreSQL ทำงานได้เต็มประสิทธิภาพต้องประกอบด้วย 3 ขั้นตอนที่วนซ้ำต่อเนื่อง ได้แก่ Monitoring เพื่อเก็บข้อมูลจริง, Analysis เพื่อระบุ bottleneck, และ Tuning เพื่อปรับ parameter หรือ query ให้ดีขึ้น ห้ามปรับโดยไม่มีข้อมูล เพราะอาจทำให้ประสิทธิภาพแย่ลงกว่าเดิม

เครื่องมือที่แนะนำประกอบด้วย pg_stat_statements สำหรับ query analysis, postgres_exporter + Prometheus + Grafana สำหรับ metric collection และ Alertmanager สำหรับ notification เมื่อ integrate ทั้งหมดเข้าด้วยกันจะได้ระบบ Observability ที่ครบถ้วน พร้อมรับมือกับการเติบโตของระบบในระยะยาว