cover

架構概覽

flowchart LR
    Client[應用服務] -->|連線請求| PgBouncer[PgBouncer<br/>連線池 :6432]
    PgBouncer -->|受控連線| PG[PostgreSQL<br/>:5432]
    PG --> WAL[WAL<br/>Write-Ahead Log]
    WAL --> DataDir[(Data Volume<br/>資料儲存)]
    PG --> Backup[pg_dump<br/>每日備份]
    Backup --> MinIO[MinIO / NAS<br/>備份歸檔]

Database:PostgreSQL 的穩健基線

資料庫是所有應用服務的核心依賴。如果 Nginx 掛了,服務暫時不可用但資料還在;如果資料庫的資料丟了,那才是真正的災難。這篇文章建立 PostgreSQL 的單節點部署基線,涵蓋安裝設定、連線池、備份還原、效能調校、以及升級策略。目標是在一台 Host 上建立一個穩健的 PostgreSQL,能應付大部分中小型應用的需求。

架構概覽

flowchart TD
  App[Application Services] -->|connection| PgBouncer[PgBouncer\n連線池 :6432]
  PgBouncer --> PG[PostgreSQL\n:5432]
  PG --> Data[(Data Volume\n/var/lib/postgresql/data)]

  Backup[Backup Script\ncron daily] -->|pg_dump| PG
  Backup -->|upload| MinIO[MinIO / NAS\n備份儲存]

  Monitor[Prometheus\npg_exporter] -->|metrics| PG
  Admin[DBA / Admin] -->|psql / pgAdmin| PG

應用服務透過 PgBouncer 連線池連到 PostgreSQL,減少直接連線數。備份腳本每天執行 pg_dump 並上傳到 MinIOPrometheus 透過 pg_exporter 收集效能指標。

核心概念

  1. 連線池(PgBouncer):PostgreSQL 的每個連線會 fork 一個 process,佔用約 10MB 記憶體。如果 10 個微服務各開 20 個連線,就是 200 個 process、2GB 記憶體。PgBouncer 作為中間層,維持少量的實際連線(例如 50 個),把應用端的大量連線請求排隊分配,大幅降低 PostgreSQL 的資源消耗。設定 pool_mode = transaction 是最常用的模式,每個 transaction 結束後連線就釋放回 pool。

  2. 備份策略:備份分兩種:邏輯備份(pg_dump)和物理備份(pg_basebackup + WAL archiving)。pg_dump 產出 SQL 或 custom format 的備份檔,優點是可以選擇性還原特定 table,缺點是大資料庫 dump 很慢。pg_basebackup 是整個資料目錄的複製,搭配 WAL archiving 可以做到 Point-in-Time Recovery(PITR),適合需要精確回復到某個時間點的場景。小型到中型資料庫(<50GB)用 pg_dump 足矣;超過 50GB 建議搭配 pg_basebackup

  3. postgresql.conf 調校:PostgreSQL 的預設設定非常保守(假設機器只有 128MB RAM),需要根據實際硬體調整。關鍵參數:shared_buffers(建議設為 RAM 的 25%)、work_mem(排序和 hash 操作的記憶體,建議 64-256MB)、effective_cache_size(告訴 query planner 有多少快取可用,建議 RAM 的 75%)、wal_buffers(WAL 寫入緩衝,建議 64MB)。不要把所有 RAM 都給 PostgreSQL,要留給 OS cache 和其他服務。

  4. 升級策略:PostgreSQL 的 minor version 升級(例如 15.4 → 15.5)只需要重啟,不需要動資料。Major version 升級(例如 15 → 16)需要用 pg_upgrade 或 dump/restore。建議在 staging 環境先執行升級、驗證應用相容性後再到 production 執行。升級前一定要做完整備份。

使用情境

  • 標準部署:一台 8GB RAM 的 Host 上部署 PostgreSQL,搭配 PgBouncer。API 服務和 Worker 服務透過 PgBouncer 連線,實際連線數控制在 50 個以內。每天凌晨 3 點 pg_dump 備份,上傳到 MinIO,保留 30 天。

  • 備份還原演練:每季做一次備份還原演練(Disaster Recovery Drill)。從 MinIO 下載最近的備份檔,在一台空白的 Host 上還原,驗證資料完整性。如果還原失敗,代表備份流程有問題,必須立即修復。不做演練的備份等於沒有備份。

  • 效能排查:應用回應變慢,懷疑是資料庫瓶頸。用 pg_stat_statements 找出最慢的查詢、用 EXPLAIN ANALYZE 分析執行計畫、用 Prometheus 看連線數和 cache hit ratio 是否異常。

實作範例 / 設定範例

PostgreSQL + PgBouncer 部署(docker-compose)

version: "3.8"
 
services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pg-data:/var/lib/postgresql/data
      - ./postgresql.conf:/etc/postgresql/postgresql.conf
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    command: postgres -c config_file=/etc/postgresql/postgresql.conf
    ports:
      - "127.0.0.1:5432:5432"  # 只允許 localhost 連線
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
 
  pgbouncer:
    image: edoburu/pgbouncer:latest
    restart: unless-stopped
    environment:
      DATABASE_URL: postgresql://myapp:${DB_PASSWORD}@postgres:5432/myapp
      POOL_MODE: transaction
      MAX_CLIENT_CONN: 200
      DEFAULT_POOL_SIZE: 50
      MIN_POOL_SIZE: 10
    ports:
      - "127.0.0.1:6432:6432"
    depends_on:
      postgres:
        condition: service_healthy
 
volumes:
  pg-data:

postgresql.conf 調校(8GB RAM Host)

# postgresql.conf - 8GB RAM 基線
 
# 連線
max_connections = 100
listen_addresses = '*'
 
# 記憶體
shared_buffers = 2GB           # RAM 的 25%
work_mem = 128MB               # 排序/hash 操作
maintenance_work_mem = 512MB   # VACUUM/CREATE INDEX
effective_cache_size = 6GB     # RAM 的 75%
wal_buffers = 64MB
 
# WAL
wal_level = replica            # 支援未來做 replication
max_wal_size = 2GB
min_wal_size = 1GB
 
# Query Planner
random_page_cost = 1.1         # SSD 環境
effective_io_concurrency = 200 # SSD 環境
 
# 日誌
logging_collector = on
log_directory = 'log'
log_filename = 'postgresql-%Y-%m-%d.log'
log_rotation_age = 1d
log_rotation_size = 100MB
log_min_duration_statement = 1000  # 記錄超過 1 秒的查詢
 
# 統計
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
 
# Autovacuum
autovacuum = on
autovacuum_max_workers = 3
autovacuum_naptime = 60

備份腳本

#!/bin/bash
# scripts/backup-postgresql.sh
set -euo pipefail
 
DB_HOST=${DB_HOST:-localhost}
DB_NAME=${DB_NAME:-myapp}
DB_USER=${DB_USER:-myapp}
BACKUP_DIR=/tmp/pg-backups
DATE=$(date +%Y-%m-%d_%H%M)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${DATE}.sql.gz"
 
mkdir -p "$BACKUP_DIR"
 
echo "=== Starting backup: $DB_NAME ==="
 
# pg_dump + gzip 壓縮
PGPASSWORD="$DB_PASSWORD" pg_dump \
  -h "$DB_HOST" \
  -U "$DB_USER" \
  -d "$DB_NAME" \
  -Fc \
  --no-owner \
  --no-privileges \
  | gzip > "$BACKUP_FILE"
 
FILESIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo "Backup created: $BACKUP_FILE ($FILESIZE)"
 
# 上傳到 MinIO
mc cp "$BACKUP_FILE" myminio/db-backups/"$(date +%Y/%m)"/
 
# 清理本地暫存
rm -f "$BACKUP_FILE"
 
echo "=== Backup complete and uploaded to MinIO ==="
# crontab - 每天凌晨 3 點備份
0 3 * * * /opt/scripts/backup-postgresql.sh >> /var/log/pg-backup.log 2>&1

還原操作

# 從 MinIO 下載備份
mc cp myminio/db-backups/2024/09/myapp_2024-09-14_0300.sql.gz /tmp/
 
# 還原到目標資料庫
gunzip /tmp/myapp_2024-09-14_0300.sql.gz
PGPASSWORD="$DB_PASSWORD" pg_restore \
  -h localhost \
  -U myapp \
  -d myapp_restore \
  --clean --create \
  /tmp/myapp_2024-09-14_0300.sql
 
# 驗證還原結果
psql -h localhost -U myapp -d myapp_restore -c "SELECT count(*) FROM users;"

常見問題與風險

  • 備份從來沒還原測試過:備份腳本跑了半年都沒問題,直到真的要還原時才發現備份檔損壞、或缺少必要的 extension。避免方式:每季做一次還原演練,在空白環境驗證備份的完整性。

  • 連線數爆掉:沒有用 PgBouncer,每個服務直接連 PostgreSQL。某天流量高峰時連線數超過 max_connections,新的連線全部被拒絕,應用服務批次報錯。避免方式:一律透過 PgBouncer 連線,設定合理的 DEFAULT_POOL_SIZE

  • 慢查詢拖垮整個資料庫:一個沒有加 index 的查詢做了 full table scan,佔住大量 I/O 和記憶體,影響其他正常查詢。避免方式:設定 log_min_duration_statement = 1000(記錄超過 1 秒的查詢)、啟用 pg_stat_statements 監控查詢效能、定期 review 慢查詢。

  • 磁碟空間不足:WAL 檔案和資料表增長超出磁碟容量,PostgreSQL 直接 crash 且無法啟動。避免方式:監控磁碟使用率、設定 max_wal_size 限制 WAL 大小、定期執行 VACUUM FULL 回收空間(注意 VACUUM FULL 會鎖表)。

  • Major version 升級失敗:直接在 production 做 major version 升級,結果某個 extension 不相容,資料庫起不來。避免方式:先在 staging 用相同的資料做升級測試、確認所有 extension 相容性、upgrade 前做完整備份。

優點

  • PostgreSQL 功能完整,支援 JSON、Full-text Search、PostGIS 等擴充
  • PgBouncer 有效控制連線數,減少資源消耗
  • pg_dump 備份簡單直觀,小型資料庫夠用

缺點 / 限制

  • 單節點沒有 High Availability,掛了就要手動還原
  • pg_dump 大型資料庫(>50GB)備份和還原時間很長
  • Major version 升級需要停機,對 24/7 服務有影響

延伸閱讀