适用场景:把 PostgreSQL / MySQL / Redis / MongoDB / ClickHouse 跑进 Docker,却担心「数据丢、高可用、备份、主从、升级、性能」——这篇给生产级答案
核心结论:数据库可以容器化,但必须:持久化卷 + 固定网络 + 资源限制 + 定时备份(含 PITR)+ 监控 + 明确升级策略
组件:CloudNativePG(PG Operator)/ Percona(MySQL)/ Bitnami Chart / Wal-G / PBM / 连接池(PgBouncer / ProxySQL)
—
0. 先回答灵魂拷问:数据库到底该不该容器化?
| 维度 | 容器化 | 裸机/系统包 | 结论 |
| 部署速度 | 秒级 docker run | 分钟级手动 | ✅ 容器快 |
| 版本隔离/多实例 | 多容器多版本并存 | 需编译或虚拟化 | ✅ 容器强 |
| 数据安全 | ⚠️ 需正确挂卷+备份 | 直接落盘 | ⚠️ 裸机省心 |
| 高可用/集群 | ✅ Operator 一键 | 手动搭 | ✅ 容器强(K8s) |
| 性能损耗 | 1-3%(host 网络/裸盘挂载) | 无 | ≈ 持平 |
| 升级/回滚 | 改 tag 重跑(有风险) | 包管理 | ⚠️ 各有风险 |
| 团队技能 | 需懂 Docker/K8s | 传统 DBA | ⚠️ 看团队 |
建议:生产单机/小规模 → Docker Compose + 固定卷 + 定时备份;需要 HA/自愈/多租户 → K8s Operator(CloudNativePG / Percona)。纯裸机跑唯一数据库且不折腾 → 系统包也行。
—
1. PostgreSQL 容器化(CloudNativePG Operator,生产首选)
1.1 为什么选 CloudNativePG(而非普通 Deployment)
| 特性 | 普通 docker run | CloudNativePG |
| 主从复制(流复制) | 手动配置 | Operator 全自动 |
| 自动故障转移 | ❌ 手搓 | ✅ 自动 promote |
| 备份/恢复(PITR) | 手动 pg_dump | ✅ Wal-G + S3 + 时间点恢复 |
| 滚动升级 | 停机 or 手动 | ✅ Operator 调度 |
| 监控指标 | 手动 | ✅ 内置 cnpg exporter |
1.2 安装 Operator + 部署集群
# 1. 安装 CloudNativePG Operator(k3s 集群内)
kubectl apply -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.24/releases/cnpg-1.24.0.yaml
# 2. 创建 PostgreSQL 集群(3 实例 + 自动备份到 S3/MinIO)
kubectl apply -f - <<'EOF'
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg-cluster
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16.3
storage:
size: 100Gi
storageClass: longhorn
postgresql:
parameters:
max_connections: "200"
shared_buffers: "2GB"
effective_cache_size: "6GB"
work_mem: "16MB"
maintenance_work_mem: "512MB"
random_page_cost: "1.1"
effective_io_concurrency: "200"
bootstrap:
initdb:
database: app
owner: app
secret:
name: pg-secret
backup:
barmanObjectStore:
destinationPath: s3://pg-backups/
endpointURL: http://minio.local:9000
s3Credentials:
accessKeyId:
name: minio-secret
key: access-key
secretAccessKey:
name: minio-secret
key: secret-key
wal:
compression: gzip
retentionPolicy: "30d"
monitoring:
enablePodMonitor: true
EOF
1.3 连接与验证
# 获取连接信息
kubectl get secret pg-cluster-app -o jsonpath='{.data.password}' | base64 -d
# 主从状态
kubectl get cluster pg-cluster
kubectl get pods -l cnpg.io/cluster=pg-cluster
# 故障转移演练:删主 Pod → Operator 自动 promote 从库
kubectl delete pod pg-cluster-1
kubectl get pods -w # 观察自动切换
1.4 PITR(时间点恢复)
# 恢复到某时间点(如误删数据前 1 小时)
kubectl apply -f - <<'EOF'
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg-cluster-restore
spec:
instances: 1
bootstrap:
recovery:
source: pg-cluster
recoveryTarget:
targetTime: "2026-09-13T10:00:00Z"
storage:
size: 100Gi
EOF
—
2. 单机 Docker Compose 数据库(家庭/小型生产)
2.1 PostgreSQL 单实例 + PgBouncer 连接池
services:
postgres:
image: postgres:16-alpine
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${PG_PASSWORD}
POSTGRES_DB: app
TZ: Asia/Shanghai
volumes:
- /data/postgres/data:/var/lib/postgresql/data
- /data/postgres/backup:/backup
# 连接池 + 避免 OOM
deploy:
resources:
limits:
memory: 2G
cpus: "2"
command:
- "postgres"
- "-c", "max_connections=200"
- "-c", "shared_buffers=512MB"
- "-c", "effective_cache_size=1.5GB"
- "-c", "work_mem=16MB"
- "-c", "maintenance_work_mem=256MB"
- "-c", "wal_level=replica"
- "-c", "archive_mode=on"
- "-c", "archive_command=test ! -f /backup/archive/%f && cp %p /backup/archive/%f"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
pgbouncer:
image: edoburu/pgbouncer:latest
container_name: pgbouncer
restart: unless-stopped
environment:
DB_USER: app
DB_PASSWORD: ${PG_PASSWORD}
DB_HOST: postgres
DB_NAME: app
POOL_MODE: transaction
MAX_CLIENT_CONN: 500
DEFAULT_POOL_SIZE: 50
ports:
- "6432:5432"
depends_on:
postgres:
condition: service_healthy
2.2 MySQL 单实例 + 主从(Bitnami Charts 或原生)
services:
mysql:
image: mysql:8.4
container_name: mysql
restart: unless-stopped
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-authentication-plugin=mysql_native_password
- --max_connections=300
- --innodb-buffer-pool-size=1G
- --innodb-flush-log-at-trx-commit=1
- --binlog-format=ROW
- --gtid-mode=ON
- --enforce-gtid-consistency=ON
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: app
MYSQL_USER: app
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Shanghai
volumes:
- /data/mysql/data:/var/lib/mysql
- /data/mysql/backup:/backup
- /data/mysql/conf:/etc/mysql/conf.d
deploy:
resources:
limits: {memory: 2G, cpus: "2"}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
—
3. Redis 容器化(主从 + 哨兵 / Cluster)
3.1 单实例
services:
redis:
image: redis:7-alpine
container_name: redis
restart: unless-stopped
command: [
"redis-server",
"--appendonly", "yes",
"--appendfsync", "everysec",
"--maxmemory", "1gb",
"--maxmemory-policy", "allkeys-lru",
"--save", "900", "1", "300", "10", "60", "10000"
]
volumes:
- /data/redis/data:/data
deploy:
resources:
limits: {memory: 1.2G, cpus: "1"}
3.2 主从 + 哨兵(高可用)
services:
redis-master:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"]
volumes: [/data/redis/master:/data]
redis-replica:
image: redis:7-alpine
command: ["redis-server", "--replicaof", "redis-master", "6379", "--masterauth", "${REDIS_PASSWORD}", "--appendonly", "yes"]
volumes: [/data/redis/replica:/data]
depends_on: [redis-master]
sentinel-1:
image: redis:7-alpine
command: ["redis-sentinel", "/etc/redis/sentinel.conf"]
volumes:
- /data/redis/sentinel1:/data
- ./sentinel.conf:/etc/redis/sentinel.conf
sentinel-2:
image: redis:7-alpine
command: ["redis-sentinel", "/etc/redis/sentinel.conf"]
volumes: [/data/redis/sentinel2:/data, ./sentinel.conf:/etc/redis/sentinel.conf]
sentinel-3:
image: redis:7-alpine
command: ["redis-sentinel", "/etc/redis/sentinel.conf"]
volumes: [/data/redis/sentinel3:/data, ./sentinel.conf:/etc/redis/sentinel.conf]
# sentinel.conf
port 26379
sentinel monitor mymaster redis-master 6379 2
sentinel auth-pass mymaster ${REDIS_PASSWORD}
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
—
4. MongoDB 容器化(副本集)
services:
mongo1:
image: mongo:7.0
container_name: mongo1
restart: unless-stopped
command: ["mongod", "--replSet", "rs0", "--bind_ip_all", "--wiredTigerCacheSizeGB", "1"]
volumes: [/data/mongo1:/data/db]
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
mongo2:
image: mongo:7.0
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
volumes: [/data/mongo2:/data/db]
mongo3:
image: mongo:7.0
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
volumes: [/data/mongo3:/data/db]
# 初始化副本集
docker exec -it mongo1 mongosh --eval '
rs.initiate({_id:"rs0", members:[
{_id:0, host:"mongo1:27017"},
{_id:1, host:"mongo2:27017"},
{_id:2, host:"mongo3:27017"}
]})'
—
5. ClickHouse / OLAP 容器化(可选)
services:
clickhouse:
image: clickhouse/clickhouse-server:24.6
container_name: clickhouse
restart: unless-stopped
ulimits:
nofile: {soft: 262144, hard: 262144}
ports:
- "8123:8123" # HTTP
- "9000:9000" # native
volumes:
- /data/clickhouse/data:/var/lib/clickhouse
- /data/clickhouse/log:/var/log/clickhouse-server
environment:
CLICKHOUSE_DB: analytics
CLICKHOUSE_USER: app
CLICKHOUSE_PASSWORD: ${CH_PASSWORD}
—
6. 备份策略(比数据库本身更重要)
6.1 PostgreSQL:逻辑 + 物理(PITR)
# 逻辑备份(每日全量,快速恢复单库表)
docker exec postgres pg_dump -U app -Fc app > /backup/app_$(date +%F).dump
# 物理备份(WAL 归档 + 基础备份,支持任意时间点恢复)
docker exec postgres pg_basebackup -U postgres -D /backup/base_$(date +%F) -Ft -z -X fetch
# WAL 归档(配 archive_command,见上文 compose)
# 恢复:先恢复基础备份,再 replay WAL 到目标时间点
docker run --rm -v /backup:/backup -v /restore:/restore postgres:16-alpine \
pg_restore -U app -d app /restore/app_2026-09-13.dump
6.2 MySQL:mysqldump + binlog(PITR)
# 全量
docker exec mysql mysqldump -uroot -p${MYSQL_ROOT_PASSWORD} --single-transaction --master-data=2 --all-databases > /backup/mysql_$(date +%F).sql
# binlog 持续归档(配 expire_logs_days、binlog_expire_logs_seconds)
# PITR:恢复全量 → mysqlbinlog 回放 binlog 到目标时间点
6.3 Redis:RDB + AOF 双写
# 配置里 combined: appendonly yes + save 定期 RDB
# 备份:直接拷贝 dump.rdb + appendonly.aof
docker exec redis redis-cli BGSAVE
cp /data/redis/data/dump.rdb /backup/redis_dump_$(date +%F).rdb
6.4 MongoDB:mongodump + oplog
docker exec mongo1 mongodump --host rs0/mongo1:27017,mongo2:27017,mongo3:27017 --oplog --out /backup/mongo_$(date +%F)
6.5 集中备份调度(统一脚本 + systemd timer / cron)
#!/usr/bin/env bash
# /usr/local/bin/backup-all-db.sh
set -euo pipefail
BACKUP_DIR="/data/backup/db"
RETENTION_DAYS=30
echo "=== 数据库备份 $(date -Is) ==="
mkdir -p "$BACKUP_DIR"/{postgres,mysql,redis,mongo}
# PostgreSQL
docker exec postgres pg_dump -U app -Fc app | gzip > "$BACKUP_DIR/postgres/app_$(date +%F).dump.gz"
# MySQL
docker exec mysql mysqldump -uroot -p"${MYSQL_ROOT_PASSWORD}" --all-databases --single-transaction | gzip > "$BACKUP_DIR/mysql/all_$(date +%F).sql.gz"
# Redis
docker exec redis redis-cli BGSAVE
cp /data/redis/data/dump.rdb "$BACKUP_DIR/redis/dump_$(date +%F).rdb"
# MongoDB
docker exec mongo1 mongodump --oplog --out /tmp/mongo_dump
tar -czf "$BACKUP_DIR/mongo/mongo_$(date +%F).tar.gz" -C /tmp mongo_dump
# 清理 30 天前
find "$BACKUP_DIR" -type f -mtime +$RETENTION_DAYS -delete
# 同步到远程(S3/另一台 NAS)
rclone sync "$BACKUP_DIR" remote-s3:db-backups --retries 3
echo "=== 完成 ==="
echo "0 3 * * * /usr/local/bin/backup-all-db.sh" >> /etc/crontabs/root
/etc/init.d/cron restart
—
7. 监控与告警(数据库必须盯的指标)
| 指标 | 说明 | 告警阈值参考 |
| 连接数 | pg_stat_activity / Threads_connected | > 80% max_connections |
| 慢查询 | log_min_duration_statement / slow_query_log | > 1s 或按业务 |
| 缓冲池命中率 | pg_buffercache / Innodb_buffer_pool_read_requests | < 95% |
| WAL/Binlog 落后 | pg_wal_lsn_diff / Seconds_Behind_Master | > 60s |
| 磁盘使用率 | 数据目录 + WAL + 备份 | > 80% |
| 锁等待 | pg_locks / innodb_lock_waits | > 10s |
| 复制延迟 | 主从 lag | > 30s |
| 死锁/回滚率 | pg_stat_database.deadlocks / Innodb_row_lock_waits | 增长趋势 |
# postgres_exporter + mysqld_exporter + redis_exporter(Prometheus)
services:
postgres-exporter:
image: prometheuscommunity/postgres-exporter:latest
environment:
DATA_SOURCE_URI: "postgres:5432/app?sslmode=disable"
DATA_SOURCE_USER: app
DATA_SOURCE_PASS: ${PG_PASSWORD}
ports: ["9187:9187"]
mysqld-exporter:
image: prom/mysqld-exporter:latest
environment:
DATA_SOURCE_NAME: "app:${MYSQL_PASSWORD}@(mysql:3306)/"
ports: ["9104:9104"]
redis-exporter:
image: oliver006/redis_exporter:latest
environment:
REDIS_ADDR: "redis://redis:6379"
REDIS_PASSWORD: ${REDIS_PASSWORD}
ports: ["9121:9121"]
—
8. 升级策略(最容易翻车的环节)
| 数据库 | 安全升级路径 | 危险操作 |
| PostgreSQL | pg_dumpall → 新版本容器 → 导入,或 pg_upgrade | 直接改镜像 tag 原地升级(大版本) |
| MySQL | mysqldump → 新版本,或 mysql_upgrade | 跳过 2+ 个大版本 |
| Redis | 先 BGSAVE,改 tag 重启(小版本),RDB 兼容 | 混用不同版本 AOF |
| MongoDB | 逐版本升级(5→6→7),cannot skip | 直接 5→7 |
# PostgreSQL 大版本升级(16 → 17)安全流程
# 1. 全量备份
docker exec postgres pg_dumpall -U postgres > /backup/all_$(date +%F).sql
# 2. 起新版本容器(新数据目录)
docker run -d --name postgres17 -v /data/postgres17:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=xxx postgres:17-alpine
# 3. 导入
docker exec -i postgres17 psql -U postgres < /backup/all_$(date +%F).sql
# 4. 验证后切换
docker stop postgres && docker rename postgres postgres16
docker rename postgres17 postgres
—
9. 常见坑 & 避坑指南
| 现象 | 原因 | 修正 |
| 容器重启后数据丢失 | 没挂卷,数据在容器层 | 必须 volumes: 挂到宿主机/命名卷 |
| 数据库 OOM 被杀 | 无内存限制,max_connections 过大 | 设 deploy.resources.limits.memory,调参 shared_buffers/innodb_buffer_pool |
| 主从延迟高 | 备库性能差 / 网络抖动 | 提高备库配置、synchronous_commit=remote_apply(数据零丢失) |
| PITR 恢复失败 | WAL/Binlog 未归档或过期 | 配 archive_command/binlog_expire,定期演练恢复 |
容器 /var/lib/mysql 权限错 | 挂载空目录 SELinux 上下文不对 | :Z/:z 挂载或 chcon -Rt svirt_sandbox_file_t |
| 连接数打满 | 没上连接池 | PgBouncer / ProxySQL,max_connections 调到 200,池化到 1000+ |
| 备份文件损坏 | 未校验 | 定期 pg_restore --list / mysql -e "CHECK TABLE" 验证 |
—
10. 资源 & 参考
—
(完)