Disaster Recovery Plan
RPO, RTO, PITR cho PostgreSQL và MongoDB.
Hãy hình dung bạn đang điều hành một ngân hàng. Đột nhiên, máy chủ database bốc cháy lúc 2 giờ sáng. Bạn sẽ làm gì? — DRP chính là tập hợp các câu trả lời có sẵn cho câu hỏi đó, được viết ra và kiểm tra trước khi thảm họa xảy ra.
Tại Sao DRP Quan Trọng Với PostgreSQL & MongoDB?
Ví Dụ Minh Họa Cụ Thể
Khi Nào Cần RPO Ngắn? Khi Nào Chấp Nhận RPO Dài?
Mỗi giao dịch đều có giá trị tiền tệ thực. Mất 1 giờ data = mất các lệnh chuyển tiền, thanh toán đã xảy ra trong 1 giờ đó. Không thể "tái tạo" lại được từ nguồn khác. RPO phải gần 0 — dùng synchronous replication, mọi write phải được confirm bởi ít nhất 2 node.
PostgreSQL: synchronous_commit = on | MongoDB: w: "majority", j: true
Đơn hàng, user data, session — mất 1–4 giờ data là có thể xử lý được (customer service liên hệ lại, order được đặt lại). Chi phí cho RPO = 0 không justify với loại business này. RPO 15 phút đến 1 giờ thường là sweet spot.
Data có thể rebuild từ nguồn khác (re-process log files, re-run ETL jobs). Mất 24 giờ log analytics không gây ra business impact trực tiếp. Daily backup là đủ, tiết kiệm đáng kể chi phí storage và compute.
RTO Ảnh Hưởng Chi Phí & Kiến Trúc Như Thế Nào?
Công cụ: PostgreSQL Patroni, Pacemaker; MongoDB Replica Set với automatic election.
Chi phí: Gần gấp đôi (phải duy trì hardware standby ở 100% capacity liên tục).
Phù hợp: 80% use cases doanh nghiệp vừa và nhỏ. Chi phí chấp nhận được.
Yêu cầu: Backup phải được test thường xuyên (ít nhất monthly).
Cách PITR Hoạt Động
PITR hoạt động bằng cách kết hợp base backup với transaction logs (WAL trong PostgreSQL, Oplog trong MongoDB). Transaction log ghi lại mọi thay đổi theo thứ tự thời gian — bằng cách replay từng log entry từ base backup đến thời điểm cần thiết, database được đưa về đúng trạng thái đó.
Use Cases Thực Tế — Khi Nào Dùng PITR?
Tình huống: DROP TABLE orders; — 200,000 đơn hàng biến mất. Không có PITR, phải restore từ backup đêm qua, mất toàn bộ dữ liệu hôm nay.
Với PITR: Restore database về 15:41:59 — chỉ mất chưa đến 1 phút, toàn bộ đơn hàng hôm nay vẫn còn nguyên.
# recovery.conf (hoặc postgresql.conf từ v12+) restore_command = 'cp /mnt/wal_archive/%f %p' recovery_target_time = '2024-01-15 15:41:59' recovery_target_action = 'promote' # Tự promote sau khi đến điểm target
Tình huống: Deploy version mới lúc 09:00, bug làm tất cả giá sản phẩm bị nhân 10 lần. Phát hiện lúc 12:00, nhưng không biết chính xác row nào bị ảnh hưởng.
Với PITR: Restore một bản sao database về 08:59 (trước khi deploy), chạy query để extract giá đúng, sau đó UPDATE production từ bản sao đó — không cần downtime toàn bộ hệ thống.
Tình huống: Ransomware encrypt database tại 03:00, phát hiện lúc 06:00. Backup đêm qua được store offline nên không bị ảnh hưởng.
Với PITR: Restore từ offline backup 00:00 + replay WAL archives (cũng offline) đến 02:59:59 — chỉ mất 3 tiếng data, thay vì mất nguyên một ngày. Lưu ý quan trọng: WAL/Oplog archive phải lưu ở nơi khác với primary database (S3, offline storage).
Điều Kiện Cần Để Thực Hiện PITR
- Base backup đầy đủ và còn đọc được
- WAL files liên tục từ base backup đến target time (PostgreSQL)
- Oplog đủ lớn và chứa đủ history (MongoDB)
- Storage để lưu WAL archive (S3, GCS, NFS...)
- Test restore ít nhất 1 lần/tháng để đảm bảo backup hợp lệ
- WAL archiving không được bật (wal_level = minimal)
- WAL files bị xóa hoặc corrupt trước thời điểm target
- Oplog đã rotate qua thời điểm cần (oplog too small)
- Base backup sau target time (không thể đi ngược thời gian)
- Archive storage bị ransomware cùng lúc với primary
RTO = nhìn về tương lai — "Bao lâu đến khi hệ thống chạy lại?" (downtime)
Ví dụ ngân hàng:
RPO = 0 (không mất giao dịch nào) + RTO = 30 phút (cho phép down 30 phút khi failover)
| Tiêu chí | PostgreSQL 🐘 | MongoDB 🍃 |
|---|---|---|
| PITR mechanism | WAL (Write-Ahead Log) archiving + pg_basebackup | Oplog (operations log) trong Replica Set |
| Bật PITR | wal_level=replica, archive_mode=on | Tự động khi dùng Replica Set (không cần config) |
| Oplog/WAL size | Không giới hạn (tùy disk, archive ra S3) | Mặc định 5% disk, tối đa 50GB — cần monitor! |
| Backup tool chính | pg_basebackup, pg_dump, pgBackRest, Barman | mongodump, mongorestore, Atlas Backup, Ops Manager |
| Hot standby | Streaming replication (synchronous/async) | Replica Set (tối đa 50 members) |
| Auto failover | Cần Patroni / Pacemaker (không built-in) | Built-in tự động election (~ 10-30s) |
| RPO tốt nhất | ~0 với synchronous_commit=on | ~0 với w:"majority", j:true |
| RTO tốt nhất | ~30s với Patroni auto-failover | ~10-30s với Replica Set election |
| Point-in-time precision | Đến từng giây (recovery_target_time) | Đến từng giây (oplog timestamp) |
| Cloud managed | AWS RDS/Aurora, GCP Cloud SQL, Azure | MongoDB Atlas (PITR built-in) |
PostgreSQL — Công Cụ & Config DR
# ── Bật WAL Archiving (bắt buộc cho PITR) ───────────────── wal_level = replica # Tối thiểu để archive (không dùng minimal!) archive_mode = on archive_command = 'aws s3 cp %p s3://my-pg-wal-archive/%f' archive_timeout = 60 # Tối đa 60 giây WAL không archive (force switch) # ── Streaming Replication ────────────────────────────────── max_wal_senders = 5 # Tối đa 5 standbys synchronous_commit = on # RPO~0: chờ standby confirm trước khi return synchronous_standby_names = 'standby1' # ── Base Backup Command ─────────────────────────────────── # pg_basebackup -h localhost -U replicator -D /backup/base -Ft -z -P -Xs
# ── PostgreSQL v15+ (recovery.conf đã merge vào postgresql.conf) ─ restore_command = 'aws s3 cp s3://my-pg-wal-archive/%f %p' recovery_target_time = '2024-01-15 14:36:59+07' # Target timestamp recovery_target_action = 'promote' # Tự promote sau recovery recovery_target_inclusive = true # Bao gồm transaction tại target time # Hoặc dùng transaction ID thay vì time: # recovery_target_xid = '12345678' # Sau khi chỉnh sửa, tạo file signal: # touch /var/lib/postgresql/data/recovery.signal # systemctl start postgresql
MongoDB — Công Cụ & Config DR
// ── Khởi tạo Replica Set ────────────────────────────────── rs.initiate({ _id: "rs0", members: [ { _id: 0, host: "mongo1:27017", priority: 2 }, // Primary preferred { _id: 1, host: "mongo2:27017", priority: 1 }, // Secondary { _id: 2, host: "mongo3:27017", priority: 0, // Hidden secondary — backup only hidden: true, votes: 1, slaveDelay: 3600 } // 1h delay = buffer cho human error ] }); // ── Write Concern cho RPO~0 ──────────────────────────────── // Mọi write phải được confirm bởi majority trước khi return db.runCommand({ setDefaultRWConcern: 1, defaultWriteConcern: { w: "majority", j: true }, defaultReadConcern: { level: "majority" } }); // ── Kiểm tra Oplog size ─────────────────────────────────── rs.printReplicationInfo(); // configured oplog size: 51200 MB // log length start to end: 604800 secs (7 days) ← RPO window
# ── Step 1: Restore từ base backup ─────────────────────── mongorestore --host localhost:27017 --gzip --archive=/backup/mongodump-2024-01-15-00-00.archive --drop # Drop existing collections trước khi restore # ── Step 2: Replay Oplog đến target time ───────────────── # Lấy oplog từ secondary có delay hoặc backup mongodump --host mongo2:27017 --db=local --collection=oplog.rs --query='{"ts":{"$gte":{"$timestamp":{"t":1705248000,"i":1}}}}' --out=/tmp/oplog_dump/ # Rename để mongorestore nhận ra là oplog replay mv /tmp/oplog_dump/local/oplog.rs.bson /tmp/oplog_dump/oplog.bson # Replay oplog đến đúng timestamp (Unix epoch) mongorestore --host localhost:27017 --oplogReplay --oplogLimit=1705282619:1 # 2024-01-15 14:36:59 UTC /tmp/oplog_dump/
PostgreSQL — Kiểm Tra Trạng Thái DR
PostgreSQL — Backup & PITR
# Tạo full backup pgbackrest --stanza=main backup --type=full # Tạo incremental backup (dùng WAL để chỉ backup phần thay đổi) pgbackrest --stanza=main backup --type=diff # Verify backup integrity pgbackrest --stanza=main check # PITR restore về thời điểm cụ thể pgbackrest --stanza=main restore --target-action=promote --type=time --target="2024-01-15 14:36:59" # Xem thông tin backup có sẵn pgbackrest --stanza=main info
MongoDB — Kiểm Tra Trạng Thái DR
MongoDB — Backup
# Full backup với oplog (PHẢI có --oplog để consistent) mongodump --host rs0/mongo1:27017,mongo2:27017 --oplog --gzip --archive=/backup/mongodump-$(date +%Y%m%d-%H%M%S).archive # Restore từ archive mongorestore --host localhost:27017 --gzip --archive=/backup/mongodump-20240115-000000.archive --oplogReplay --drop # Kiểm tra backup hợp lệ (không cần restore full) mongorestore --host localhost:27018 # Port khác để test --archive=/backup/mongodump.archive --gzip --dryRun # Dry run — không ghi thực
☐ Bật WAL archiving (PostgreSQL) hoặc Replica Set (MongoDB)
☐ Backup chạy định kỳ và được lưu offsite (S3/GCS khác region)
☐ Monitor replication lag (alert khi > 60 giây)
☐ Monitor oplog window (MongoDB) — phải > RPO target
☐ Test restore hàng tháng và ghi lại thời gian thực tế
☐ Có runbook bằng văn bản cho mọi DR scenario
☐ Ít nhất 2 người trong team biết quy trình restore