SLIDE 66: Backups: Strategy
Backup Types and Trade-offs
Backup Types:
Backup Strategy (Typical):
- Full backup: Weekly (Sunday 2am)
- Incremental: Daily
- WAL archiving: Continuous
- Retention: 30 days
Recovery Point Objective (RPO):
Maximum acceptable data loss:
- RPO = 1 hour: Backup every hour
- RPO = 0: Synchronous replication + WAL archiving
SLIDE 67: Backups: Strategy (continued)
Recovery Time Objective (RTO):
Maximum acceptable downtime:
- RTO = 4 hours: Restore from backup is acceptable
- RTO = 1 minute: Need hot standby with automatic failover
Backup Storage:
3-2-1 Rule:
- 3 copies of data (primary + 2 backups)
- 2 different storage types (disk + object storage)
- 1 offsite location (different region)
SLIDE 68: Backups: Testing Recovery
The Most Important Practice
Why Testing Matters:
Industry statistics:
- 34% of companies do not test backups
- 77% of those that test find failures
- Average cost of data loss: $150,000 per incident
What to Test:
SLIDE 69: Backups: Testing Recovery (continued)
Full Restore Drill:
Step 1: Spin up new database instance
Step 2: Restore from latest backup
Step 3: Apply WAL logs to bring up to date
Step 4: Verify data integrity (checksums, row counts)
Step 5: Run application test suite against restored DB
Step 6: Measure total time (compare to RTO)
Step 7: Document results and issues
Automated Backup Verification:
def verify_backup():
# Restore to test environment
restore_db = restore_backup(latest_backup, target="test-db")
# Run integrity checks
assert restore_db.checksum() == expected_checksum
assert restore_db.row_count("users") > 0
# Run sample queries
assert restore_db.query("SELECT COUNT(*) FROM orders") > 0
# Cleanup
restore_db.destroy()
alert_success("Backup verification passed")
Key Insight:
A backup that has not been tested is not a backup. It is a hope.
SLIDE 70: Backups: Point-in-Time Recovery
Recovering to a Specific Moment
Scenario:
Timeline:
- 2:00 AM: Full backup completed
- 9:00 AM: Normal operations
- 9:15 AM: Developer runs DELETE without WHERE clause
- 9:16 AM: 10 million rows deleted
- 9:20 AM: Team notices
- Goal: Restore to 9:14 AM
Point-in-Time Recovery (PITR):
PostgreSQL PITR:
# Restore base backup
pg_restore --dbname=restored /backups/base_backup.sql
# Apply WAL logs up to specific time
recovery_target_time = '2025-01-15 09:14:00'
SLIDE 71: Backups: Point-in-Time Recovery (continued)
Requirements for PITR:
PITR Trade-offs:
Production Practice:
- Enable WAL archiving on all production databases
- Ship WAL to durable storage every 1 minute
- Retain WAL for at least 7 days
- Test PITR quarterly
SLIDE 72: Backups: Disaster Recovery
Planning for the Worst
Disaster Scenarios:
Multi-Region Setup:
SLIDE 73: Backups: Disaster Recovery (continued)
DR Runbook:
DATACENTER FAILURE RUNBOOK
1. DETECT (0-5 minutes)
- Monitoring alerts on region failure
- Oncall acknowledges alert
2. ASSESS (5-15 minutes)
- Confirm failure scope
- Estimate recovery time of primary
- Decision: Wait vs Failover
3. FAILOVER (15-30 minutes)
- Promote DR replica to primary
- Update DNS/load balancer
- Verify application connectivity
4. VERIFY (30-45 minutes)
- Run smoke tests
- Monitor error rates
- Confirm data consistency
5. COMMUNICATE (Ongoing)
- Status page update
- Customer notification
- Internal updates
DR Testing:
SLIDE 74: Data in Production: Anti-Patterns
Common Mistakes to Avoid
Anti-Pattern 1: Schema Changes in Application Deployment
Bad:
1. Deploy code that expects new column
2. Run migration to add column
3. Application errors for 30 seconds
Good:
1. Run migration to add column
2. Wait for replication
3. Deploy code that uses new column
Anti-Pattern 2: Large Transactions
-- Bad: One transaction with 1M updates
BEGIN;
UPDATE users SET status = 'active'; -- 1M rows
COMMIT;
-- Good: Small batches, separate transactions
UPDATE users SET status = 'active' WHERE id BETWEEN 1 AND 10000;
UPDATE users SET status = 'active' WHERE id BETWEEN 10001 AND 20000;
-- etc.
SLIDE 75: Data in Production: Anti-Patterns (continued)
Anti-Pattern 3: Ignoring Replication Lag
# Bad: Assumes instant replication
def create_and_read(data):
db.insert(data)
return db.query(data.id) # May not exist on replica
# Good: Read from leader after write
def create_and_read(data):
db.insert(data)
return leader_db.query(data.id)
Anti-Pattern 4: No Backup Testing
"We have backups" is not the same as "We can restore from backups"
Anti-Pattern 5: Unbounded Queries
-- Bad: Could return billions of rows
SELECT * FROM events;
-- Good: Always limit
SELECT * FROM events WHERE created_at > NOW() - INTERVAL '1 day' LIMIT 1000;
SLIDE 76: Data in Production: Summary
Key Takeaways
Schema Evolution:
- Separate schema changes from code deployments
- Use expand-contract pattern for renames
- Add columns as nullable, then backfill
- Remove columns only after code no longer references them
Migrations:
- Batch large updates (1,000-10,000 rows)
- Use online schema change tools for large tables
- Monitor replication lag during migrations
- Always have a rollback plan
Replication:
- Understand sync vs async trade-offs
- Implement read-your-writes consistency where needed
- Plan for failover (it will happen)
- Test failover regularly
SLIDE 77: Data in Production: Summary (continued)
Consistency:
- Use strong consistency for financial/transactional data
- Use eventual consistency for high-scale reads
- Implement compensating transactions for distributed operations
- Design for acceptable staleness, not zero staleness
Hot Partitions:
- Monitor for uneven shard distribution
- Cache hot data aggressively
- Use key salting for write-heavy keys
- Add read replicas for hot shards
Backups:
- Follow 3-2-1 rule (3 copies, 2 types, 1 offsite)
- Test recovery regularly (quarterly full restore)
- Enable point-in-time recovery
- Document and practice DR runbooks
Coming Up Next: Section 4 - Caching, Queues, and Async Workflows