Docker数据管理与存储

📌 本文由 23 篇相关文章智能合并整理而成

恢复数据卷

恢复数据卷

为什么需要恢复数据卷

Docker 数据卷(Volume)是持久化数据的主要方式。当出现以下情况时需要恢复:
– 误删了 Volume
– 容器被误删
– 需要在不同环境中迁移数据

Volume 的位置

# Docker 管理的 volume 默认位置
/var/lib/docker/volumes//_data/

# 查看所有 volume
docker volume ls

# 查看 volume 详情
docker volume inspect 

恢复场景

场景一:误删 Volume(有备份)

# 假设有备份文件 backup-data.tar.gz

# 1. 重新创建 volume
docker volume create mydata

# 2. 启动临时容器恢复数据
docker run --rm \
  -v mydata:/data \
  -v $(pwd):/backup \
  ubuntu \
  tar xzf /backup/backup-data.tar.gz -C /data

# 3. 验证数据
docker run --rm -v mydata:/data ubuntu ls -la /data

场景二:误删容器但 Volume 还在

# 检查 volume 是否还存在
docker volume ls | grep mydata

# 查看 volume 中的数据
docker run --rm -v mydata:/data ubuntu ls -la /data

# 重建容器并挂载原 volume
docker run -d \
  --name myapp \
  -v mydata:/app/data \
  myapp:latest

场景三:从另一个 Docker 主机迁移

# 源主机:导出 volume
docker run --rm \
  -v mydata:/data \
  -v $(pwd):/backup \
  ubuntu \
  tar czf /backup/mydata-backup.tar.gz /data/

# 传输备份文件到目标主机
scp mydata-backup.tar.gz target-host:/tmp/

# 目标主机:导入 volume
docker volume create mydata
docker run --rm \
  -v mydata:/data \
  -v /tmp:/backup \
  ubuntu \
  tar xzf /backup/mydata-backup.tar.gz -C /data

备份策略

手动备份

#!/bin/bash
# backup_volumes.sh

BACKUP_DIR="/backup/volumes"
DATE=$(date +%Y%m%d)

# 备份所有 volume
for volume in $(docker volume ls -q); do
  echo "Backing up $volume..."
  docker run --rm \
    -v $volume:/data \
    -v $BACKUP_DIR:/backup \
    ubuntu \
    tar czf /backup/${volume}_${DATE}.tar.gz /data/
done

定时备份(crontab)

# 每天凌晨 2 点备份
0 2 * * * /opt/scripts/backup_volumes.sh

自动备份脚本

#!/bin/bash
# backup_and_rotate.sh

BACKUP_DIR="/backup/volumes"
RETENTION_DAYS=30

# 备份
docker run --rm \
  -v myapp-data:/data \
  -v $BACKUP_DIR:/backup \
  ubuntu \
  tar czf /backup/mydata_$(date +%Y%m%d_%H%M%S).tar.gz /data/

# 清理过期备份
find $BACKUP_DIR -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete

恢复数据库 Volume

MySQL

# 备份
docker exec mysql-container \
  mysqldump -u root -p$PASSWORD --all-databases > backup.sql

# 恢复
docker exec -i mysql-container \
  mysql -u root -p$PASSWORD < backup.sql

# 或者使用 volume 方式
docker run --rm \
  -v mysql_data:/var/lib/mysql \
  -v $(pwd):/backup \
  ubuntu \
  tar xzf /backup/mysql_backup.tar.gz -C /

PostgreSQL

# 备份
docker exec pg-container pg_dumpall -U postgres > backup.sql

# 恢复
cat backup.sql | docker exec -i pg-container psql -U postgres

使用 Volume 驱动备份

使用 Restic

# 安装 restic 并配置 s3 后端
docker run --rm \
  -v mydata:/data \
  -e RESTIC_REPOSITORY=s3:s3.amazonaws.com/bucket-name \
  -e RESTIC_PASSWORD=secret \
  restic/restic backup /data

使用 Velero(K8s)

# 如果数据运行在 Kubernetes 中
velero backup create myapp-backup --include-namespaces myapp
velero restore create --from-backup myapp-backup

恢复检查清单

# 1. 确认 volume 是否存在
docker volume ls

# 2. 确认备份文件完整性
tar tzf backup.tar.gz | head

# 3. 恢复数据
# (执行恢复命令)

# 4. 验证数据完整性
docker run --rm -v restored-volume:/data ubuntu \
  find /data -type f | wc -l

# 5. 挂载到新容器测试
docker run -d -v restored-volume:/app/data myapp

面试要点

  1. Volume 数据存储在 /var/lib/docker/volumes/
  2. 恢复 Volume 通常通过临时容器完成
  3. 数据库 Volume 推荐使用 SQL dump 备份而不是直接复制数据文件
  4. 定时备份 + 异地存储是生产环境标配
  5. Volume 的 --rm 容器不会保留数据,确保先创建 Volume

面试官常问:你们的数据卷备份方案是怎么做的?如何保证数据一致性?


容器删除时如何保留数据卷

容器删除时如何保留数据卷

默认行为

Docker 容器删除时,数据卷的默认行为是保留

# 创建一个容器并挂载命名卷
docker run -d --name test-app \
  -v my-data:/app/data \
  alpine sleep 360

# 删除容器(不使用 -v)
docker rm test-app

# 卷仍然存在
docker volume ls
# DRIVER    VOLUME NAME
# local     my-data

各种删除方式的数据保留情况

删除方式 命名卷 匿名卷 说明
docker rm 保留 保留 默认行为
docker rm -v 保留 删除 只删除匿名卷
docker rm --force 保留 保留 强制删除
docker rm -v --force 保留 删除 强制删除+匿名卷清理
docker compose down 保留 删除 Compose 默认行为
docker compose down -v 删除 删除 需要谨慎

关键规则

规则一:docker rm 不会删除命名卷

# 创建命名卷
docker volume create prod-data

# 容器使用命名卷
docker run -d --name db -v prod-data:/var/lib/mysql mysql:8.0

# 删除容器,命名卷保留
docker rm db
docker volume ls | grep prod-data
# local     prod-data    ← 还在

规则二:docker rm -v 仅删除匿名卷

# 使用匿名卷(未指定名称)
docker run -d --name temp -v /data nginx

# 删除容器并删除匿名卷
docker rm -v temp
# 匿名卷一起被删除

# 但是命名卷不受影响
docker run -d --name db -v my-named-data:/data alpine
docker rm -v db
docker volume ls | grep my-named-data
# local     my-named-data    ← 命名卷仍然保留

规则三:停止的容器不影响卷

# 停止容器
docker stop db

# 卷仍然可用
docker volume ls | grep prod-data

# 新容器可以立即使用旧卷
docker run -d --name db-v2 -v prod-data:/var/lib/mysql mysql:8.0
# 数据完全保留!

Docker Compose 中的行为

compose down 的默认行为

# docker-compose.yml
services:
  db:
    image: mysql:8.0
    volumes:
      - named-data:/var/lib/mysql
      - /data/logs  # 匿名卷

volumes:
  named-data:
# 停止并删除容器和网络,但保留卷
docker compose down
# 命名卷 named-data 保留
# 匿名卷 /data/logs 被删除

# 再次启动时,命名卷数据还在
docker compose up -d

删除所有内容(谨慎使用)

# 删除所有:容器、网络、命名卷、匿名卷
docker compose down -v

这会删除所有命名卷!生产环境务必小心

保留数据的最佳实践

1. 始终使用命名卷

# 推荐:使用命名卷
docker run -d -v app-data:/data myapp

# 不推荐:使用匿名卷(容易丢失)
docker run -d -v /data myapp

2. 容器更新时保留数据

# 升级 MySQL 版本时保留数据
# 1. 停止旧容器(保留卷)
docker stop mysql-old

# 2. 启动新容器使用同一卷
docker run -d --name mysql-new \
  -v mysql-data:/var/lib/mysql \
  mysql:8.0

# 3. 确认数据正常后删除旧容器
docker rm mysql-old

# 升级脚本版本
```bash
#!/bin/bash
# upgrade-container.sh
CONTAINER_NAME=$1
IMAGE=$2
VOLUMES=$(docker inspect $CONTAINER_NAME --format='{{range .Mounts}}{{.Name}}:{{.Destination}} {{end}}')
NETWORKS=$(docker inspect $CONTAINER_NAME --format='{{range $net, $v := .NetworkSettings.Networks}}{{$net}} {{end}}')

# 停止并删除旧容器(不删除卷)
docker stop $CONTAINER_NAME
docker rm $CONTAINER_NAME

# 启动新容器使用同一卷
docker run -d --name ${CONTAINER_NAME}-new \
  $(for vol in $VOLUMES; do echo "-v $vol"; done) \
  $(for net in $NETWORKS; do echo "--network $net"; done) \
  $IMAGE

3. 使用 –rm 时注意匿名卷

# --rm 标志
docker run --rm -v /app/data alpine ...
# 容器退出后自动删除,但匿名卷会保留(除非创建时就 标记为 cleanup)

# 命名卷不受 --rm 影响
docker run --rm -v named-data:/app/data alpine ...
# 退出后容器删除,卷保留

特殊情况:存储驱动层面的清理

如果你使用的是 docker system prune

# 默认不清理卷
docker system prune
# Volumes 不会被删除

# 显式要求清理卷
docker system prune --volumes
# 所有未使用的卷(包括匿名卷和未被容器引用的命名卷)都会被删除

排查容器删除后的卷状态

# 查看哪些容器在使用某个卷
docker ps -a --filter volume=my-data

# 查找 orphan 卷(未与任何容器关联)
docker volume ls --filter dangling=true

# 检查特定 Volume 的引用关系
docker volume inspect my-data | grep -A 5 "CreatedAt"

面试常见问题

Q:我想升级容器镜像但保留数据库,应该怎么做?

A:使用命名卷。停止旧容器(docker stop),删除旧容器(docker rm,不加 -v),然后启动新容器使用同一个命名卷。

Q:docker-compose down -v 在生产环境有什么风险?

A:-v 会删除所有 Compose 文件中定义的命名卷。在生产环境中这意味着丢失全部数据。应当只在开发环境或明确需要重置数据时使用。

Q:如果不小心删除了容器但没有删卷,数据能找回来吗?

A:只要没有显式执行 docker volume rmdocker compose down -v,数据卷就还在。用 docker volume ls 查看,然后重新挂载到新容器即可。


NFS 数据卷驱动

NFS 数据卷驱动

为什么需要 NFS 数据卷

在集群环境中,容器可能运行在不同主机上。如果每个容器都使用本地存储:

  • 数据无法在主机间共享
  • 容器迁移到其他主机后数据丢失
  • 高可用(HA)方案无法实现

NFS(Network File System)提供了一个简单可靠的网络共享存储方案。

使用 NFS Volume Driver

方式一:直接使用 –opt

# 创建 NFS 数据卷
docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rw,nfsvers=4 \
  --opt device=:/exports/data \
  nfs-share

# 使用 NFS 卷
docker run -d --name web \
  -v nfs-share:/var/www/html \
  nginx

方式二:使用 –mount 语法

docker run -d --name web \
  --mount type=volume,source=nfs-share,target=/var/www/html \
  --mount type=bind,source=/host/path,target=/data \
  nginx

方式三:直接挂载(不使用 Volume)

docker run -d --name app \
  --mount type=bind,source=/mnt/nfs/static,target=/app/static \
  nginx

# 或者直接在容器内 mount
docker run --privileged alpine \
  sh -c "mount -t nfs 192.168.1.100:/exports/data /mnt && ls /mnt"

NFS Volume 配置详解

基础选项

docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100 \
  --opt device=:/path/on/nfs/server \
  vol-name

高级选项

选项 说明 示例
addr NFS 服务器地址 192.168.1.100
nfsvers NFS 版本 4, 4.1, 4.2, 3
rw 读写模式 rw(读写), ro(只读)
hard/soft 错误处理模式 hard(持续重试), soft(返回错误)
intr 允许中断挂起操作 intr
timeo 超时时间(十分之一秒) timeo=600
retrans 重试次数 retrans=5
async/sync 写入模式 sync(同步写入,更安全)
# 生产环境 NFS 卷配置
docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.200,nfsvers=4.1,rw,hard,intr,timeo=600,retrans=5 \
  --opt device=:/mnt/production/data \
  prod-nfs

在 Docker Compose 中使用 NFS

# docker-compose.yml
services:
  web:
    image: nginx
    volumes:
      - nfs-static:/usr/share/nginx/html

volumes:
  nfs-static:
    driver: local
    driver_opts:
      type: nfs
      o: addr=192.168.1.100,nfsvers=4,rw
      device: ":/exports/static"

NFS 服务端配置

安装和配置 NFS 服务器

# Ubuntu/Debian
apt-get update && apt-get install -y nfs-kernel-server

# CentOS/RHEL
yum install -y nfs-utils

配置导出目录

# /etc/exports
/exports/data     192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
/exports/static   192.168.0.0/22(ro,sync,no_subtree_check)

# 使配置生效
exportfs -a

# 查看当前导出
showmount -e localhost

客户端安装

# Ubuntu/Debian
apt-get install -y nfs-common

# CentOS/RHEL
yum install -y nfs-utils

NFS 的性能优化

调整挂载参数

docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rsize=1048576,wsize=1048576,noatime,nodiratime \
  --opt device=:/exports/data \
  perf-nfs

网络优化

# 使用专用的存储网络,避免与其他流量共享
# 考虑使用更快的网络(10GbE 或更高)

# MTU 调整
# NFS over Jumbo Frames(9000 MTU)
docker volume create \
  --opt o=addr=192.168.1.100,mountproto=tcp,rsize=1048576 \
  ...

缓存策略

策略 一致性 性能 适用场景
sync 强一致 较低 数据库
async 弱一致 静态文件
noac 强一致 需要实时性

NFS 的常见问题和排查

挂载失败

# 检查 NFS 服务器是否可达
ping 192.168.1.100

# 检查 NFS 导出是否可用
showmount -e 192.168.1.100

# 手动挂载测试
mount -t nfs 192.168.1.100:/exports/data /mnt/test

权限问题

# 容器内 UID 映射问题
# 确保 NFS 导出目录的权限与容器内 UID 匹配
chown -R 999:999 /exports/data  # PostgreSQL 常用 UID=999

# 或使用 squash 选项
# /etc/exports 配置
/exports/data 192.168.1.0/24(rw,sync,all_squash,anonuid=999,anongid=999)

性能问题排查

# 检查 NFS 延迟
nfsstat -s

# 监控 NFS 操作
nfswatch -h 192.168.1.100

生产环境最佳实践

  1. 使用 NFSv4:协议更高效,支持 ACL 和锁
  2. 专用存储网络:避免与业务流量竞争带宽
  3. 合理的缓存策略:根据数据重要性选择 sync/async
  4. 监控:监控 NFS 延迟和吞吐量
  5. 冗余:部署冗余 NFS 服务器或使用 HA-NFS
  6. 防火墙配置:开放 2049(NFS)、111(portmapper)

面试知识点

Q:NFS Volume 和本地 Volume 的性能差异?

A:NFS Volume 通过网络传输数据,延迟必然高于本地存储。对于 I/O 密集型应用如数据库,建议使用本地存储配合副本方案;静态文件、日志等可以使用 NFS 共享。

Q:NFS 的 no_root_squash 是什么?有什么风险?

A:no_root_squash 允许容器内的 root 用户以 root 身份写入 NFS 目录,避免权限问题。但会降低安全性——容器 root 可以覆盖 NFS 上的任何文件。


如何备份和恢复数据卷

如何备份和恢复数据卷

备份和恢复的核心思路

Docker 数据卷的备份和恢复统一使用一个技巧:挂载数据卷到辅助容器中,使用 tar 打包/解包

核心命令模式:

# 备份:将 Volume 打包到宿主机
docker run --rm -v :/data -v :/backup alpine \
  tar czf /backup/.tar.gz -C /data .

# 恢复:将备份包解压到 Volume
docker run --rm -v :/data -v :/backup alpine \
  tar xzf /backup/.tar.gz -C /data

详细备份方法

方法一:备份单个 Volume

# 创建一个测试数据卷并写入数据
docker volume create myapp-data
docker run --rm -v myapp-data:/data alpine sh -c "echo 'Hello Docker' > /data/test.txt"

# 备份 myapp-data 到当前目录
docker run --rm \
  -v myapp-data:/source \
  -v $(pwd):/backup \
  alpine \
  tar czf /backup/myapp-data-20240101.tar.gz -C /source .

方法二:备份多个 Volume

#!/bin/bash
# backup-all-volumes.sh

BACKUP_DIR="/backups/volumes"
mkdir -p $BACKUP_DIR

for vol in $(docker volume ls -q); do
  echo "Backing up volume: $vol"
  docker run --rm \
    -v $vol:/data \
    -v $BACKUP_DIR:/backup \
    alpine \
    tar czf /backup/${vol}-$(date +%Y%m%d).tar.gz -C /data .
done

echo "All volumes backed up to $BACKUP_DIR"

方法三:使用增量备份(rsync)

docker run --rm \
  -v myapp-data:/source \
  -v $(pwd)/incremental:/target \
  alpine \
  sh -c "apk add --no-cache rsync && rsync -av --delete /source/ /target/"

方法四:云端备份

# 备份到 S3 兼容存储
docker run --rm \
  -v myapp-data:/data \
  -e AWS_ACCESS_KEY_ID=xxx \
  -e AWS_SECRET_ACCESS_KEY=xxx \
  amazon/aws-cli \
  s3 sync /data s3://my-backup-bucket/volumes/myapp-data/

详细恢复方法

场景一:恢复到同名 Volume

# 1. 如果 Volume 不存在则创建
docker volume create myapp-data

# 2. 恢复到 Volume
docker run --rm \
  -v myapp-data:/target \
  -v $(pwd):/backup \
  alpine \
  tar xzf /backup/myapp-data-20240101.tar.gz -C /target

# 3. 验证恢复结果
docker run --rm -v myapp-data:/data alpine cat /data/test.txt
# 输出:Hello Docker

场景二:恢复到不同名的 Volume

将备份数据恢复到不同名的 Volume,用于环境复制:

# 创建目标 Volume
docker volume create staging-data

# 恢复备份数据
docker run --rm \
  -v staging-data:/target \
  -v $(pwd):/backup \
  alpine \
  tar xzf /backup/myapp-data-20240101.tar.gz -C /target

场景三:恢复到指定目录

# 恢复备份文件中的特定子目录
docker run --rm \
  -v myapp-data:/target \
  -v $(pwd):/backup \
  alpine \
  tar xzf /backup/myapp-data-20240101.tar.gz \
    -C /target \
    --wildcards 'subdir/*'

自动备份方案

Cron Job 定时备份

# /etc/cron.d/docker-volume-backup
0 2 * * * root /usr/local/bin/backup-volumes.sh

# 备份脚本内容
# backup-volumes.sh
#!/bin/bash
BACKUP_DIR=/backups/volumes
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p $BACKUP_DIR

# 备份指定 Volume
for vol in prod-db prod-redis prod-config; do
  docker run --rm \
    -v $vol:/data \
    -v $BACKUP_DIR:/backup \
    alpine \
    tar czf /backup/${vol}-${DATE}.tar.gz -C /data .
done

# 删除过期备份
find $BACKUP_DIR -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete

使用专门的备份工具

docker-volume-backup 是一个广受欢迎的备份工具:

docker run -d --name volume-backup \
  -v prod-data:/backup/data:ro \
  -v /backups:/backup \
  -e BACKUP_RETENTION=7 \
  offen/docker-volume-backup:v2

数据一致性保证

对于正在运行的数据库,直接备份 Volume 可能导致数据不一致。

MySQL 备份

# 一致性备份需要使用 mysqldump
docker exec mysql-container mysqldump --all-databases --single-transaction \
  > /backups/mysql/mysql-$(date +%Y%m%d).sql

PostgreSQL 备份

# 使用 pg_dump 保证一致性
docker exec postgres-container pg_dump -U postgres -Fc mydb \
  > /backups/postgres/mydb-$(date +%Y%m%d).dump

文件锁配合

如果必须备份 Volume 文件,可以先锁定应用写入:

# 应用支持优雅停止写入
docker exec app-container kill -USR1 1  # 应用停止写入
docker run --rm -v app-data:/data -v $(pwd):/backup alpine tar czf ...
docker exec app-container kill -USR2 1  # 恢复写入

面试要点

Q:备份 Volume 时为什么使用辅助容器而不是直接在宿主机 cp?

A:Volume 存储在 Docker 管理的目录(/var/lib/docker/volumes/),需要 root 权限直接访问。更安全的方式是通过辅助容器挂载 Volume 然后打包,这样不需要关心具体文件路径和权限问题。

Q:如何备份一个正在运行的 MySQL 容器的数据?

A:使用 docker exec 执行 mysqldump 而非直接备份 Volume 文件,因为 mysqldump 能保证数据一致性。或者先停止容器再备份 Volume。


通过数据卷容器共享数据

通过数据卷容器共享数据

什么是数据卷容器

数据卷容器(Data Volume Container)是一个专门用来提供数据卷供其他容器挂载的容器。它本身不需要运行任何服务,只需要定义和持有数据卷即可。

注意:在 Docker 的新版本中,官方更推荐直接使用命名 Volume 而不是数据卷容器。但数据卷容器的模式仍在广泛使用,尤其在 Legacy 环境中。

基本用法

创建数据卷容器

# 创建一个数据卷容器(没有运行任何服务)
docker create -v /shared-data --name data-container busybox /bin/true

或者在启动时创建:

# 创建并保持运行
docker run -d -v /shared-data --name data-container busybox sleep infinity

挂载数据卷容器

其他容器通过 --volumes-from 参数挂载数据卷:

# 容器 B 共享数据卷容器的数据
docker run -d --name app1 --volumes-from data-container nginx

docker run -d --name app2 --volumes-from data-container alpine tail -f /dev/null

级联共享

数据卷容器的挂载可以级联:

# data-container 是源
docker run -d --volumes-from data-container --name level1 nginx
docker run -d --volumes-from level1 --name level2 nginx
# level2 也能访问 data-container 的数据

工作原理

数据卷容器本身并不特殊,关键在于 --volumes-from 的工作原理:

  1. Docker 记录每个容器的 Volume 挂载信息
  2. --volumes-from 告诉 Docker 把目标容器的所有 Volume 挂载点复制一份挂载到当前容器
  3. 所有容器共享相同的宿主机目录
数据卷容器(data-container)
  └── /shared-data (Volume: abc123)
        ├── app1 (/shared-data)
        ├── app2 (/shared-data)
        └── app3 (/shared-data)

使用场景

场景一:配置文件共享

# 数据卷容器存放配置文件
docker create -v /etc/nginx/conf.d --name config-container busybox

# 写入配置(通过辅助容器)
docker run --rm --volumes-from config-container alpine sh -c "
  echo 'server { listen 80; }' > /etc/nginx/conf.d/default.conf
"

# 多个 Nginx 容器使用同一套配置
docker run -d --volumes-from config-container --name nginx1 nginx
docker run -d --volumes-from config-container --name nginx2 nginx

场景二:日志收集

# 应用容器写数据到共享卷
docker run -d -v /app/logs --name app-server myapp

# Logstash 容器读取日志
docker run -d --volumes-from app-server \
  -v logstash.conf:/etc/logstash/conf.d/logstash.conf \
  logstash

场景三:备份还原(传统方式)

# 备份数据卷容器的数据
docker run --rm --volumes-from data-container \
  -v $(pwd):/backup alpine \
  tar czf /backup/data-backup.tar.gz /shared-data

# 还原
docker run --rm --volumes-from data-container \
  -v $(pwd):/backup alpine \
  tar xzf /backup/data-backup.tar.gz -C /shared-data

场景四:开发环境数据库初始化

# 先初始化数据库脚本到一个共用卷
docker create -v /docker-entrypoint-initdb.d --name init-scripts busybox

# 拷贝初始化 SQL
docker cp init.sql init-scripts:/docker-entrypoint-initdb.d/

# 多个 MySQL 实例使用同一份初始化脚本
docker run -d --volumes-from init-scripts -e MYSQL_ROOT_PASSWORD=root mysql:8.0
docker run -d --volumes-from init-scripts -e MYSQL_ROOT_PASSWORD=root mysql:8.0

数据卷容器 vs 命名 Volume

对比项 数据卷容器 命名 Volume
引入版本 较早(v1.0+) v1.9+
管理方式 通过容器管理 Docker Volume API
可移植性 需保留容器 独立管理
命名访问 容器名引用 Volume 名称引用
Compose 支持 有限 原生支持
备份复杂度 通过辅助容器 专用命令

何时仍使用数据卷容器

虽然命名 Volume 更现代,但以下情况仍可能使用数据卷容器:

  1. 旧版本 Docker(v1.9 之前的限制)
  2. 批量容器共享:一次创建,多容器引用
  3. 与旧 Compose 文件兼容:部分历史配置仍在使用
  4. 临时环境:快速搭建测试环境

最佳实践与注意事项

  1. 使用命名 Volume 优先:新项目和 Docker v1.9+ 环境优先使用命名 Volume
  2. 不要运行不必要的服务:数据卷容器不需要运行服务
  3. 权限管理:注意 UID/GID 映射问题
  4. 停止前先卸载:删除数据卷容器前确保没有容器在引用它
  5. 备份独立容器:数据卷容器的元数据也需要备份

面试知识要点

Q:数据卷容器停止后,其他容器还能访问它的数据吗?

可以。数据卷的生命周期独立于容器。即使数据卷容器停止或删除,只要 Volume 未被删除,其他容器仍然可以访问数据。但请确保 Volume 没有被 docker rm -v 删除。

Q:–volumes-from 支持远程主机吗?

不支持。--volumes-from 只能在同一个 Docker 主机上使用。跨主机共享数据需要使用分布式存储方案,如 NFS Volume Driver、GlusterFS 等。


创建管理数据卷

创建管理数据卷

面试题

如何创建和管理 Docker 数据卷?有哪些常用命令和操作?

标准答案

Docker 数据卷的完整生命周期包括创建、使用、查看、备份、迁移和删除。掌握这些操作是 Docker 数据管理的基础。

基本命令

# 命令结构
docker volume <子命令> [参数]

# 常用子命令
docker volume create     # 创建卷
docker volume ls         # 列出卷
docker volume inspect    # 查看卷详情
docker volume rm         # 删除卷
docker volume prune      # 清理未使用的卷

创建数据卷

方式一:手动创建(推荐)

# 创建最简单的卷
docker volume create mydata

# 创建带标签的卷
docker volume create \
  --label environment=production \
  --label app=myapp \
  mydata

# 创建指定驱动的卷
docker volume create \
  --driver local \
  --opt type=none \
  --opt device=/mnt/external_drive \
  --opt o=bind \
  external-volume

# 创建 NFS 卷
docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rw,nfsvers=4 \
  --opt device=:/export/data \
  nfs-data

方式二:自动创建(使用容器时)

# 运行容器时自动创建匿名卷
docker run -d -v /var/lib/mysql mysql:8
# Docker 自动生成一个随机卷名

# 运行容器时自动创建命名卷
docker run -d -v mydata:/data alpine
# 如果 mydata 不存在,Docker 自动创建

使用数据卷

容器挂载

# 基础挂载
docker run -d --name app -v mydata:/app/data myapp

# 只读挂载
docker run -d --name app -v mydata:/app/data:ro myapp

# --mount 语法(更明确,推荐)
docker run -d --name app \
  --mount type=volume,source=mydata,target=/app/data,readonly \
  myapp

# 指定挂载的子路径
docker run -d --name app \
  -v mydata:/app/data:ro \
  myapp

多容器共享

# 创建共享卷
docker volume create shared-data

# 所有容器挂载同一个卷
docker run -d --name writer1 -v shared-data:/data alpine watch -n 1 'echo $$ >> /data/pids.txt'
docker run -d --name writer2 -v shared-data:/data alpine watch -n 1 'date >> /data/dates.txt'
docker run -d --name reader -v shared-data:/data alpine tail -f /data/*

查看和管理

# 列出所有卷
docker volume ls

# 过滤
docker volume ls --filter "label=environment=production"
docker volume ls --filter "dangling=true"
docker volume ls --filter "name=mydata"

# 格式化输出
docker volume ls --format "table {{.Name}}\t{{.Driver}}\t{{.Mountpoint}}"

# 查看详情
docker volume inspect mydata
# 输出包含:
# - Name: 卷名
# - Driver: 驱动类型
# - Mountpoint: 宿主机实际路径
# - Labels: 标签
# - Scope: 作用域(local/global)
# - Options: 驱动选项

# 查看磁盘使用
docker system df
docker system df -v  # 包含每个卷的详细信息

备份数据卷

备份到本地

# 方法一:挂载卷 + tar(推荐)
docker run --rm \
  -v mydata:/data \
  -v $(pwd):/backup \
  alpine \
  tar czf /backup/mydata-$(date +%Y%m%d).tar.gz -C /data .

备份到另一个卷

# 创建备份卷
docker volume create mydata-backup

# 复制数据
docker run --rm \
  -v mydata:/source \
  -v mydata-backup:/target \
  alpine \
  sh -c "cp -a /source/. /target/"

增量备份

# 使用 rsync(需要容器内有 rsync)
docker run --rm \
  -v mydata:/source \
  -v mydata-backup:/target \
  alpine \
  sh -c "apk add rsync && rsync -avh /source/ /target/"

恢复数据卷

# 从 tar 文件恢复
docker run --rm \
  -v mydata:/target \
  -v $(pwd):/backup \
  alpine \
  tar xzf /backup/mydata-20240101.tar.gz -C /target/

数据迁移(跨宿主机)

# 源机器:导出
docker run --rm \
  -v mydata:/data \
  alpine \
  tar czf - -C /data . | \
  ssh user@target-host "cat > /tmp/mydata.tar.gz"

# 或者:
docker run --rm \
  -v mydata:/data \
  -v $(pwd):/backup \
  alpine \
  tar czf /backup/mydata.tar.gz -C /data .

scp mydata.tar.gz user@target-host:/tmp/

# 目标机器:导入
ssh target-host
docker volume create mydata
docker run --rm \
  -v mydata:/data \
  -v /tmp:/backup \
  alpine \
  tar xzf /backup/mydata.tar.gz -C /data

清理数据卷

# 删除一个卷
docker volume rm mydata

# 批量删除
docker volume rm vol1 vol2 vol3

# 删除所有未使用的卷(慎用)
docker volume prune

# 删除所有未使用的卷(不提示)
docker volume prune -f

# 删除未使用且超过 24 小时的卷
docker volume prune --filter "until=24h"

# 只删除指定标签的未使用卷
docker volume prune --filter "label!=keep"

数据卷的生命周期管理

# 好的实践:使用标签管理
docker volume create \
  --label project=myapp \
  --label environment=production \
  --label backup=scheduled \
  myapp-data

# 定期清理脚本
#!/bin/bash
# cleanup-volumes.sh
echo "=== 清理未使用的卷 ==="
docker volume prune -f

echo "=== 备份关键卷 ==="
for vol in myapp-data myapp-db app-logs; do
  docker run --rm \
    -v $vol:/source \
    -v $(pwd)/backup:/backup \
    alpine \
    tar czf /backup/$vol-$(date +%Y%m%d).tar.gz -C /source .
done

docker-compose 中的数据卷

# docker-compose.yml
version: '3.8'
services:
  db:
    image: postgres:15
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro

  app:
    image: myapp:latest
    volumes:
      - app-data:/app/data
      - app-logs:/var/log/myapp

volumes:
  pgdata:           # 创建命名卷
  app-data:
  app-logs:
    driver: local
    driver_opts:
      type: none
      device: /mnt/ssd/logs
      o: bind

公共技巧

# 查看卷内容
docker run --rm -it -v mydata:/data alpine ls -la /data

# 复制文件到卷
docker run --rm -v mydata:/data alpine sh -c \
  'echo "config data" > /data/config.json'

# 查看卷的实际存储路径
docker volume inspect mydata --format '{{.Mountpoint}}'

总结

数据卷管理的关键命令是 createlsinspectrmprune。核心操作是备份和恢复,通过 docker run --rm -v volume:/source alpine tar czf 模式进行。记住:卷的生命周期独立于容器,只有手动 docker volume rm 才会真正删除数据。


数据卷存储位置

数据卷存储位置

面试题

Docker 数据卷(Volume)在宿主机上存储在哪里?可以改存储位置吗?有哪些注意事项?

标准答案

Docker 数据卷默认存储在 /var/lib/docker/volumes/ 目录下,每个卷对应一个子目录。这是 Docker 数据卷的”物理存储位置”,了解它对于备份、迁移、磁盘空间管理和故障排查都非常重要。

默认存储位置

# 默认存储路径
ls -la /var/lib/docker/volumes/

# 典型输出
drwx-----x  4 root root   4096 Jan 15 10:00 mydata
drwx-----x  4 root root   4096 Jan 15 10:01 app-logs
drwx-----x  4 root root   4096 Jan 15 10:02 pgdata
...

# 每个卷是一个目录
# 目录名 = 卷名
# 实际数据在 _data 子目录中

数据卷的目录结构

# 查看具体卷的目录结构
ls -la /var/lib/docker/volumes/mydata/
# total 20
# drwx-----x  3 root root 4096 Jan 15 10:00 .
# drwx--x--x 12 root root 4096 Jan 15 10:00 ..
# drwxr-xr-x  2 root root 4096 Jan 15 10:00 _data   ← 实际数据

# 查看数据卷挂载点
docker volume inspect mydata
# [
#   {
#     "CreatedAt": "2024-01-15T10:00:00+08:00",
#     "Driver": "local",
#     "Labels": {},
#     "Mountpoint": "/var/lib/docker/volumes/mydata/_data", ← 实际路径
#     "Name": "mydata",
#     "Options": {},
#     "Scope": "local"
#   }
# ]

修改存储位置

方法一:修改 Docker 数据目录(全局修改)

# 这会移动 Docker 的所有数据(包括镜像、容器、卷)
# 不仅是数据卷

# 1. 停止 Docker
systemctl stop docker

# 2. 移动现有数据
mv /var/lib/docker /data/docker

# 3. 创建软链接(最简单的方式)
ln -s /data/docker /var/lib/docker

# 4. 或者修改 daemon.json
# /etc/docker/daemon.json
{
  "data-root": "/data/docker"
}

# 5. 启动 Docker
systemctl start docker

# 验证
docker info | grep "Docker Root Dir"
# Docker Root Dir: /data/docker

方法二:使用软链接(仅移动数据卷)

# 如果只想移动卷数据
systemctl stop docker

# 移动卷目录
mv /var/lib/docker/volumes /data/docker-volumes

# 创建软链接
ln -s /data/docker-volumes /var/lib/docker/volumes

# 或者指向另一个磁盘
ln -s /mnt/ssd/volumes /var/lib/docker/volumes

systemctl start docker

方法三:使用 Volume Driver 重定向

# 创建卷时指定存储位置
docker volume create \
  --driver local \
  --opt type=none \
  --opt device=/mnt/external_drive/myapp-data \
  --opt o=bind \
  custom-location-volume

# 验证
docker volume inspect custom-location-volume
# "Mountpoint": "/var/lib/docker/volumes/custom-location-volume/_data"
# 实际数据在 /mnt/external_drive/myapp-data

查看磁盘使用

# 查看所有卷占用的空间
du -sh /var/lib/docker/volumes/*/

# 更友好的方式
sudo docker system df -v | grep -A 10 "Volumes"

# 按大小排序
du -sh /var/lib/docker/volumes/*/ | sort -hr

# 查看 Docker 数据目录总体占用
docker system df
# TYPE                TOTAL    ACTIVE   SIZE       RECLAIMABLE
# Images              15       8        3.245GB    1.2GB (37%)
# Containers          12       5        512.3MB    0B
# Local Volumes       8        5        1.8GB      856.3MB
# Build Cache         23       0        125MB      125MB

备份策略(按位置)

# 方式 1:直接备份卷目录
tar czf /backup/docker-volumes-$(date +%Y%m%d).tar.gz /var/lib/docker/volumes/

# 方式 2:只备份特定卷
tar czf /backup/pgdata-$(date +%Y%m%d).tar.gz /var/lib/docker/volumes/pgdata/

# 方式 3:使用卷备份模式(推荐)
docker run --rm \
  -v pgdata:/source \
  -v /backup:/backup \
  alpine \
  tar czf /backup/pgdata-$(date +%Y%m%d).tar.gz -C /source .

存储驱动差异

# Docker 版本不同,存储驱动可能不同
docker info | grep "Storage Driver"
# Storage Driver: overlay2

# 不同存储驱动对卷的影响
# overlay2:卷在 /var/lib/docker/volumes/ 下
# devicemapper:卷可能在不同位置
# btrfs/zfs:利用文件系统快照能力

数据安全与权限

# 数据卷目录的默认权限
ls -la /var/lib/docker/volumes/
# drwx--x--x  4 root root ...

# 普通用户无法直接访问
# 需要通过 docker volume 命令操作

# 如果确实需要手动操作,用 sudo
sudo ls /var/lib/docker/volumes/mydata/_data

常见问题

Q:默认存储位置磁盘空间不够怎么办?

# 方案一:迁移到新磁盘(推荐)
# 1. 停止 Docker
systemctl stop docker

# 2. 迁移
mv /var/lib/docker /mnt/new-disk/docker
ln -s /mnt/new-disk/docker /var/lib/docker

# 3. 启动 Docker
systemctl start docker

# 方案二:创建卷时指定其他位置
docker volume create \
  --opt device=/mnt/new-disk/volumes/myvol \
  --opt type=none --opt o=bind \
  myvol

Q:如何找到特定卷的物理路径?

docker volume inspect <卷名> --format '{{.Mountpoint}}'
# 或
docker volume inspect <卷名> | jq -r '.[0].Mountpoint'

Q:不同存储驱动下卷的位置会变吗?

overlay2 驱动下卷始终在 /var/lib/docker/volumes/。其他驱动(如 devicemapper)可能将数据存储在独立设备上,但 Docker 仍然会通过 mount 挂载到卷目录。

Q:可以跨节点共享卷的存储目录吗?

直接共享 /var/lib/docker/volumes/ 不推荐(有并发写入问题)。更好的方式是使用分布式文件系统(NFS、GlusterFS)或 Volume Driver。

最佳实践

# 1. 将数据卷放在单独的分区或磁盘上
#    避免与系统分区争用 IO

# 2. 监控卷磁盘使用
#    定期检查 du -sh /var/lib/docker/volumes/*/

# 3. 使用命名卷而非匿名卷
#    便于管理和识别

# 4. 生产环境考虑使用专门的存储
#    NFS、SAN、云存储等通过 Volume Driver 集成

# 5. 备份策略
#    卷数据 + 元数据(volume inspect 输出)一起备份

总结

数据卷的物理存储位置是 /var/lib/docker/volumes/<卷名>/_data。可以通过修改 data-root、软链接或 Volume Driver 改变存储位置。理解存储位置对于备份、迁移、磁盘空间管理和故障排查至关重要。记住:操作 Volume 要用 Docker 命令而非直接操作文件系统。


数据卷 Volume 优势

数据卷 Volume 优势

面试题

为什么 Docker 官方推荐使用数据卷(Volume)而不是绑定挂载?数据卷有哪些独特的优势?

标准答案

数据卷(Volume)是 Docker 推荐的数据持久化方案。相比绑定挂载,它在管理便捷性、安全性、可移植性和高级功能方面有显著优势。

核心优势

Volume 六大优势
├── 1. Docker 完全管理
├── 2. 权限自动处理
├── 3. 可移植性
├── 4. 备份恢复方便
├── 5. 多容器共享
├── 6. 驱动扩展(NFS、加密等)

优势一:Docker 完全管理

# Volume 有完整的生命周期管理
docker volume create mydata     # 创建
docker volume ls               # 列出
docker volume inspect mydata   # 查看详情
docker volume rm mydata        # 删除
docker volume prune            # 清理未使用的卷

# Bind Mount 无任何 Docker 管理命令
# 需要手动管理宿主机上的文件

优势二:权限自动处理

# Volume 不需要关心 UID/GID 映射
# Docker 自动处理容器内外的权限

# 示例:MySQL 数据库
docker volume create mysql-data
docker run -d \
  -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  mysql:8

# MySQL 容器内的 mysql 用户(UID 999)写入数据
# Volume 自动处理了权限,不需要手动 chown

# 如果用 Bind Mount:
docker run -d \
  -v /home/user/mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  mysql:8
# 可能遇到权限问题,需要手动 chown 999:999 /home/user/mysql-data

优势三:空目录初始化行为

# 这是 Volume 和 Bind Mount 的重大差异

# Volume 行为:空卷 → 自动复制镜像内容
docker volume create nginx-html
docker run -v nginx-html:/usr/share/nginx/html nginx
# 空的 Volume 会自动把镜像中 /usr/share/nginx/html 的内容复制过来

# Bind Mount 行为:空目录 → 覆盖镜像内容
docker run -v /tmp/empty-html:/usr/share/nginx/html nginx
# /usr/share/nginx/html 被空目录覆盖!Nginx 没有默认页面可提供!

优势四:跨平台和可移植性

# Volume 不依赖宿主机目录结构
# 在 Linux、macOS、Windows 上行为一致

# Volume 可以配合 Volume Driver 使用
# 比如 NFS、Cloud 存储驱动

# 创建 NFS 数据卷
docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rw,nfsvers=4 \
  --opt device=:/share/mydata \
  nfs-volume

# docker-compose 中使用
volumes:
  shared-data:
    driver: nfs
    driver_opts:
      type: nfs
      o: "addr=192.168.1.100,rw,nfsvers=4"
      device: ":/share/mydata"

优势五:备份恢复简洁

# 备份 Volume
docker run --rm \
  -v mydata:/source \
  -v $(pwd):/backup \
  alpine \
  tar czf /backup/mydata-backup.tar.gz -C /source .

# 恢复 Volume
docker run --rm \
  -v mydata:/target \
  -v $(pwd):/backup \
  alpine \
  tar xzf /backup/mydata-backup.tar.gz -C /target

# 或者使用 Volume 容器(老方式)
docker create -v mydata:/data --name datacontainer alpine
docker cp ./backup.tar.gz datacontainer:/data/

优势六:多容器共享

# 多个容器可以安全地共享同一个 Volume
docker volume create shared-data

# 生产者
docker run -d --name producer \
  -v shared-data:/data \
  alpine watch -n 5 'date >> /data/timestamps.txt'

# 消费者
docker run -d --name consumer \
  -v shared-data:/data \
  alpine tail -f /data/timestamps.txt

# 第三个容器也可以加入
docker run -d --name worker \
  -v shared-data:/data \
  alpine sh -c 'while true; do cat /data/timestamps.txt; sleep 10; done'

优势七:驱动扩展能力

# Volume 支持多种存储后端
# 内置驱动:local(默认)
# 第三方驱动:
# - REX-Ray(多种存储阵列)
# - Portworx(分布式存储)
# - Cloudstor(云存储)
# - NetApp、EMC 等企业存储

# 例如使用 SSHFS 驱动
docker plugin install vieux/sshfs
docker volume create \
  --driver vieux/sshfs \
  -o sshcmd=user@host:/remote/path \
  -o password=secret \
  ssh-volume

优势八:Volume 操作不影响容器

# 可以在容器运行时操作 Volume
docker run -d --name app -v app-data:/data alpine sleep 3600

# 不需要停止容器即可备份
docker run --rm -v app-data:/source -v $(pwd):/backup alpine \
  tar czf /backup/app-data-backup.tar.gz -C /source .

# 或查看 Volume 使用情况
docker system df
# TYPE                TOTAL   ACTIVE  SIZE    RECLAIMABLE
# Volumes             5       3       1.2GB   800MB

性能考量

# Native Linux 上 Volume 和 Bind Mount 性能差异极小
# Volume 存储在 /var/lib/docker/volumes/,底层使用同一文件系统

# 但在 macOS/Windows 上差异显著
# Bind Mount(osxfs):性能较差,大量文件操作延迟明显
# Volume(Linux VM 内部存储):接近原生性能

# macOS 开发环境推荐
docker run -v my-npm-cache:/root/.npm node:18 npm install  # ✅ 快
docker run -v $(pwd):/app node:18 npm install              # ❌ 慢

何时仍然用 Bind Mount

# 虽然有这么多优势,但 Bind Mount 在某些场景仍不可替代:

# 1. 开发热重载
docker run -v $(pwd):/app -e NODE_ENV=dev node:18 npm run dev

# 2. 配置注入
docker run -v /etc/myapp/config.yaml:/app/config.yaml:ro myapp

# 3. Socket 共享
docker run -v /var/run/docker.sock:/var/run/docker.sock docker

# 4. 日志采集
docker run -v /var/log/host:/var/log/host:ro log-collector

面试问答

问:Volume 适合存在性的类型或大小有上限吗?

没有硬性上限,取决于宿主机磁盘空间。Docker 默认对 Volume 没有大小限制,但可以通过 Volume Driver 或文件系统配额来实现。

问:如何查看所有 Volume 占用的磁盘空间?

docker system df
docker system df -v  # 详细信息

问:为什么 macOS 上 Volume 比 Bind Mount 快?

因为 Docker for Mac 的 Bind Mount 通过 osxfs 文件系统共享实现,每次文件操作需要穿越 macOS 和 Linux VM 之间的边界。Volume 直接存储在 Linux VM 内部,没有这个开销。

总结

Volume 是 Docker 的”一等公民”持久化方案。六大优势(管理、权限、初始化行为、可移植、备份、扩展)使其成为生产环境的不二之选。只在需要直接操作宿主机文件(开发热重载、配置注入等)时才用 Bind Mount。


备份恢复网络配置

备份恢复网络配置

面试题

如何备份和恢复 Docker 的网络配置?网络配置保存在哪里?

标准答案

Docker 的网络配置不像数据卷那样”明面”地存在,大部分是运行时状态。备份网络配置主要涉及自定义网络的元数据、iptables 规则和容器网络设置。

备份的内容

Docker 网络配置包含:
1. 自定义网络定义(网络名称、子网、驱动等)
2. iptables 规则(端口映射、NAT 规则)
3. 容器网络设置(IP 分配、网络连接关系)

方式一:导出网络定义(推荐)

# 1. 列出所有自定义网络
docker network ls --filter "driver=bridge" --filter "scope=local"

# 2. 导出网络详情为 JSON
for net in $(docker network ls --filter "scope=local" -q); do
  docker network inspect $net > "network-$(date +%Y%m%d)-$net.json"
done

# 3. 恢复时重新创建
# 从备份文件中读取配置
docker network create \
  --driver bridge \
  --subnet 10.0.0.0/16 \
  --gateway 10.0.0.1 \
  --ip-range 10.0.1.0/24 \
  mynet

方式二:备份 iptables 规则

# 1. 导出 Docker 相关的 iptables 规则
iptables-save | grep -E "DOCKER|docker0" > docker-iptables-backup.rules

# 2. 导出 NAT 表
iptables-save -t nat > docker-nat-backup.rules

# 3. 恢复
iptables-restore < docker-iptables-backup.rules
iptables-restore -t nat < docker-nat-backup.rules

方式三:备份 daemon.json 配置

# Docker daemon 的网络全局配置
# /etc/docker/daemon.json
{
  "bip": "172.17.0.1/16",
  "default-address-pools": [
    {"base": "172.20.0.0/16", "size": 24},
    {"base": "172.21.0.0/16", "size": 24}
  ],
  "iptables": true,
  "ip-forward": true,
  "ip-masq": true,
  "mtu": 1450,
  "dns": ["8.8.8.8", "114.114.114.114"]
}

# 备份
cp /etc/docker/daemon.json /backup/docker-daemon.json

方式四:备份网络命名空间

# Docker 网络命名空间通常不需要单独备份
# 因为它们随容器创建和删除而管理
# 但某些特殊场景需要

# 查看网络命名空间
ls -la /var/run/docker/netns/

# 导出命名空间状态(较少使用)
ip netns list

完整备份脚本

#!/bin/bash
# backup-docker-network.sh

BACKUP_DIR="/backup/docker-network-$(date +%Y%m%d%H%M%S)"
mkdir -p $BACKUP_DIR

echo "=== 备份 Docker 网络配置 ==="

# 1. 备份所有网络定义
echo "1. 备份网络定义..."
docker network ls -q 2>/dev/null | while read net_id; do
  net_name=$(docker network inspect --format='{{.Name}}' $net_id)
  docker network inspect $net_id > "$BACKUP_DIR/network-$net_name.json"
  echo "   - 已备份网络: $net_name"
done

# 2. 备份 iptables 规则
echo "2. 备份 iptables 规则..."
iptables-save > "$BACKUP_DIR/iptables-backup.rules"
iptables-save -t nat > "$BACKUP_DIR/iptables-nat-backup.rules"

# 3. 备份 daemon.json
echo "3. 备份 Docker daemon 配置..."
[ -f /etc/docker/daemon.json ] && \
  cp /etc/docker/daemon.json "$BACKUP_DIR/daemon.json" || \
  echo "   - daemon.json 不存在"

# 4. 备份网络状态
echo "4. 生成网络状态摘要..."
echo "=== 网络列表 ===" >> "$BACKUP_DIR/network-summary.txt"
docker network ls >> "$BACKUP_DIR/network-summary.txt"
echo "" >> "$BACKUP_DIR/network-summary.txt"
echo "=== 容器网络映射 ===" >> "$BACKUP_DIR/network-summary.txt"
docker ps -q 2>/dev/null | while read cid; do
  name=$(docker inspect --format='{{.Name}}' $cid | cut -c2-)
  nets=$(docker inspect --format='{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' $cid)
  echo "$name: $nets" >> "$BACKUP_DIR/network-summary.txt"
done

echo "=== 备份完成: $BACKUP_DIR ==="

恢复脚本

#!/bin/bash
# restore-docker-network.sh

BACKUP_DIR=$1
if [ -z "$BACKUP_DIR" ]; then
  echo "Usage: $0 "
  exit 1
fi

echo "=== 恢复 Docker 网络配置 ==="

# 1. 恢复 daemon.json
echo "1. 恢复 daemon.json..."
if [ -f "$BACKUP_DIR/daemon.json" ]; then
  cp "$BACKUP_DIR/daemon.json" /etc/docker/daemon.json
  echo "   - 已恢复,需要重启 Docker"
fi

# 2. 恢复网络定义
echo "2. 恢复自定义网络..."
for backup in $BACKUP_DIR/network-*.json; do
  [ ! -f "$backup" ] && continue
  net_name=$(basename "$backup" | sed 's/network-\(.*\)\.json/\1/')
  driver=$(jq -r '.[0].Driver' "$backup")
  subnet=$(jq -r '.[0].IPAM.Config[0].Subnet // ""' "$backup")
  gateway=$(jq -r '.[0].IPAM.Config[0].Gateway // ""' "$backup")

  if [ -n "$subnet" ]; then
    docker network create \
      --driver $driver \
      --subnet $subnet \
      --gateway $gateway \
      "$net_name" 2>/dev/null && \
      echo "   - 已恢复网络: $net_name" || \
      echo "   - 跳过已有网络: $net_name"
  else
    docker network create --driver $driver "$net_name" 2>/dev/null && \
      echo "   - 已恢复网络: $net_name"
  fi
done

# 3. 恢复 iptables 规则
echo "3. 恢复 iptables 规则..."
iptables-restore < "$BACKUP_DIR/iptables-backup.rules" 2>/dev/null
iptables-restore -t nat < "$BACKUP_DIR/iptables-nat-backup.rules" 2>/dev/null
echo "   - iptables 规则已恢复"

echo "=== 恢复完成 ==="

需要注意的要点

# 1. 默认网络(bridge、host、none)是 Docker 自动创建的
# 不需要备份,也无法删除

# 2. 容器网络连接关系需要手动重建
docker network connect mynet container-a

# 3. iptables 规则在 Docker 重启后会自动重建
# 备份主要是为了灾难恢复或迁移

# 4. Overlay 网络配置存在 Swarm 的持久化存储中
# 备份需要额外处理(备份 Swarm 状态)

# 5. 修改 daemon.json 后必须重启 Docker
systemctl restart docker

迁移到新机器

# 将网络配置迁移到另一台宿主机

# 1. 备份源机器
./backup-docker-network.sh
scp -r /backup/docker-network-* new-host:/backup/

# 2. 在新机器上恢复(先安装 Docker)
./restore-docker-network.sh /backup/docker-network-xxxxxxxx/

# 3. 创建容器并连接网络
docker run -d --name web --network mynet nginx
docker run -d --name db --network mynet postgres

面试问答

问:Docker 重启后网络配置会丢失吗?

默认网络(bridge、host、none)不会丢失,它们是自动重建的。自定义网络信息保存在 Docker 的数据目录中(/var/lib/docker/network/),正常重启不会丢失,但数据目录损坏可能丢失。

问:如何查看网络配置的存储位置?

# 网络元数据存储在
ls -la /var/lib/docker/network/files/
# 包含每个网络的定义文件

问:备份 iptables 规则有什么风险?

直接 restore iptables 规则可能导致 Docker 内部状态不一致(因为 Docker 也会管理 iptables)。更好的做法是备份网络定义,让 Docker 重新生成规则。

总结

备份 Docker 网络配置的核心是:备份网络定义(docker network inspect)和 daemon.json。iptables 规则由 Docker 自动管理,一般不需要单独备份。迁移到新环境时,重建自定义网络 + 重新连接容器的模式更可靠。


生产备份策略设计

生产备份策略设计

备份策略的核心目标

一个好的备份策略需要满足三个核心目标:

目标 含义 衡量指标
RPO(恢复点目标) 最多能丢失多少数据 时间范围(如 1 小时)
RTO(恢复时间目标) 恢复需要多长时间 时间(如 4 小时)
可靠性 备份一定能恢复 验证频次

备份策略设计方法论

第一步:明确业务需求

-- 与业务方确认以下问题:
-- 1. 丢失多久的数据可以接受?(RPO)
-- 2. 恢复要多快?(RTO)
-- 3. 哪些表/库最重要?
-- 4. 是否要跨地域容灾?

第二步:评估数据库特征

-- 数据量
SELECT 
  ROUND(SUM(data_length + index_length) / 1024 / 1024 / 1024, 2) AS size_gb
FROM information_schema.tables;

-- 写入量
SHOW GLOBAL STATUS LIKE 'Com_insert%';
SHOW GLOBAL STATUS LIKE 'Com_update%';

-- 数据变更频率
SHOW BINARY LOGS;  -- 看 Binlog 生成速度

第三步:选择备份类型组合

# 典型方案:全量 + 增量 + Binlog
# 全量备份:物理备份(xtrabackup)
# 增量备份:xtrabackup 增量
# 实时备份:Binlog 同步

不同业务场景的备份策略

场景 A:在线交易系统(高可用,金融级)

指标 要求
RPO < 1 分钟
RTO < 1 小时
数据量 一般 100GB-2TB

备份策略

# 1. 全量备份:每天凌晨 2:00(xtrabackup)
# 2. 增量备份:每 6 小时
# 3. 实时 Binlog:同步到备份服务器
# 4. 异地备份:每天同步到异地对象存储

# cron 配置
0 2 * * 0  /usr/bin/xtrabackup --backup --target-dir=/backup/full/\$(date +\%Y\%m\%d)    # 周日全量
0 2 * * 1-6  /usr/bin/xtrabackup --backup --target-dir=/backup/inc/\$(date +\%Y\%m\%d) --incremental-basedir=/backup/full/\$(date +\%Y\%m\%d -d 'last sunday')  # 其他天增量
* * * * *  /usr/bin/mysqlbinlog --raw --read-from-remote-server --host=localhost mysql-bin.* > /backup/binlog/  # Binlog 实时同步

恢复流程

故障 → 恢复最近的全量备份
     → 合并最近的增量备份
     → 重放 Binlog 到故障发生前的最后一条事务

场景 B:内容管理系统(中等业务)

指标 要求
RPO < 30 分钟
RTO < 4 小时
数据量 一般 10-100GB

备份策略

# 1. 全量备份:每天(mysqldump + gzip)
# 2. Binlog 保留 3 天

# cron 配置
0 3 * * * mysqldump --single-transaction --all-databases | gzip > /backup/full_\$(date +\%Y\%m\%d).sql.gz
0 0 * * * mysql -e "PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 3 DAY)"

场景 C:数据仓库/分析系统(大表,容忍停机)

指标 要求
RPO < 1 天
RTO < 8 小时
数据量 TB 级别

备份策略

# 1. 全量备份:每周 1 次(xtrabackup + 压缩)
# 2. 增量备份:每天
# 3. 不需要 Binlog(RPO 要求不高)

0 2 * * 0 /usr/bin/xtrabackup --backup --compress --parallel=4 --target-dir=/backup/full/\$(date +\%Y\%m\%d)
0 2 * * 1-6 /usr/bin/xtrabackup --backup --incremental-basedir=/backup/full/\$(date +\%Y\%m\%d -d 'last sunday') --target-dir=/backup/inc/\$(date +\%Y\%m\%d)

场景 D:开发/测试环境

指标 要求
RPO < 1 周
RTO < 1 天

备份策略:每周全量即可

备份策略的 3-2-1 原则

3 份副本至少保留 3 份备份
2 种介质备份存储在至少 2 种不同介质上如本地磁盘 + 对象存储
1 份异地至少 1 份备份存储在异地

实现方案

# 本地副本 1(SSD 磁盘)
/backup/local/

# 本地副本 2(另一块磁盘或 NAS)
/backup/nas/

# 异地副本(对象存储)
aws s3 sync /backup/ s3://my-db-backup/

# 或者从备份服务器自动同步
rsync -avz /backup/ backup_server:/backup/

备份策略配置文件模板

# backup_config.yaml
database:
  host: localhost
  port: 3306

backup:
  base_dir: /backup/mysql
  full_backup_day: sunday
  full_backup_time: "02:00"
  incremental_backup: true
  incremental_interval_hours: 6

retention:
  full_backup_keep: 30  # 保留 30 天全量
  incremental_keep: 14  # 保留 14 天增量
  binlog_keep_days: 7

remote:
  enabled: true
  type: s3  # oss, s3, scp
  bucket: my-db-backup
  region: cn-north-1

verify:
  quick_check: true  # 备份后自动 L1 验证
  full_verify_interval_days: 30  # 每月 L3 验证

备份策略的监控与告警

#!/bin/bash
# backup_monitor.sh

# 1. 检查备份是否按时完成
LAST_FULL=$(ls -la /backup/full/* 2>/dev/null | tail -1 | awk '{print $6, $7}')
if [ -z "$LAST_FULL" ]; then
  echo "❌ 最近 24 小时内没有全量备份"
  exit 2
fi

# 2. 检查备份大小是否异常
BACKUP_SIZE=$(du -sch /backup/full/ 2>/dev/null | grep total | awk '{print $1}')
echo "当前备份大小:$BACKUP_SIZE"

# 3. 检查最近 Binlog 是否同步成功
LATEST_BINLOG=$(ls -t /backup/binlog/ 2>/dev/null | head -1)
if [ -z "$LATEST_BINLOG" ]; then
  echo "❌ Binlog 同步失败"
  exit 2
fi

# 4. 检查磁盘空间
DISK_USAGE=$(df -h /backup | tail -1 | awk '{print $5}' | sed 's/%//')
if [ $DISK_USAGE -gt 85 ]; then
  echo "⚠️ 备份磁盘使用率:$DISK_USAGE%"
fi

echo "✅ 备份监控检查通过"

最佳实践清单

✅ 应该做的

  • 全量备份与增量备份结合
  • Binlog 实时同步到备份服务器
  • 使用物理备份(xtrabackup)而非逻辑备份
  • 备份文件压缩存储
  • 备份文件异地保存
  • 定期演练恢复流程
  • 备份文件设置合理的保留策略
  • 备份过程性能监控

❌ 不该做的

  • 备份文件和生产数据在同一块磁盘
  • 只保留最近一份备份
  • 从不验证备份文件完整性
  • 备份前不做磁盘空间检查
  • 全量备份和 DDL 同时进行

面试常问题

Q:怎么设计一个满足 RPO=5 分钟的备份策略?
A:全量备份(每天或每两天) + 增量备份 + 实时 Binlog 同步。Binlog 同步到独立的备份服务器,任何时间发生故障,Binlog 同步最多落后 5 分钟。这就是”1 小时全量 + 实时 Binlog”策略。

Q:RPO 和 RTO 冲突时怎么权衡?
A:RPO 越严格(丢失数据越少),通常 RTO 越长(因为要恢复更精细)。比如 PITR 恢复到分钟级需要更多 Binlog 重放。需要在业务容忍范围内找到平衡点:重要数据(用户、资金)RPO 严格,日志数据可以放宽。

Q:备份文件保留多久?
A:一般原则:全量备份保留 30 天,增量备份保留 14 天,Binlog 保留 7 天。关键业务可以延长到 90 天。但也要考虑”带库”或”对象存储”的存储成本。

Q:新的备份策略上线前应该怎么做?
A:先在测试环境验证完整流程:备份 → 验证 → 恢复 → 数据比对。确认 RPO/RTO 达标后再推广到生产。同时制定回退方案——新策略不行就切回旧的。


大数据量下如何加快备份恢复速度

大数据量下如何加快备份恢复速度

面临的挑战

当数据库达到 TB 级别时,备份和恢复成为 DBA 面临的最大挑战。全量备份可能花费数小时到数十小时,恢复同样耗时:

100GB  数据库全量备份  30-60 分钟
1TB    数据库全量备份  5-10 小时
10TB   数据库全量备份  2-3 

恢复时间是备份时间的 1.5-2 倍(因为要包括 Prepare 和索引重建)。

备份加速策略

1. 使用物理备份

物理备份(xtrabackup)比逻辑备份(mysqldump)快 10-100 倍:

备份类型 100GB 耗时 1TB 耗时
mysqldump 1-2 小时 10-20 小时
xtrabackup 5-15 分钟 1-3 小时

2. 并行备份

# xtrabackup 支持并行复制
xtrabackup --backup \
  --target-dir=/backup/full \
  --parallel=4          # 4 个并发线程复制数据文件

# mysqlpump(MySQL 5.7+)支持并行导出
mysqlpump \
  --parallel-schemas=4:mydb \
  --default-parallelism=4 \
  --output=/backup/mydb_pump.sql

3. 压缩备份

# xtrabackup 内置压缩
xtrabackup --backup \
  --compress \
  --compress-threads=4 \
  --target-dir=/backup/compressed

# mysqldump + 管道压缩
mysqldump --single-transaction mydb | gzip > /backup/mydb.sql.gz

# 使用更快的压缩算法
mysqldump --single-transaction mydb | zstd -c > /backup/mydb.sql.zst

压缩对比

算法 压缩率 速度 CPU 消耗
gzip 3-5x
zstd 3-6x 快(2-5x gzip)
lz4 2-3x 极快
bzip2 4-6x

4. 增量备份

# 全量备份(周日)
xtrabackup --backup --target-dir=/backup/full_sun

# 每日增量
xtrabackup --backup \
  --incremental-basedir=/backup/full_sun \
  --target-dir=/backup/inc_mon

# 增量备份通常比全量小 90-99%

5. 流式备份 + 远程传输

# 边备份边传输,避免中间文件
xtrabackup --backup --stream=xbstream | \
  pigz -c | \
  ssh remote_srv "cat > /backup/db_$(date +%Y%m%d).xbstream.gz"

# pigz 是 gzip 的并行版本

6. 跳过不重要的数据

# mysqldump 忽略大日志表
mysqldump --ignore-table=mydb.audit_log mydb > /backup/mydb.sql

# xtrabackup 排除特定表
xtrabackup --backup --tables-exclude='mydb\.audit_log|mydb\.temp_data'

恢复加速策略

1. 使用并行 Prepare

# xtrabackup 并行 Prepare(MySQL 8.0 支持)
xtrabackup --prepare \
  --target-dir=/backup/full \
  --use-memory=4G \
  --parallel=4

2. 跳过 Binlog 和 Redo Log 写入

# 恢复时关闭 Binlog 和双写
# 修改恢复实例的 my.cnf
skip-log-bin
innodb_doublewrite = 0  # 恢复完成后需要改回

3. 增大 Buffer Pool

# 恢复过程增大 Buffer Pool
# 恢复实例设置
innodb_buffer_pool_size = 32G  # 尽量分配大内存

4. 调整 IO 相关参数

# 恢复时的配置
innodb_io_capacity = 10000     # 允许更多 IO
innodb_io_capacity_max = 20000
innodb_flush_log_at_trx_commit = 2  # 减少 fsync
sync_binlog = 0                 # 如果开启 Binlog
innodb_log_file_size = 4G       # 大 Redo Log

5. 并行导入(逻辑备份)

-- 分割 SQL 文件后并行导入
# 分割 SQL 文件
csplit -z -f /tmp/table_ /backup/mydb.sql '/^DROP TABLE IF EXISTS/' '{*}'

# 并行导入
for f in /tmp/table_*; do
  mysql mydb < $f &
done
wait

6. 使用更大的数据目录磁盘

# 使用 NVMe SSD 或 RAID 10
# 恢复时用 tmpfs 加速
# 注意:tmpfs 重启后数据丢失,恢复完成需立即同步

7. 使用恢复专用实例

# 准备一台高性能恢复机器
# CPU:更多核心
# 内存:更大 Buffer Pool
# 磁盘:NVMe SSD
# 网络:万兆网卡(传输备份)

备份恢复加速对比表

策略 备份加速比 恢复加速比 实施难度
物理备份替代逻辑备份 10-100x 10-100x
并行复制 2-4x 2-4x
压缩 存储节省 3-5x 需解压
增量备份 10-100x 无需增量恢复
流式备份 减少磁盘 IO
跳过 Binlog 2-3x
大 Buffer Pool 1.5-2x
NVMe SSD 2-5x 2-5x 高(硬件成本)

大型数据库备份架构建议

推荐架构

生产 MySQL → xtrabackup 增量备份 → 备份服务器
                                  → 压缩存储(对象存储/磁带)
                                  → 异地备份
                                  → 恢复演练环境

备份周期

每日增量(周一至周六)→ 每周全量(周日)
每小时 Binlog 实时同步

面试常问题

Q:1TB 的数据库怎么优化备份速度?
A:必须用 xtrabackup 物理备份,开启并行(--parallel=8),使用流式压缩(--compress),配合增量备份(减少日常备份量),在业务低峰期执行。

Q:恢复时开启并行会有风险吗?
A:并行 Prepare 会增加 CPU 消耗,但不会影响数据一致性。主要风险是 CPU 跑满后恢复速度反而下降,建议根据 CPU 核数设置 并行度不超过核数。

Q:增量备份恢复速度比全量快吗?
A:增量备份本身的文件量小,但恢复时需要合并(Prepare 阶段的 merge 操作)。日常恢复更快,但如果是从损坏的全量开始恢复,合并增量需要额外时间。

Q:恢复时用 Skip Binlog 会有什么后果?
A:恢复时间显著缩短(减少 50-70%)。但恢复后如果这个实例要作为主库使用,需要重新开启 Binlog。如果只是恢复后用于数据验证,Skip Binlog 完全没问题。


在线 DDL 与备份的配合策略

在线 DDL 与备份的配合策略

什么是在线 DDL

在线 DDL(Online DDL)是指在不中断数据库读写服务的情况下执行表结构变更操作。MySQL 提供了两种在线 DDL 方式:

MySQL 5.6+ 原生 Online DDL

MySQL 5.6 引入了 InnoDB 的 Online DDL 支持,通过 ALGORITHMLOCK 子句控制:

-- 指定算法和锁级别
ALTER TABLE users ADD COLUMN age INT,
  ALGORITHM=INPLACE,
  LOCK=NONE;

-- 查看 DDL 进度
SHOW PROCESSLIST;

DDL 操作分类:

操作类型 支持的 ALGORITHM 是否允许并发 DML
添加二级索引 INPLACE ✅ 允许
删除二级索引 INPLACE ✅ 允许
添加列 INSTANT (8.0+) / INPLACE ✅ 允许(INSTANT 更快)
删除列 INPLACE ✅ 允许
修改列默认值 INPLACE ✅ 允许
修改列数据类型 COPY ❌ 不允许
添加主键 INPLACE ✅ 允许
重命名列 INPLACE ✅ 允许
修改字符集 COPY ❌ 不允许

MySQL 8.0 的新特性:INSTANT DDL

MySQL 8.0.12+ 支持即时 DDL(INSTANT DDL),对某些操作只修改元数据,不重建表:

-- 即时添加列(不占用额外磁盘,不阻塞)
ALTER TABLE users ADD COLUMN age INT, ALGORITHM=INSTANT;

-- 即时删除列默认值
ALTER TABLE users ALTER COLUMN age DROP DEFAULT, ALGORITHM=INSTANT;

pt-online-schema-change(Percona Toolkit)

对于 MySQL 5.6 之前的版本或者原生 Online DDL 不支持的操作,可以使用 Percona Toolkit:

# pt-osc 原理:创建影子表 → 同步数据 → swap 表名
pt-online-schema-change \
  h=localhost,u=root,p=password,D=mydb,t=users \
  --alter "ADD COLUMN age INT" \
  --execute

gh-ost(GitHub 出品)

gh-ost 是另一种 Online DDL 方案,它使用 Binlog 进行增量同步:

gh-ost \
  --user="root" \
  --password="password" \
  --host="localhost" \
  --database="mydb" \
  --table="users" \
  --alter="ADD COLUMN age INT" \
  --execute

在线 DDL 对备份的影响

问题 1:DDL 期间备份一致性

-- 如果在线 DDL 运行时进行备份
-- 备份可能捕获到新旧两张表的数据

特别是 mysqldump 使用 --single-transaction 时,由于 DDL 操作(如 DROP COLUMN)的元数据变更,可能导致备份进程崩溃或数据不一致:

事务备份开始 → 读快照 A(含列 name)
DDL → ALTER TABLE users DROP COLUMN name
备份继续 → 试图读 name 列 → 报错"Unknown column 'name'"

问题 2:DDL 导致备份文件重复或遗漏

物理备份(xtrabackup)在复制数据文件时,如果遇到 DDL,可能导致备份中包含新旧两种版本的 .ibd 文件。

问题 3:DDL 期间的备份 Schema 不一致

备份表 users → 含列 a,b,c(ddl 前版本)
DDL → ADD COLUMN d
备份表 orders → 含列 x,y,z
DDL → DROP COLUMN y
备份表 products → 含列 p,q

恢复后不同表处于不同时间点——数据不一致。

最佳实践:DDL 与备份的协调策略

策略一:备份窗口避开 DDL

最简单也最有效的策略:

# 定时任务
00:00 - 全量备份(xtrabackup)
01:00 - 01:30 - DDL 维护窗口
02:00 - 增量备份(确保 DDL 变更被捕获)

策略二:DDL 完成后立刻做增量备份

# 1. 开始 DDL
pt-online-schema-change --alter "ADD INDEX idx_email(email)" --execute

# 2. DDL 完成后立即进行增量备份
xtrabackup --backup --incremental-basedir=/backup/full --target-dir=/backup/inc_ddl

这样增量备份就能捕获 DDL 变更后的完整状态。

策略三:mysqldump 配合 –master-data

# DDL 前备份
mysqldump --single-transaction --master-data=2 mydb > before_ddl.sql

# 执行 DDL
mysql -e "ALTER TABLE mydb.users ADD INDEX..."

# DDL 后备份
mysqldump --single-transaction --master-data=2 mydb > after_ddl.sql

恢复时,需要知道 DDL 操作在 Binlog 中的位置,精确恢复到 DDL 之前或之后。

策略四:使用 pt-osc 的 –check-alter 验证

# 在 pt-osc 执行前先验证备份是否存在
pt-online-schema-change \
  --check-alter \
  --alter "ADD COLUMN age INT" \
  --execute 2>&1 | grep -i "backup"

策略五:备份前检查 DDL 进度

-- 查看是否有正在执行的 DDL
SELECT * FROM performance_schema.events_statements_current
WHERE SQL_TEXT LIKE 'ALTER%' OR SQL_TEXT LIKE 'CREATE INDEX%';

-- 查看是否有表正在重建
SELECT * FROM information_schema.INNODB_ALTER_TABLE_PROGRESS;

各种 DDL 工具对备份的影响对比

DDL 工具 备份冲突 恢复兼容性 推荐策略
原生 ALGORITHM=INPLACE ⚠️ 可能影响 mysqldump ✅ 恢复正常 DDL 后做备份
原生 ALGORITHM=INSTANT (8.0+) ✅ 不影响 ✅ 恢复正常 最安全
pt-online-schema-change ✅ 不影响 mysqldump ✅ 恢复正常 自动更新
gh-ost ✅ 不影响 自动更新

实战建议

#!/bin/bash
# ddl_with_backup.sh

DB="mydb"
TABLE="users"
ALTER_SQL="ADD COLUMN age INT DEFAULT 0 AFTER name"

echo "=== 步骤 1: 检查当前备份时间 ==="
ls -la /backup/full/ | tail -5

echo "=== 步骤 2: 记录 DDL 前备份位置 ==="
mysql -e "SHOW MASTER STATUS" > /tmp/ddl_before_position.txt

echo "=== 步骤 3: 执行 DDL ==="
mysql -e "ALTER TABLE $DB.$TABLE $ALTER_SQL, ALGORITHM=INPLACE, LOCK=NONE"
echo "DDL 完成: $(date)"

echo "=== 步骤 4: DDL 后立即做增量备份 ==="
xtrabackup --backup \
  --incremental-basedir=/backup/full \
  --target-dir=/backup/inc_$(date +%Y%m%d_%H%M)

echo "=== 步骤 5: 记录 DDL 后备份位置 ==="
mysql -e "SHOW MASTER STATUS" > /tmp/ddl_after_position.txt

echo "✅ 完成:DDL 后增量备份已创建"

面试常问题

Q:mysqldump 备份时遇到 ALTER TABLE 会怎样?
A:可能失败。因为 --single-transaction 使用一致性快照读,但 DDL 操作会导致表结构变更,后续备份操作发现表结构不一致而报错。

Q:物理备份(xtrabackup)时进行在线 DDL 安全吗?
A:大部分场景是安全的。InnoDB 的在线 DDL 在备份期间是允许的,但部分 DDL 操作(如 DROP COLUMN)可能导致备份文件中的 .ibd 文件与 Redo Log 的一致性受影响。建议在 DDL 前后各做一次备份。

Q:INSTANT DDL 对备份有什么优势?
A:INSTANT DDL 只修改元数据,不重建表文件,对备份几乎无影响。备份文件中的 .ibd 文件在 DDL 前后都是同一份物理文件,不会出现一致性冲突。

Q:备份窗口和 DDL 窗口冲突了怎么办?
A:最稳妥的方案是 DDL 完成后立刻做增量备份。如果冲突无法避免,优先保证备份完整,DDL 延后到备份完成后再执行。


验证备份文件可用性

验证备份文件可用性

为什么必须验证备份

这是 DBA 的第一守则:没有验证过的备份等于没有备份

备份文件可能在以下情况下”失效”:
– 写入过程中磁盘满了,备份文件不完整
– CRC 校验失败的损坏文件
– MySQL 版本升级后,旧备份格式与新版本不兼容
– 备份脚本中有 BUG,导致数据遗漏
– 文件被误删除或覆盖

验证方法分类

按验证深度

验证级别 方法 耗时 可信度
L0 文件存在性检查 几秒 ⭐ 最低
L1 文件完整性校验 几分钟 ⭐⭐
L2 逻辑内容检查 几十秒 ⭐⭐⭐
L3 恢复 + 数据校验 几十分钟 ⭐⭐⭐⭐⭐

生产环境建议

每周:L0 + L1 快速检查
每月:L2 逻辑检查
每季度:L3 完整恢复验证

L0:文件存在性检查

#!/bin/bash
# 检查备份文件是否存在且非空

BACKUP_DIR="/backup/mysql"
TODAY=$(date +%Y%m%d)

# 检查全量备份
if [ -f "$BACKUP_DIR/full_$TODAY.xbstream.gz" ]; then
    FILE_SIZE=$(stat -c%s "$BACKUP_DIR/full_$TODAY.xbstream.gz")
    if [ $FILE_SIZE -gt 1000000 ]; then  # > 1MB
        echo "✅ 全量备份文件存在,大小:$FILE_SIZE 字节"
    else
        echo "❌ 全量备份文件过小:$FILE_SIZE 字节"
        exit 1
    fi
else
    echo "❌ 全量备份文件不存在"
    exit 1
fi

L1:文件完整性校验

1. MD5/SHA256 校验

# 备份时计算校验和
md5sum /backup/mydb_20250615.sql.gz > /backup/mydb_20250615.sql.gz.md5

# 验证时比对
md5sum -c /backup/mydb_20250615.sql.gz.md5

2. Gzip 完整性检查

# 校验压缩文件完整性(不实际解压)
gzip -t /backup/mydb_20250615.sql.gz
echo $?  # 0 = 正常,非 0 = 损坏

3. xtrabackup 完整性检查

# xtrabackup 备份验证
# 尝试 Prepare(但回滚事务可以继续使用 prepare --apply-log-only)
xtrabackup --prepare --target-dir=/backup/full 2>&1 | tail -10

# 检查 xtrabackup_checkpoints
cat /backup/full/xtrabackup_checkpoints

4. mysqlbinlog 检查

# 检查 Binlog 文件是否可读
mysqlbinlog /var/lib/mysql/mysql-bin.000123 > /dev/null 2>&1
if [ $? -eq 0 ]; then
    echo "✅ Binlog 文件完整性良好"
else
    echo "❌ Binlog 文件损坏"
fi

L2:逻辑内容检查

1. SQL 备份文件内容检查

# 检查 mysqldump 文件头部
head -50 mydb.sql | grep -q "CREATE TABLE"
if [ $? -eq 0 ]; then
    echo "✅ 备份文件包含有效的 CREATE TABLE 语句"
fi

# 检查备份文件行数(太小可能不全)
echo "SQL 文件行数:$(wc -l < mydb.sql)"

# 检查结尾是否完整
tail -20 mydb.sql | grep -q "Dump completed"
if [ $? -eq 0 ]; then
    echo "✅ 备份文件结尾完整"
fi

# 检查表数量
grep -c "CREATE TABLE" mydb.sql

2. xtrabackup 内容检查

# 查看备份中包含哪些表
ls -la /backup/full/mydb/*.ibd | wc -l

# 检查 binlog 位置记录
cat /backup/full/xtrabackup_binlog_info

# 检查备份元信息
cat /backup/full/xtrabackup_info | grep -E "start_time|end_time|binlog_pos|uuid"

3. mysqldump 的逻辑验证

# 不执行,仅检查 SQL 语法的正确性
mysql -u root -p --force < /dev/null 2>&1
# 或者用 pt-find
pt-find --host=localhost --user=root --password=xxx

# 最直接的检查:尝试导入到临时数据库
mysql -e "CREATE DATABASE IF NOT EXISTS verify_backup"
mysql mydb_verify < mydb.sql 2>&1 | grep -i error
mysql -e "DROP DATABASE IF EXISTS verify_backup"

L3:完整恢复验证(黄金标准)

全流程自动化脚本

#!/bin/bash
# verify_and_restore.sh — 安全验证备份文件

BACKUP_FILE=$1
DB_NAME="verify_$(date +%s)"
MYSQL_CMD="mysql -u root -p'$MYSQL_ROOT_PASSWORD'"

echo "=== 开始验证备份文件 ==="
echo "备份文件:$BACKUP_FILE"

# 1. 创建临时数据库
$MYSQL_CMD -e "CREATE DATABASE $DB_NAME"

# 2. 恢复备份到临时数据库
case "$BACKUP_FILE" in
  *.sql.gz)
    gunzip -c $BACKUP_FILE | $MYSQL_CMD $DB_NAME
    ;;
  *.sql)
    $MYSQL_CMD $DB_NAME < $BACKUP_FILE
    ;;
  *.xbstream*)
    # xtrabackup 恢复需要先 Prepare 再 copy-back
    # 建议使用独立的 MySQL 实例
    echo "xtrabackup 恢复请使用独立环境"
    exit 1
    ;;
  *)
    echo "未知备份格式"
    exit 1
esac

# 3. 验证数据
echo "=== 数据验证 ==="
$MYSQL_CMD -e "
  SELECT '表总数' as '检查项', COUNT(*) as '结果'
  FROM information_schema.tables
  WHERE table_schema = '$DB_NAME';
"

# 4. 检查特定表的数据
$MYSQL_CMD $DB_NAME -e "
  SELECT 'users表' as '表名', COUNT(*) as '行数' FROM users;
  SELECT 'orders表' as '表名', COUNT(*) as '行数' FROM orders;
"

# 5. 检查索引
$MYSQL_CMD -e "
  SELECT TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
  FROM information_schema.STATISTICS
  WHERE TABLE_SCHEMA = '$DB_NAME';
"

# 6. 清理临时数据库
$MYSQL_CMD -e "DROP DATABASE $DB_NAME"

echo "=== 验证完成 ==="

自动化验证工具

pt-table-checksum

# 校验恢复后的数据与源数据是否一致
pt-table-checksum \
  h=recovery_host,u=root,p=password \
  --databases=mydb \
  --tables=users,orders

percona-playback

# 回放以前的生产流量验证
percona-playback \
  --mysql-host=localhost \
  --mysql-password=xxx \
  --workload-file=/tmp/general.log

时间与资源规划

备份大小 预计下载时间 L1 验证 L2 验证 L3 完整恢复
10GB 1-2 分钟 10 秒 30 秒 5 分钟
100GB 10-20 分钟 1 分钟 5 分钟 30-60 分钟
1TB 1-2 小时 10 分钟 30 分钟 2-4 小时

面试常问题

Q:最快速的备份验证方式是什么?
A:L0 级文件存在性和非空检查,配合 L1 级的 gzip/xtrabackup 完整性校验。可以在几分钟内完成,但只能验证”文件没坏”,不能验证”数据完整”。

Q:怎么确保恢复后的数据和生产完全一致?
A:做 L3 完整恢复验证,然后在恢复的数据库上执行 checksum 查询(CHECKSUM TABLE)或使用 pt-table-checksum 比对。

Q:备份验证的频率应该是多少?
A:每个备份完成后都应该做 L1 级验证。全量备份至少每月做一次 L3 完整恢复验证。关键业务建议每周验证一次。

Q:验证过程中如果发现备份损坏怎么办?
A:立即检查最近的可用备份。保留多代备份(至少 7 代),如果当天的备份坏了就用昨天的。同时调查备份脚本是否有 BUG,修复后重新备份。


搭建备份恢复演练环境

搭建备份恢复演练环境

为什么需要演练环境

备份恢复演练是 DBA 的核心责任。没有经过验证的备份等于没有备份。根据业界经验:

  • 40% 的备份在需要恢复时存在问题(数据不完整、元数据不一致、文件损坏等)
  • 实践中常见的问题:备份文件损坏、权限问题、版本不兼容、空间不足

所以,定期演练恢复流程是数据库运维的必修课。

搭建演练环境的通用流程

生产环境备份文件 → 传输到演练环境 → 执行恢复 → 验证数据 → 清理环境

场景一:本机演练(临时实例)

在同一个服务器上启动临时 MySQL 实例,从备份恢复后验证。

方法 1:使用 Docker 快速搭建隔离环境

# 1. 启动临时 MySQL 容器
docker run -d \
  --name mysql_recovery_test \
  -e MYSQL_ROOT_PASSWORD=test123 \
  -v /backup:/backup:ro \
  -p 3307:3306 \
  mysql:8.0

# 2. 进入容器
docker exec -it mysql_recovery_test bash

# 3. 使用 mysqldump 备份恢复
mysql -u root -p test123 < /backup/mydb_20250615.sql

# 4. 使用 xtrabackup 备份恢复
apt-get update && apt-get install -y percona-xtrabackup-80
xtrabackup --copy-back --target-dir=/backup/xtrabackup_full/
chown -R mysql:mysql /var/lib/mysql/

方法 2:独立数据目录(本机多实例)

# 1. 准备 MySQL 二进制包
wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.35-linux-glibc2.28-x86_64.tar.xz
tar xf mysql-8.0.35-*.tar.xz -C /usr/local/
mv /usr/local/mysql-8.0.35-* /usr/local/mysql_recovery

# 2. 创建数据目录
mkdir -p /data/mysql_recovery_data
chown -R mysql:mysql /data/mysql_recovery_data

# 3. 初始化数据目录
/usr/local/mysql_recovery/bin/mysqld \
  --initialize-insecure \
  --datadir=/data/mysql_recovery_data \
  --user=mysql

# 4. 启动临时实例
/usr/local/mysql_recovery/bin/mysqld \
  --datadir=/data/mysql_recovery_data \
  --port=3308 \
  --socket=/tmp/mysql_recovery.sock \
  --user=mysql \
  --skip-log-bin &

# 5. 恢复备份
/usr/local/mysql_recovery/bin/mysql \
  -S /tmp/mysql_recovery.sock \
  -u root < /backup/mydb.sql

# 6. 验证数据
/usr/local/mysql_recovery/bin/mysql \
  -S /tmp/mysql_recovery.sock \
  -u root -e "SELECT COUNT(*) FROM mydb.users"

# 7. 清理
kill `cat /data/mysql_recovery_data/*.pid`
rm -rf /data/mysql_recovery_data

场景二:使用独立服务器演练

架构

备份服务器(存储备份文件)
     |
     | rsync / scp / s3
     ↓
演练服务器(独立的 MySQL 实例)

自动化脚本($RECOVERY_SERVER 为目标 IP)

#!/bin/bash
# recover_test.sh — 自动恢复验证脚本

BACKUP_FILE=$1
RECOVERY_SERVER="10.0.1.200"

# 1. 传输备份
rsync -avz /backup/$BACKUP_FILE mysql@$RECOVERY_SERVER:/tmp/

# 2. 在恢复服务器上执行恢复
ssh mysql@$RECOVERY_SERVER << 'REMOTE'
  # 停止正在运行的 MySQL
  systemctl stop mysql
  rm -rf /var/lib/mysql/*

  # 恢复备份
  if [[ "$BACKUP_FILE" == *.sql ]]; then
    # mysqldump 恢复
    mysqld_safe --skip-grant-tables & 
    sleep 3
    mysql -u root < /tmp/$BACKUP_FILE
  elif [[ "$BACKUP_FILE" == *.xbstream ]]; then
    # xtrabackup 恢复
    xtrabackup --prepare --target-dir=/tmp/backup/
    xtrabackup --copy-back --target-dir=/tmp/backup/
    chown -R mysql:mysql /var/lib/mysql/
  fi

  systemctl start mysql
REMOTE

# 3. 验证
ssh mysql@$RECOVERY_SERVER "mysql -e 'CHECK TABLE mydb.users; SHOW TABLES;'"

场景三:PITR 恢复演练(完整流程)

#!/bin/bash
# pitr_test.sh

# 模拟场景:数据库恢复到 "2025-06-15 14:59:59"

FULL_BACKUP_DIR="/backup/full/20250614"
BINLOG_DIR="/backup/binlog"
RECOVERY_TIME="2025-06-15 14:59:59"

# 1. 准备恢复实例
docker run -d --name pitr_test mysql:8.0

# 2. 恢复全量备份
docker cp $FULL_BACKUP_DIR pitr_test:/tmp/full_backup
docker exec pitr_test bash -c "cd /tmp/full_backup && xtrabackup --prepare --target-dir=."
docker exec pitr_test bash -c "rm -rf /var/lib/mysql/* && xtrabackup --copy-back --target-dir=/tmp/full_backup"
docker exec pitr_test bash -c "chown -R mysql:mysql /var/lib/mysql"

# 3. 获取全量备份的 Binlog 位置
BINLOG_POS=$(cat $FULL_BACKUP_DIR/xtrabackup_binlog_info | awk '{print $2}')

# 4. 应用 Binlog 到目标时间点
docker cp $BINLOG_DIR pitr_test:/tmp/binlog
docker exec pitr_test bash -c "
  mysqlbinlog \
    --start-position=$BINLOG_POS \
    --stop-datetime='$RECOVERY_TIME' \
    /tmp/binlog/mysql-bin.* | mysql -u root
"

# 5. 验证数据
docker exec pitr_test mysql -u root -e "SELECT COUNT(*) FROM mydb.orders"
echo "PITR 恢复完成,请验证数据准确性"

验证备份的核心检查项

1. 表结构验证

-- 检查表和字段数量是否一致
SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'mydb';

-- 检查自增 ID 最大值
SELECT MAX(id) FROM mydb.orders;

2. 数据一致性验证

-- 行数对比
SELECT COUNT(*) FROM mydb.orders;

-- 关键业务指标
SELECT SUM(amount) FROM mydb.orders WHERE status = 'completed';

-- 检查约束是否有效
SELECT * FROM mydb.orders WHERE status NOT IN ('pending','paid','shipped','delivered','cancelled');

3. 性能验证

-- 简单查询走索引
EXPLAIN SELECT * FROM mydb.orders WHERE id = 1;

-- 检查索引状态
SHOW INDEX FROM mydb.orders;

4. 自动化验证脚本

#!/usr/bin/env python3
# verify_backup.py

import pymysql
import sys

def verify_backup(host, user, password, db):
    conn = pymysql.connect(host=host, user=user, passwd=password, db=db)
    cursor = conn.cursor()

    checks = [
        ("表数量", "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s"),
        ("用户数", "SELECT COUNT(*) FROM users"),
        ("订单数", "SELECT COUNT(*) FROM orders"),
        ("最大订单金额", "SELECT MAX(amount) FROM orders"),
    ]

    all_ok = True
    for name, sql in checks:
        cursor.execute(sql, (db,))
        result = cursor.fetchone()[0]
        print(f"{name}: {result}")
        if result is None or result == 0:
            print(f"  ⚠️  {name} 异常!")
            all_ok = False

    cursor.close()
    conn.close()

    if all_ok:
        print("✅ 备份验证通过")
    else:
        print("❌ 备份验证失败")
        sys.exit(1)

if __name__ == "__main__":
    verify_backup("localhost", "root", "password", "mydb")

演练计划的建议

频率 演练内容
每周 单表 mysqldump 备份恢复(自动化)
每月 全量 xtrabackup 恢复演练
每季度 PITR 演练(Binlog 重放)
每半年 跨版本升级 + 迁移恢复演练
每年 灾难恢复(DR)演练

面试常问题

Q:为什么备份验证比备份本身更重要?
A:因为未经验证的备份可能在需要时无法使用(文件损坏、版本不兼容、权限问题等)。定期演练恢复流程是确保备份可用的唯一方式。

Q:恢复演练应该包含哪些步骤?
A:传输备份文件 → 启动恢复环境 → 执行恢复 → 验证数据完整性 → 清理环境。重点验证:表结构完整、数据行数一致、索引可用。

Q:在生产服务器上做恢复演练安全吗?
A:不安全。应该使用独立的演练环境(Docker 容器、备用服务器或临时实例),避免影响生产数据库。生产数据目录的覆盖是不可逆的。


Binlog 在增量备份中的核心作用

Binlog 在增量备份中的核心作用

Binlog 与备份的关系

Binary Log(二进制日志)是 MySQL 最重要的日志文件之一,它记录了所有对数据库的修改操作(DDL 和 DML),但不包括 SELECTSHOW 操作。

Binlog 在备份恢复体系中扮演三个关键角色:

1. 增量备份的基础记录全量备份之后的所有变更
2. PITR 的核心通过重放 Binlog 恢复到任意时间点
3. 主从复制的载体从库通过读取 Binlog 同步主库变更

为什么 Binlog 适合做增量

Binlog 有以下特性使其成为增量备份的天然载体:

1. 顺序写入

Binlog 以追加方式写入,是顺序 IO,比数据文件的随机 IO 快得多:

写入流程:
事务提交 → 写入 Binlog(追加到文件末尾,顺序写)
        → 写入完成 → 返回成功

2. 文件管理方便

-- 查看所有 Binlog 文件
SHOW BINARY LOGS;

-- 切换到下一个 Binlog 文件
FLUSH LOGS;

-- 设置保留时间
-- 5.7
SET GLOBAL expire_logs_days = 7;
-- 8.0
SET GLOBAL binlog_expire_logs_seconds = 604800;

3. 多种记录格式

# Row 格式(默认,推荐)
binlog_format = ROW
# 记录每一行数据的变更前和变更后值
# 优点:精确恢复单行、兼容性好
# 缺点:文件大

# Statement 格式(5.7 之前默认)
# 记录执行的 SQL 语句
# 优点:文件小
# 缺点:函数如 NOW() 可能导致主从数据不一致

# Mixed 格式
# 混合模式,MySQL 自动选择
# 确定性操作用 Statement,不确定性用 Row

增量备份的不同实现

方案一:Binlog 文件直接作为增量备份

# 备份 Binlog 文件
cp /var/lib/mysql/mysql-bin.000001 /backup/binlog/
cp /var/lib/mysql/mysql-bin.000002 /backup/binlog/
# ...

# 恢复时(先恢复全量备份)
mysqlbinlog mysql-bin.000001 mysql-bin.000002 | mysql -u root -p

优点:简单直接,不需要额外工具
缺点:很难管理”基于哪个全量备份的增量”

方案二:Binlog + LSN 增量备份(xtrabackup 增量)

xtrabackup 增量备份的原理是基于 LSN(Log Sequence Number),但恢复时往往还需要 Binlog 来补全”从增量备份完成到目标时间点”之间的数据:

# 1. 全量备份(周一)
xtrabackup --backup --target-dir=/backup/full

# 2. 增量备份(周二)
xtrabackup --backup --incremental-basedir=/backup/full \
           --target-dir=/backup/inc1

# 3. 如果周二 15:00 发生故障
# 恢复到周二的增量备份 + 重放周二 15:00 前的 Binlog

方案三:Binlog Server

# 使用 mysqlbinlog 的远程同步功能
mysqlbinlog --read-from-remote-server \
            --host=master_host \
            --user=repl \
            --password=xxx \
            --raw \
            --stop-never \
            mysql-bin.000001 | \
  gzip > /backup/binlog_server/binlog.$(date +%Y%m%d%H%M%S).gz

优点:实时同步 Binlog,主库故障时 Binlog 不会丢失
缺点:占用网络带宽和备份服务器的存储

增量备份恢复流程

时间线:
14:00 → 全量备份完成
14:01 → 开始产生新的 Binlog(mysql-bin.000050)
15:30 → 误操作发生

恢复步骤:
1. 恢复 14:00 的全量备份 → 恢复到 14:00 状态
2. 应用 Binlog(mysql-bin.000050) 从 14:00 到 15:29:59
3. 数据库恢复到 15:29:59 的状态(误操作前)

恢复命令示例

# 确认全量备份的 Binlog 位置
cat /backup/full/xtrabackup_binlog_info
# 输出:mysql-bin.000049 438945618

# 应用增量备份后的 Binlog(从全量备份完成后开始)
mysqlbinlog \
  --start-position=438945618 \
  --stop-datetime="2025-06-15 15:29:59" \
  /var/lib/mysql/mysql-bin.000050 \
  /var/lib/mysql/mysql-bin.000051 \
  | mysql -u root -p

Binlog 备份的安全策略

1. 异地保存

# 每天将 Binlog 同步到远程服务器
rsync -avz /var/lib/mysql/mysql-bin.* backup_server:/backup/binlog/

# 或使用对象存储
tar czf - /var/lib/mysql/mysql-bin.* | \
  s3cmd put - s3://my-bucket/binlog/$(date +%Y%m%d).tar.gz

2. 加密传输

# 使用 SSL 加密传输 Binlog 备份
mysqlbinlog \
  --read-from-remote-server \
  --ssl-mode=REQUIRED \
  --raw \
  --host=master_host \
  mysql-bin.*

3. 定期清理

-- 防止磁盘写满
-- 保留 7 天
SET GLOBAL binlog_expire_logs_seconds = 604800;

-- 手动清理(谨慎!)
PURGE BINARY LOGS BEFORE '2025-06-08 00:00:00';
PURGE BINARY LOGS TO 'mysql-bin.000100';

监控 Binlog 状态

-- Binlog 文件列表和大小
SHOW BINARY LOGS;

-- 当前正在写入的 Binlog
SHOW MASTER STATUS;

-- Binlog 写入量
SHOW GLOBAL STATUS LIKE 'Binlog_%';

-- 重要指标
Binlog_cache_use         -- 事务写入 Binlog 缓存的次数
Binlog_cache_disk_use    -- 事务写入磁盘 Binlog 缓存的次数
Binlog_bytes_written     -- 总写入字节数

最佳实践

[mysqld]
# Binlog 配置
log_bin = mysql-bin
binlog_format = ROW
binlog_row_image = FULL
expire_logs_days = 7          # 5.7
# binlog_expire_logs_seconds = 604800  # 8.0
max_binlog_size = 1G          # 每个 Binlog 文件大小

# 全量备份策略
# 每天凌晨 2:00 全量备份(使用 xtrabackup)
# Binlog 实时同步到备份服务器

# 恢复考验
# 每季度至少一次 PITR 演练

面试常问题

Q:Binlog 在增量备份中扮演什么角色?
A:Binlog 记录了全量备份之后的所有数据变更。将全量备份恢复到某个时间点后,再重放到该时间点之前的 Binlog,就能实现增量恢复和 PITR。

Q:Binlog 保留多长时间合适?
A:至少能覆盖最长的 PITR 需求。一般是”全量备份间隔 + 1 天”的 Binlog 量。例如每天做全量备份,则 Binlog 保留 2 天(1 天的全量间隔 + 1 天缓冲),但生产环境通常保留 7 天。

Q:增量备份和 Binlog 备份的区别?
A:增量备份(如 xtrabackup 增量)是基于 LSN 的物理层面增量的,只复制修改过的数据页。Binlog 备份是逻辑层面变更的记录。恢复时常用方式:全量备份 → xtrabackup 增量 → Binlog 补齐到精确时间点。

Q:Binlog 文件丢失了怎么办?
A:如果 Binlog 文件丢失,则全量备份之后的数据无法恢复,只能恢复到全量备份的时间点。这就是为什么 Binlog 需要异地备份!


xtrabackup 备份原理与优势

xtrabackup 备份原理与优势

什么是 xtrabackup

xtrabackup 是 Percona 开发的开源物理备份工具,专门用于 MySQL 的在线热备份。它支持 InnoDB、XtraDB、MyISAM 等多种存储引擎,是目前生产环境中最广泛使用的 MySQL 物理备份工具。

Percona XtraBackup 包含两个主要工具:
xtrabackup:核心工具,备份 InnoDB 数据
xbstream:流式备份格式支持

备份原理

核心思路:Datafile + Redo Log

xtrabackup 的核心原理可以概括为两个步骤:

阶段一:复制数据文件(可能不一致)
         ↓
阶段二:同步 Redo Log(使数据一致)

详细流程

1. 启动备份记录当前 LSNLog Sequence Number
2. 后台线程持续监控 Redo Log 的写入
3. 前台线程开始复制 InnoDB 数据文件.ibd
   ── 此时复制的文件是"时间点快照"——可能有部分页正在被 Redo所以不一致
4. 复制完成后停止监控 Redo Log
5.  MyISAM 表加读锁复制 MyISAM 数据
6. 记录 Binlog 位置用于 PITR
7. 释放锁

至此备份完成但数据文件内部可能不一致
需要执行 --prepare "重放"Redo Log使数据文件达到一致状态

Prepare 阶段

xtrabackup --prepare --target-dir=/backup/base

Prepare 做两件事:

1. Redo 阶段前滚):
    Redo Log 中已提交但未写入数据文件的修改应用到数据文件

2. Undo 阶段回滚):
   将备份过程中未提交的事务回滚

完成 Prepare 后的数据文件等同于 MySQL crash recovery 后的状态——完全一致。

关键技术细节

Redo Log 的持续监控

xtrabackup 不是”开始备份时看一眼 Redo Log 位置、备份完再处理”。它会在整个备份过程中持续监听新生成的 Redo Log,通过:

# 后台线程持续读取 Redo Log
# 监控的位置(LSN)随数据文件复制进度推进

这个过程叫Redo Log shadow copy

LSN 对齐

xtrabackup 通过 LSN 来对齐数据文件和 Redo Log:

开始备份 → 记录 LSN_start
         → 复制数据文件
         → 同时监听从 LSN_start 开始的新 Redo Log
复制完成 → 记录 LSN_end
Prepare  → 从 LSN_start 重放到 LSN_end

基本用法

全量备份

# 备份
xtrabackup --backup --target-dir=/backup/base --user=root --password=xxx

# Prepare(恢复前必须执行)
xtrabackup --prepare --target-dir=/backup/base

# 恢复(复制到数据目录)
xtrabackup --copy-back --target-dir=/backup/base
# 或手动
cp -r /backup/base/* /var/lib/mysql/
chown -R mysql:mysql /var/lib/mysql/

增量备份

# 1. 先做一次全量备份
xtrabackup --backup --target-dir=/backup/base

# 2. 基于全量做增量备份
xtrabackup --backup --target-dir=/backup/inc1 \
           --incremental-basedir=/backup/base

# 3. 再做一次增量(基于上一次增量)
xtrabackup --backup --target-dir=/backup/inc2 \
           --incremental-basedir=/backup/inc1

# 4. Prepare 时需要合并增量
xtrabackup --prepare --apply-log-only --target-dir=/backup/base
xtrabackup --prepare --apply-log-only --target-dir=/backup/base \
           --incremental-dir=/backup/inc1
xtrabackup --prepare --apply-log-only --target-dir=/backup/base \
           --incremental-dir=/backup/inc2
xtrabackup --prepare --target-dir=/backup/base  # 最后一步不加 apply-log-only

流式备份

# 直接流式压缩并发送到远程
xtrabackup --backup --stream=xbstream ./ | \
  gzip | ssh user@remote "cat > /backup/db_$(date +%Y%m%d).xbstream.gz"

xtrabackup 的优势

对比 mysqldump

特性 xtrabackup mysqldump
备份类型 物理备份 逻辑备份
备份速度 快(10-100MB/s) 慢(取决于 SELECT 速度)
恢复速度 快(文件复制) 慢(逐行 INSERT)
增量备份 ✅ 原生支持 ❌ 不支持
100GB+ 数据库 ✅ 可行 ❌ 太慢
在线热备 ✅ 不影响业务 ✅ 但速度慢

对比 MySQL Enterprise Backup

特性 xtrabackup MEB
开源 ✅ 完全开源 ❌ 商业授权
功能 核心功能齐全 更多企业特性
社区 活跃 Oracle 支持
成本 免费 付费

注意事项

1. Prepare 必须在备份机上执行

Prepare 是 CPU 密集操作(重放 Redo Log),建议在备份服务器或低峰期做:

# 在备份服务器执行(不要在运行 MySQL 的生产机上做)
xtrabackup --prepare --target-dir=/backup/base

2. 磁盘空间至少 2 倍

  • 备份过程中需要存储原始数据 + Redo Log 副本
  • Prepare 阶段需要额外空间
  • 建议预留备份大小的 2-3 倍空间

3. 版本兼容

# xtrabackup 版本必须与 MySQL 版本对应
# 2.4.x → MySQL 5.7
# 8.0.x → MySQL 8.0

# 不匹配的版本会导致备份失败或数据损坏

4. 流式备份的内存占用

# 流式备份在高并发时需要注意内存
xtrabackup --backup --stream=xbstream --throttle=100M ./ | ...
# --throttle 限制 IO 速度,减少对业务的影响

面试常问题

Q:xtrabackup 备份时数据库还在写入,为什么备份文件是一致的?
A:因为在 Prepare 阶段 Redo Log 会被”重放”到数据文件中,将已提交但未落盘的数据补上,将未提交的事务回滚掉。最终得到的就是一个一致性数据快照。

Q:全量备份 + 增量备份可以恢复到任意时间点吗?
A:需要配合 Binlog 可以实现。Xtrabacup 备份记录了 Binlog 位置,在全量/增量基础上再应用 Binlog 即可实现时间点恢复(PITR)。

Q:增量备份的原理是什么?
A:InnoDB 数据页以 LSN 标识版本。增量备份只复制 LSN 大于上次备份的页,通过 xtrabackup_checkpoints 文件中的 LSN 范围实现。Prepare 时将增量页合并到全量中。

Q:xtrabackup 如何限制对业务的影响?
A:通过 --throttle 限制 IO 速率(如 100MB/s);在业务低峰期执行;备份过程中监控磁盘 IO 和 CPU 使用率。


备份方式详解:物理备份与逻辑备份

备份方式详解:物理备份与逻辑备份

什么是备份

数据库备份是指将数据库中的数据和结构复制到其他位置,以便在数据丢失或损坏时能够恢复。MySQL 的备份方式主要分为两大类:

  • 物理备份:直接复制数据库文件
  • 逻辑备份:通过 SQL 语句导出数据

物理备份

工作原理

直接复制 MySQL 的数据文件(.ibd、.frm、ibdata1、Redo Log 等),保留数据库文件的二进制格式。

# 最简单的物理备份(但生产环境不能这样冷备)
systemctl stop mysql
cp -r /var/lib/mysql /backup/
systemctl start mysql

# 实际使用 xtrabackup 或 MySQL Enterprise Backup 在线热备

常用工具

工具 类型 特点
xtrabackup(Percona) 开源热备 最流行,支持增量备份
MySQL Enterprise Backup 商业 Oracle 官方,功能齐全
cp / rsync + 锁 手动 简单,但需要停机
LVM 快照 文件系统级 利用 LVM 快照一致性

物理备份的优缺点

优点
1. 速度快:直接复制文件,比逐条导出 SQL 快 10-100 倍
2. 数据量大时更合适:100GB+ 数据库的备份只有物理备份靠谱
3. 恢复快:直接复制回数据目录即可,不需要逐条执行 INSERT
4. 包含所有数据:索引结构、事务日志等全部保留
5. 支持在线备份:xtrabackup 可以在业务运行时备份

缺点
1. 可移植性差:恢复的目标机器需要相同或兼容的 MySQL 版本和配置
2. 选择性差:不能只恢复某张表(xtrabackup 支持单表恢复,但有限制)
3. 文件较大:包含索引、碎片等,比逻辑备份占空间大
4. 依赖特定存储引擎:与 InnoDB 强绑定

逻辑备份

工作原理

通过 SQL 语句(SELECT ... INTO OUTFILEmysqldump)导出 CREATE TABLE 和 INSERT 语句,或 CSV 格式的数据。

# mysqldump 逻辑备份
mysqldump -u root -p --all-databases > backup.sql

# 导出为 CSV
mysql -u root -p -e "SELECT * INTO OUTFILE '/tmp/users.csv' FROM users"

常用工具

工具 类型 特点
mysqldump MySQL 官方 简单易用,适合中小库
mysqlpump MySQL 5.7+ 并行导出,比 mysqldump 快
mydumper 第三方 多线程备份恢复
SELECT INTO OUTFILE SQL 语句 导出 CSV 格式

逻辑备份的优缺点

优点
1. 可移植性好:SQL 文件可以在不同版本甚至不同数据库间迁移
2. 选择性恢复:可以只恢复单表、单库,甚至部分数据
3. 可读性好:SQL 文件是文本,可以 grep 查看或编辑
4. 空间紧凑:只存数据本身,不存索引和碎片
5. 细粒度控制:支持 --where 条件导部分数据

缺点
1. 速度慢:逐条 SELECT 和 INSERT,大数据库备份非常慢
2. 恢复慢:逐条执行 INSERT,重建索引需要额外时间
3. 占用网络带宽:默认通过 TCP 传输
4. 锁定问题:某些选项需要全局锁

对比总结

对比维度 物理备份 逻辑备份
备份速度 快(文件复制) 慢(SQL 解析)
恢复速度
文件体积 大(含索引) 较小(仅数据)
跨版本迁移 ❌ 受限 ✅ 适合
单表恢复 部分支持(xtrabackup) ✅ 容易
在线备份 ✅ 可以(xtrabackup) ✅ 可以(mysqldump)
数据一致性 ⭐ 优秀(LSN 点) ✅ 支持
选择性 ❌ 全库/全实例 ✅ 按库/表/条件

选择建议

什么时候用物理备份

  • 数据库 > 50GB
  • 要求更短的恢复时间(RTO)
  • 需要增量备份和 PITR
  • MySQL 版本固定,不需要跨版本迁移

什么时候用逻辑备份

  • 数据库 < 50GB
  • 需要跨版本或跨平台迁移
  • 需要细粒度恢复(单表、按条件)
  • 做数据审计(检查备份内容)
  • 作为辅助备份(物理备份之外的额外保护)

典型备份策略组合

实际生产环境中,推荐两者结合:

# 每日:物理备份(全量 + 增量)
00:00  xtrabackup --backup --target-dir=/backup/base  # 周日全量
00:00  xtrabackup --backup --target-dir=/backup/inc1 --incremental-basedir=/backup/base  # 周一增量
...

# 每日:逻辑备份(小库或关键表)
00:30  mysqldump --single-transaction --routines --triggers mydb > /backup/mydb_daily.sql

# 每周:验证备份可恢复性
07:00  xtrabackup --prepare --target-dir=/backup/base

备份原理对比图

物理备份流程:
源数据文件 ──→ 直接复制 ──→ 备份文件(二进制)
恢复:备份文件 ──→ 复制回数据目录 ──→ MySQL 加载启动

逻辑备份流程:
源数据 → SELECT 逐行读取 → 生成 SQL 文件(文本)
恢复:SQL 文件 → 逐条执行 INSERT → 重建索引

面试常问题

Q:mysqldump 属于哪种备份?
A:逻辑备份。它通过 SQL 语句导出表结构和数据,生成可读的 SQL 文本文件。

Q:为什么 xtrabackup 号称热备?它不需要加锁吗?
A:xtrabackup 通过复制 InnoDB 数据文件 + 同步 Redo Log 来实现一致性,不需要对 InnoDB 表加锁。但在复制非 InnoDB 表时仍需要短暂的全局锁(FLUSH TABLES WITH READ LOCK)。

Q:物理备份能跨 MySQL 版本恢复吗?
A:有限制。小版本升级(5.7.30 → 5.7.35)通常可以。大版本升级(5.7 → 8.0)的文件格式变了,不能直接物理恢复,需要用逻辑备份或 MySQL 的升级工具。

Q:线上业务数据库应该优先用哪种备份?
A:优先物理备份(xtrabackup),因为速度快、支持增量、可用性好。同时配合逻辑备份作为补充,用于细粒度恢复场景。备份策略的核心是”3-2-1 原则”:3 份副本,2 种不同介质,1 份异地。


Redis 备份对 CPU 和内存的影响

Redis 备份对 CPU 和内存的影响

概述

Redis 在进行持久化和备份操作时(特别是 BGSAVE 和 AOF 重写),会对服务器资源产生显著影响。理解这些影响对于生产环境的容量规划和运维至关重要。

备份对 CPU 的影响

BGSAVE 的 CPU 消耗

BGSAVE 执行时的 CPU 使用:
┌────────────────────────────────────┐
│ 父进程:正常处理请求(单核)        │
│ 子进程:写 RDB 文件(单核 100%)    │
│ 合计:约 2 个核的 CPU 使用          │
└────────────────────────────────────┘

影响量级

数据量 RDB 大小 BGSAVE 耗时 CPU 消耗
10GB ~5GB ~30秒 1核 100%
50GB ~30GB ~3分钟 1核 100%
100GB ~60GB ~10分钟 1核 100%

实际观察

# 在 BGSAVE 期间的 CPU 监控
top -p $(pgrep -f redis-server)
# PID USER  PR NI VIRT RES SHR S %CPU %MEM
# 1234 redis 20 0 12.5G 9.8G 3.2G R  **198%**  9.8
#   ↑ 父进程 + 子进程合计约 200%

AOF 重写的 CPU 消耗

# AOF 重写期间
# - 父进程:正常处理请求
# - 子进程:重写 AOF(写临时文件)
# - 中间进程:记录重写期间的增量命令

# 对 CPU 的影响与 BGSAVE 类似
# 但 AOF 重写可能更消耗 CPU(需要解析和重新编码命令)

备份对内存的影响

RDB BGSAVE 的内存消耗

1. fork 内存

fork 时内存消耗:
父进程内存:10GB
fork 操作:复制页表(约 4KB/页 × 页表项)
          10GB / 4KB = 2,621,440 页
          页表大小 ≈ 2,621,440 × 8 = 20MB

fork 本身不复制数据,只复制页表

2. COW(Copy on Write)内存

# COW 是主要的内存消耗
# 父进程在子进程运行期间修改数据时,需要复制该页

COW 内存  写入速度 × BGSAVE 持续时间

# 示例
# 场景:10GB Redis,BGSAVE 耗时 30 秒
# 写入频率:每秒 10000 次 SET,每次修改约 100 字节
# 写入数据量:30 秒 × 10000 × 100 = 30MB
# COW 内存:约 30MB ~ 60MB(取决于内存页分布)

影响 COW 内存的因素

因素 影响 说明
写入频率 正比 写入越多,COW 越多
写入大小 正比 修改的数据越大,COW 越大
BGSAVE 耗时 正比 耗时越长,COW 越多
内存页大小 4KB 修改 1 字节也要复制整个 4KB 页
数据结构 间接 某些结构修改更频繁

COW 内存监控

# 查看上次 BGSAVE 的 COW 大小
redis-cli INFO Persistence | grep rdb_last_cow_size
# rdb_last_cow_size:52428800  → 约 50MB

# 查看当前 BGSAVE 的 COW 大小
redis-cli INFO Persistence | grep rdb_current_cow_size
# rdb_current_cow_size:10485760  → 约 10MB

# AOF 重写的 COW
redis-cli INFO Persistence | grep aof_last_cow_size

内存消耗估算

# 估算生产环境 BGSAVE 的峰值内存

# 场景参数
used_memory = 10GB          # 数据占用
COW_rate = 20%              # 估计的 COW 内存比例(高峰期)
fork_overhead = 20MB        # 页表

# 峰值内存
peak_memory = used_memory + COW_size + fork_overhead
peak_memory = 10GB + 2GB + 20MB  12GB

# 给系统留余量
recommended_system_memory = peak_memory + 20% Buffer
                         = 12GB + 2.4GB  15GB

性能影响的实际案例

案例 1:高写入场景

场景:电商订单系统,Redis 32GB
写入频率:每秒 50000 次 SET

BGSAVE 影响:
- 耗时:约 5 分钟
- COW 内存:高达 8-12GB(因为写入密集)
- 慢查询:部分请求延迟从 1ms 上升到 10ms+
  (fork 期间 + COW 缺页中断)

优化:
- 考虑使用从库备份
- 增大 vm.max_map_count
- 使用 SSD 加速 RDB 写入

案例 2:低写入场景

场景:用户缓存,Redis 50GB
写入频率:每秒 1000 次修改

BGSAVE 影响:
- 耗时:约 8 分钟
- COW 内存:约 2-3GB
- 对服务几乎无影响

主要问题:
- fork 耗时约 500ms
- 这 500ms 内请求延迟上升

减少备份影响的策略

1. 从库备份(推荐)

# 最佳实践:不要在繁忙的主库上做 BGSAVE
# 在从库上做备份

# 从库上执行
redis-cli -p 6380 BGSAVE
# 主库不受任何影响

2. 调整备份时间窗口

# 在业务低峰期执行备份
crontab -e
# 凌晨 3 点执行(低写入量)
0 3 * * * redis-cli BGSAVE

3. 系统参数优化

# sysctl 优化
echo never > /sys/kernel/mm/transparent_hugepage/enabled
sysctl vm.overcommit_memory=1
sysctl vm.swappiness=0

# 增大进程数限制
ulimit -n 65536

4. 硬件优化

# 使用 NVMe SSD
- 明显减少 RDB 写入时间
- 降低 COW 内存(因为备份更快)

# 增加 CPU 核心
- 备份子进程可以独享一个核心

# 足够的内存余量
- 至少预留 20-30% 的内存给 COW

监控与告警

# 内存和 CPU 监控脚本
import redis
import psutil

def monitor_backup_impact():
    r = redis.Redis()
    info = r.info('persistence')

    # 检查是否在进行备份
    bgsave_in_progress = info['rdb_bgsave_in_progress']
    rewrit_in_progress = info['aof_rewrite_in_progress']

    if bgsave_in_progress or rewrit_in_progress:
        # 获取系统资源
        cpu_percent = psutil.cpu_percent(interval=1)
        memory = psutil.virtual_memory()

        print(f"备份进行中...")
        print(f"系统 CPU: {cpu_percent}%")
        print(f"系统内存: {memory.percent}% ({memory.used/1024**3:.2f}GB)")

        # Redis 统计
        cow_size = info.get('rdb_current_cow_size', 0) or info.get('aof_current_rewrite_time_sec', 0)
        print(f"Redis COW: {cow_size/1024**3:.2f}GB")

        # 告警阈值
        if memory.percent > 80:
            print("ALERT: 系统内存超过 80%!")
        if cpu_percent > 90:
            print("ALERT: CPU 使用率超过 90%!")

    else:
        print("当前没有备份任务")

面试要点

  • CPU 影响:BGSAVE/AOF 重写期间额外消耗 1 个 CPU 核心
  • 内存影响:主要来自 COW,与写入量成正比
  • fork 短暂卡顿:大实例 fork 耗时可能导致几秒的请求延迟上升
  • COW 监控:用 INFO Persistence 查看 rdb_last_cow_size
  • 从库备份:消除对业务服务器的所有影响
  • 高峰期避开:在写入量低的时间段执行备份
  • 预留内存:至少预留 20-30% 内存余量给 COW

Redis 跨机房备份方案

Redis 跨机房备份方案

为什么需要跨机房备份

跨机房备份是 Redis 高可用体系的最后一道防线,应对以下灾难场景:

  • 机房级故障:整机房断电、网络中断、火灾
  • 区域性灾难:地震、洪水导致区域通信不可用
  • 运维事故:误操作波及整个机房

跨机房备份的核心挑战

挑战 1:网络延迟
  同城机房 = 1-3ms   异城 = 10-50ms  跨大洲 = 100-300ms

挑战 2:数据一致性
  两地副本之间存在延迟,无法保证强一致

挑战 3:带宽成本
  跨地域带宽昂贵,大量数据传输受限

挑战 4:写入冲突
  多机房写操作可能导致数据冲突

方案一:主从跨机房复制

架构

主机房(生产中心)
  └── Redis 主库
      └── 从库 1(同机房,热备份)
      └── 从库 2(同机房,备份)
      └── 从库 3(异地,跨机房备份)
                         ↓ 跨地域网络
            备份机房
              └── Redis 从库

配置

# 异地从库 redis.conf
replicaof master-host-in-primary-idc 6379
replica-serve-stale-data no       # 复制断开时不提供过时数据
replica-read-only yes             # 只读
repl-backlog-size 512mb           # 增大复制积压缓冲区
repl-timeout 120                  # 跨机房需要更长的超时时间

跨机房复制的优化

# 1. 增大 TCP 缓冲区
sysctl -w net.core.rmem_max=134217728
sysctl -w net.core.wmem_max=134217728

# 2. 使用专用线路 / VPN
# 3. 启用压缩传输
# 目前 Redis 主从同步不支持实时压缩

# 4. 调整复制超时
redis-cli CONFIG SET repl-timeout 180

# 5. 监控复制延迟
redis-cli INFO replication | grep master_repl_offset
redis-cli INFO replication | grep slave_repl_offset

方案二:异步双机房写入

架构

机房 A                      机房 B
Redis 主库 A ──→ 异步 ──→ Redis 主库 B
     ↑                          ↑
  应用写入                  应用写入(紧急切换)

注意风险

# 这种方案存在数据冲突风险
# 例如:
# 机房 A: SET counter 100
# 机房 B: SET counter 200
# 同步后:counter 值不确定(后写入者胜出)
# 适用于:只追加不覆盖的场景(如日志)

方案三:备份文件异地同步

这是最经济且安全的方案。

完整流程

#!/bin/bash
# /usr/local/bin/redis_cross_idc_backup.sh

PRIMARY_BACKUP_DIR="/data/backup/redis"
REMOTE_HOST="backup.idc2.example.com"
REMOTE_PATH="/backup/redis/primary"
S3_BUCKET="s3://redis-backup-bucket"

# 第一步:本地备份(从从库)
redis-cli -p 6380 BGSAVE
sleep 60  # 等待完成

# 第二步:同步到异地机房
echo "=== 开始异地机房同步 ==="
rsync -avz --bwlimit=50000 \        # 限速 50Mbps
    ${PRIMARY_BACKUP_DIR}/daily/ \
    ${REMOTE_HOST}:${REMOTE_PATH}/

# 第三步:同步到对象存储(云端备份)
echo "=== 开始云存储同步 ==="
aws s3 sync ${PRIMARY_BACKUP_DIR}/daily/ ${S3_BUCKET}/daily/ \
    --storage-class STANDARD_IA \
    --no-progress

# 第四步:异地保留策略
ssh ${REMOTE_HOST} "find ${REMOTE_PATH} -name '*.rdb.gz' -mtime +14 -delete"

echo "=== 跨机房备份完成 ==="

限速传输

# 使用 rsync 限速,避免占满跨机房带宽
rsync -avz --bwlimit=100000  /backup/ remote:/backup/  # 限速 100Mbps

# 使用 scp 限速
scp -l 100000 /backup/dump.rdb remote:/backup/  # 限速 100Kbps

方案四:一主三从跨地域方案

完整部署

上海机房(主)
  ├── 上海从库(同城,主从复制)
  ├── 北京从库(异地,跨专线)
  └── 美国从库(海外,跨境加速)

监控指标

# 监控跨机房复制延迟
import redis
import time

def monitor_cross_idc_replication():
    local = redis.Redis(host='local-slave', port=6379)
    beijing = redis.Redis(host='beijing-slave', port=6379)
    us = redis.Redis(host='us-slave', port=6379)

    def get_lag(conn, name):
        info = conn.info('replication')
        master_offset = info['master_repl_offset']
        slave_offset = info.get('slave_repl_offset', 0)
        lag = master_offset - slave_offset
        return lag

    while True:
        beijing_lag = get_lag(beijing, '北京')
        us_lag = get_lag(us, '美国')

        print(f"北京时间: {time.ctime()}")
        print(f"  北京从库延迟: {beijing_lag} bytes")
        print(f"  美国从库延迟: {us_lag} bytes")

        # 超过 100MB 延迟时告警
        if beijing_lag > 100 * 1024 * 1024:
            alert("北京跨机房复制延迟过高")

        time.sleep(60)

跨机房恢复流程

主机房故障时的切换

# 场景:上海主库机房整体不可用

# Step 1: 确认主库不可用
redis-cli -h beijing-slave PING
# 没问题,但从库只能读

# Step 2: 将最"新"的从库提升为主库
redis-cli -h beijing-slave SLAVEOF NO ONE

# Step 3: 检查数据完整性
redis-cli -h beijing-slave DBSIZE
redis-cli -h beijing-slave INFO keyspace

# Step 4: 更新 DNS 或配置,将应用指向新主库
# 更新 sentinel 或 DNS 记录

# Step 5: 其他从库重新指向新主库
redis-cli -h us-slave SLAVEOF beijing-slave 6379

跨机房数据差异恢复

# 主库恢复后,将恢复期间丢失的数据同步过来

# 差异检查
redis-cli -h beijing-slave KEYS '*' > /tmp/beijing_keys.txt
redis-cli -h shanghai-original KEYS '*' > /tmp/shanghai_keys.txt

# 找出差异
comm -13 <(sort /tmp/beijing_keys.txt) <(sort /tmp/shanghai_keys.txt) > /tmp/lost_keys.txt

方案选型对比

方案 数据实时性 复杂度 成本 数据丢失风险
跨机房主从复制 秒级 中高 低(复制延迟内)
定时文件同步 小时级 可能有小时级丢失
多主异步 准实时 存在冲突风险
对象存储备份 小时级 低(存储费) 同上

面试要点

  • 跨机房备份≠跨机房复制:复制是热备,备份是冷备
  • 网络延迟是最大挑战:跨地域复制延迟不可忽略
  • 异地恢复流程:SLAVEOF NO ONE → 验证 → 切换
  • 数据一致性取舍:跨机房永远做不到强一致
  • 专线推荐:跨机房复制强烈推荐使用专线
  • 定期演练:跨机房切换必须定期演练,避免纸上谈兵
  • 监控延迟:复制延迟、备份延迟都必须监控告警

Redis 持久化与备份大小管理

Redis 持久化与备份大小管理

理解持久化文件大小

Redis 的持久化文件(RDB 和 AOF)大小与内存数据量并不完全相等。了解它们之间的关系对磁盘规划至关重要。

RDB 文件大小

RDB 文件 ≈ 内存数据量 × 压缩率

不同数据类型的压缩效率:
字符串(小):        ~30-50%  压缩率(最易压缩)
字符串(大/二进制):  ~80-95%  压缩率(难以压缩)
哈希:               ~40-60%  压缩率
列表:               ~45-65%  压缩率
集合:               ~50-70%  压缩率
有序集合:           ~50-70%  压缩率

实际经验公式:
RDB 文件 ≈ 内存使用量 × 0.5 ~ 0.8

AOF 文件大小

AOF 文件 ≈ 写操作体积 × 操作次数

影响因素:
- 写命令本身的大小(包括 key 和 value)
- 命令协议格式的开销
- AOF 重写的频率

实际经验:
AOF 文件可以轻松达到数据量的 10-100 倍
AOF 重写后 ≈ RDB 文件大小(混合持久化)或 AOF 基础快照 + 增量

持久化文件大小对比

指标 RDB AOF(未重写) AOF(已重写) 混合 AOF
典型大小 数据量×0.5-0.8 数据量×10-100 数据量×1.5-2 数据量×0.6-1.0
节省方式 关闭压缩 重写频率 重写+ 默认最小
增长模式 快照式跳跃 线性增长 重写后缩小 重写后最小

磁盘空间规划

内存 → 磁盘空间映射

# 假设场景
Redis 内存使用:50GB
数据压缩率:约 60%

# RDB 文件大小预估
RDB  50GB × 0.6 = 30GB

# RDB 持久化期间的临时空间
tmp_file + final_rdb  30GB × 2 = 60GB(写时使用临时文件)

# AOF 文件大小预估(每 2 小时重写)
AOF_growth  每小时写入量 × 保留时长
            = 5GB/h × 2h = 10GB(重写间隔内)
AOF_total  RDB_base + AOF_increments

磁盘空间计算公式

最小磁盘空间 ≥ max(rdb_during_bgsave, aof_during_rewrite) × 2

其中:
rdb_during_bgsave = RDB 文件大小 × 2(临时文件 + 最终文件)
aof_during_rewrite = 当前 AOF 文件大小 + 重写期间的增量命令

实际推荐

# 推荐磁盘空间配置
# 仅 RDB 持久化:内存量 × 3
# 仅 AOF 持久化:内存量 × 4
# 混合持久化:内存量 × 5(安全起见)

# 监控磁盘使用
df -h /data/redis
# /dev/sdb1        200G  80G  120G  40% /data/redis

控制持久化文件大小的策略

1. 调整 RDB 压缩

# redis.conf
rdbcompression yes          # 开启压缩(节省磁盘)
rdbcompression no           # 关闭压缩(节省 CPU)

选择建议
– 磁盘不够 → 压缩
– CPU 是瓶颈 → 不压缩(BGSAVE 速度更快)

2. 调整 AOF 重写策略

# redis.conf
auto-aof-rewrite-percentage 100    # 增长 100% 时触发
auto-aof-rewrite-min-size 64mb     # 最小 64MB

# 更激进的重写策略(适合写入量大的场景)
auto-aof-rewrite-percentage 50     # 增长 50% 就重写
auto-aof-rewrite-min-size 1gb      # 最小 1GB 才重写

3. 查看持久化文件尺寸信息

# RDB 尺寸信息
redis-cli INFO Persistence | grep rdb
# rdb_changes_since_last_save:0
# rdb_last_bgsave_time_sec:30
# rdb_last_cow_size:41943040

# AOF 尺寸信息
redis-cli INFO Persistence | grep aof_current_size
# aof_current_size:1048576000  → 约 1GB
# aof_base_size:524288000      → 基础大小 512MB

# 通过文件系统查看
ls -lh /var/lib/redis/
# -rw-r--r-- 1 redis redis 1.0G May 18 12:00 appendonly.aof
# -rw-r--r-- 1 redis redis 512M May 18 12:00 temp-rewriteaof-bg-12345.aof
# -rw-r--r-- 1 redis redis 256M May 18 12:00 dump.rdb

持久化文件的性能影响

RDB 文件与性能

# RDB 大小影响 BGSAVE 耗时
10GB 内存  RDB ~5GB  BGSAVE ~30s
50GB 内存  RDB ~30GB  BGSAVE ~3min
100GB 内存  RDB ~60GB  BGSAVE ~10min

# 太大 RDB 的风险:
# 1. fork 耗时延长(页表复制)
# 2. COW 期间父进程写入越多,额外内存越多
# 3. 磁盘 I/O 压力

AOF 文件与性能

# AOF 文件大小对性能的影响较小
# 因为 AOF 是追加写,不是全量写

# 但 AOF 重写需要开临时文件:
temp-rewriteaof-bg-xxx.aof  # 文件大小 ~ RDB 大小

# 需要额外磁盘空间:
df -h /data/redis
# 确保有足够空间容纳 AOF 重写临时文件

备份大小管理实践

RDB 备份的压缩策略

# 备份时再次压缩(节省存储成本)
gzip /backup/redis/dump.rdb
# 原始: 10GB → 压缩后: 3-5GB

# 选择压缩级别(默认 6)
gzip -1 /backup/redis/dump.rdb     # 快速压缩,压缩率较低
gzip -9 /backup/redis/dump.rdb     # 高压缩率,耗时较长

增量备份 vs 全量备份

# 全量备份:每天一次(大但完整)
# 增量备份:每小时一次(小但需要全量才可用)

# 备份大小策略
# 方案 A:所有备份都是全量(简单但费空间)
00:00  dump.rdb  10GB
01:00  dump.rdb  10GB
... 每天 24 个全量 = 240GB

# 方案 B:每天一次全量 + 保留 N 天(推荐)
# 保留 7 天 = 7 × 10GB = 70GB
find /backup/redis/ -name "*.rdb" -mtime +7 -delete

压缩率对比

# 不同压缩方式的对比
# 原始 RDB: 10GB(已使用 Redis 的 LZF 压缩)

# 再次压缩:
gzip (默认):    ~3.5GB | 耗时 2min
bzip2 (默认):   ~2.8GB | 耗时 15min
xz (默认):      ~2.5GB | 耗时 30min
zstd (默认):    ~3.2GB | 耗时 1min

# 推荐:gzip 或 zstd(平衡压缩率和速度)

监控持久化文件大小

# 监控脚本
import redis
import os

def check_persistence_size(host='localhost', port=6379):
    r = redis.Redis(host=host, port=port)
    info = r.info('persistence')

    # 获取配置路径
    dir = r.config_get('dir')['dir']
    dbfilename = r.config_get('dbfilename')['dbfilename']
    aof_filename = r.config_get('appendfilename')['appendfilename']

    # 检查文件大小
    rdb_path = f"{dir}/{dbfilename}"
    aof_path = f"{dir}/{aof_filename}"

    rdb_size = os.path.getsize(rdb_path) if os.path.exists(rdb_path) else 0
    aof_size = os.path.getsize(aof_path) if os.path.exists(aof_path) else 0

    # 检查磁盘使用
    stat = os.statvfs(dir)
    disk_free = stat.f_frsize * stat.f_bavail

    print(f"RDB 文件大小: {rdb_size / 1024**3:.2f} GB")
    print(f"AOF 文件大小: {aof_size / 1024**3:.2f} GB")
    print(f"磁盘剩余: {disk_free / 1024**3:.2f} GB")

    # 告警
    if aof_size > 50 * 1024**3:
        print("ALERT: AOF 文件超过 50GB,建议触发重写")
    if rdb_size > 100 * 1024**3:
        print("ALERT: RDB 文件超过 100GB,fork 时间可能较长")

面试要点

  • RDB < 内存:压缩后通常比内存小(例外:二进制数据)
  • AOF > 内存:因为记录的是命令而非内存镜像
  • 重写是 AOF 瘦身的关键:定期重写控制 AOF 大小
  • 磁盘规划要打余量:至少 3-5 倍内存的磁盘空间
  • BGSAVE/AOF 重写需要临时空间:等于最终文件大小
  • 备份压缩:gzip/zstd 可以再节省 60-70% 空间
  • COW 内存 ≠ 持久化文件:COW 是备份期间的内存额外消耗

Redis 生产环境备份策略

Redis 生产环境备份策略

备份策略设计原则

一个完善的生产备份策略需要平衡三个核心因素:

数据安全性(恢复点目标 RPO)
    ↑
可靠性 ────────────── 成本
(恢复时间目标 RTO)

关键指标定义

指标 定义 举例
RPO 最多能容忍丢失多长时间的数据 RPO=1小时,最多丢失1小时数据
RTO 从故障到恢复服务的最大时间 RTO=10分钟,10分钟内必须恢复

推荐策略:分层备份体系

第一层:实时持久化(防止进程崩溃)

# redis.conf
# 开启混合持久化(Redis 4.0+ 推荐)
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
save 900 1
save 300 10
save 60 10000

作用:防止 Redis 进程崩溃导致数据丢失
RPO:1 秒(AOF everysec)

第二层:定时快照(防止介质故障)

# 每小时从从库生成 RDB 快照
crontab -e
0 * * * * /usr/local/bin/redis_backup_from_slave.sh

作用:AOF 损坏时有备份可用
RPO:1 小时

第三层:异地备份(防止机房级灾难)

# 每天将备份同步到异地
crontab -e
0 3 * * * /usr/local/bin/redis_remote_backup.sh

作用:机房故障不影响备份
RPO:24 小时

完整备份脚本

本地备份脚本

#!/bin/bash
# /usr/local/bin/redis_backup.sh

REDIS_CLI="redis-cli"
SLAVE_PORT="6379"
BACKUP_ROOT="/data/backup/redis"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7

# 检查从库状态
SLAVE_STATUS=$($REDIS_CLI -p $SLAVE_PORT INFO replication | grep master_link_status | cut -d: -f2)
if [ "$SLAVE_STATUS" != "up" ]; then
    echo "[ERROR] 从库连接异常,备份中止"
    exit 1
fi

# 第一步:在从库上执行 BGSAVE
$REDIS_CLI -p $SLAVE_PORT BGSAVE
if [ $? -ne 0 ]; then
    echo "[ERROR] BGSAVE 触发失败"
    exit 1
fi

# 第二步:等待 BGSAVE 完成
TIMEOUT=3600  # 1小时超时
ELAPSED=0
while [ $ELAPSED -lt $TIMEOUT ]; do
    STATUS=$($REDIS_CLI -p $SLAVE_PORT INFO Persistence | grep rdb_bgsave_in_progress | cut -d: -f2)
    if [ "$STATUS" = "0" ]; then
        echo "[OK] BGSAVE 完成,耗时 ${ELAPSED}秒"
        break
    fi
    sleep 5
    ELAPSED=$((ELAPSED + 5))
done

# 第三步:获取 RDB 路径
DIR=$($REDIS_CLI -p $SLAVE_PORT CONFIG GET dir | tail -1)
FILENAME=$($REDIS_CLI -p $SLAVE_PORT CONFIG GET dbfilename | tail -1)
RDB_PATH="${DIR}/${FILENAME}"

# 第四步:备份并压缩
mkdir -p "${BACKUP_ROOT}/daily"
cp "$RDB_PATH" "${BACKUP_ROOT}/daily/redis_${DATE}.rdb"
gzip "${BACKUP_ROOT}/daily/redis_${DATE}.rdb"
echo "[OK] 备份文件: redis_${DATE}.rdb.gz"

# 第五步:清理旧备份
find "${BACKUP_ROOT}/daily" -name "*.rdb.gz" -mtime +$RETENTION_DAYS -delete
echo "[OK] 清理 ${RETENTION_DAYS} 天前的备份"

# 第六步:记录备份元数据
echo "{\"date\":\"$DATE\",\"status\":\"ok\",\"size_rdb\":$(stat -c%s "$RDB_PATH" 2>/dev/null || echo 0),\"duration_sec\":$ELAPSED}" >> "${BACKUP_ROOT}/backup_history.jsonl"

echo "[OK] 备份流程全部完成"

异地备份脚本

#!/bin/bash
# /usr/local/bin/redis_remote_backup.sh

REMOTE_HOST="backup-server.example.com"
REMOTE_PATH="/backup/redis"
LOCAL_BACKUP="/data/backup/redis/daily"

# 同步本地备份到异地
rsync -avz --progress --timeout=60 \
    ${LOCAL_BACKUP}/ \
    ${REMOTE_HOST}:${REMOTE_PATH}/daliy/

# 保持异地备份 30 天
ssh ${REMOTE_HOST} "find ${REMOTE_PATH}/daily -name '*.rdb.gz' -mtime +30 -delete"

echo "[OK] 异地备份同步完成"

不同规模 Redis 的备份策略

小型(< 5GB)

# 直接在主库 BGSAVE,简单直接
# 每 6 小时备份一次
0 */6 * * * redis-cli BGSAVE && sleep 5 && cp /var/lib/redis/dump.rdb /backup/redis_$(date +\%Y\%m\%d\%H).rdb

中型(5GB – 50GB)

# 从库备份,每小时一次
# 保留 7 天
0 * * * * /usr/local/bin/redis_backup.sh

# 每天同步到异地
0 4 * * * /usr/local/bin/redis_remote_backup.sh

大型(> 50GB)

# 从库备份,注意 fork 耗时
# 考虑使用 Redis-Shake 进行增量同步作为补充备份
# 考虑增大 vm.overcommit_memory=1

sysctl -w vm.overcommit_memory=1
sysctl -w vm.max_map_count=262144

备份验证策略

备份的价值在于可恢复,必须定期验证:

#!/bin/bash
# /usr/local/bin/verify_backup.sh

# 随机选取一个备份文件
LATEST_BACKUP=$(ls -t /data/backup/redis/daily/*.rdb.gz | head -1)
if [ -z "$LATEST_BACKUP" ]; then
    echo "[FAIL] 没有备份文件"
    exit 1
fi

# 解压
gunzip -k -f "$LATEST_BACKUP" -c > /tmp/verify.rdb

# 使用临时 Redis 实例恢复
redis-server --port 6380 --dir /tmp --dbfilename verify.rdb \
    --daemonize yes --loglevel warning

# 等待启动
sleep 2

# 检查数据
SIZE=$(redis-cli -p 6380 DBSIZE 2>/dev/null)
if [ "$SIZE" -gt 0 ]; then
    echo "[OK] 备份验证通过,共 $SIZE 个 key"
    redis-cli -p 6380 INFO Keyspace
else
    echo "[FAIL] 备份恢复后无数据"
fi

# 清理
redis-cli -p 6380 SHUTDOWN NOSAVE 2>/dev/null
rm -f /tmp/verify.rdb

监控与告警

# Prometheus 告警规则示例
groups:
- name: redis_backup
  rules:
  - alert: RedisBackupStale
    expr: time() - redis_rdb_last_save_timestamp_seconds > 7200
    for: 5m
    annotations:
      summary: "Redis 超过 2 小时未备份"

  - alert: RedisBackupFailed
    expr: redis_rdb_last_bgsave_status{status="ok"} == 0
    annotations:
      summary: "Redis BGSAVE 失败"

  - alert: RedisBackupCOWTooHigh
    expr: redis_rdb_last_cow_size / redis_memory_used_bytes > 0.2
    annotations:
      summary: "COW 内存超过 20%,备份期间写入量过大"

灾难恢复演练

定期(每季度)进行灾难恢复演练:

场景 1:单节点宕机
  验证:从库提升为主库,确认数据完整

场景 2:全部丢失
  验证:从备份文件恢复到新实例

场景 3:AOF 损坏
  验证:用 RDB 备份 + AOF 日志恢复

面试要点

  • 三层备份:实时 AOF + 定时 RDB + 异地备份
  • RPO vs RTO:备份策略围绕这两个指标设计
  • 从库备份:生产标准做法,主库零影响
  • 备份验证:不验证的备份就是没备份
  • 定期演练:备而不用等于没备
  • 监控告警:备份失败必须有告警
  • 保留策略:根据数据重要性决定保留时长

Redis 在线备份:如何不影响服务

Redis 在线备份:如何不影响服务

为什么在线备份需要特别设计

Redis 是内存数据库,在线备份意味着在不清空不重启的情况下获取一致性的数据快照。核心挑战在于:

  • Redis 是单线程模型
  • 在线备份需要读数据,但读数据会占用 CPU
  • 数据一致性:备份期间的数据变更如何处理

在线备份方案

方案一:BGSAVE(推荐)

# 触发异步 RDB 备份
redis-cli BGSAVE
# 返回: Background saving started

工作原理:

BGSAVE 触发
    ↓
Redis 父进程 fork() 出子进程
    ↓
子进程继承父进程的内存快照
    ↓
子进程将快照写入 RDB 文件
    ↓
父进程继续处理请求(COW 机制)
    ↓
子进程完成 → 通知父进程

关键点:fork + COW
fork():创建子进程,复制页表(而非数据本身)
COW(Copy-on-Write):父子进程共享物理内存
– 父进程修改某页内存 → 复制该页 → 修改副本
– 子进程看到的还是原来的数据
对服务的影响:fork 耗时 + COW 内存增长

监控 BGSAVE 的状态

# 检查 BGSAVE 是否正在进行
redis-cli INFO Persistence | grep rdb_bgsave_in_progress
# rdb_bgsave_in_progress:0  → 没有进行中

# 查看上次 BGSAVE 状态
redis-cli INFO Persistence | grep rdb_last_bgsave_*
# rdb_last_bgsave_status:ok
# rdb_last_bgsave_time_sec:5

# 查看 COW 内存
redis-cli INFO Persistence | grep rdb_last_cow_size
# rdb_last_cow_size:50331648  → COW 约 48MB

方案二:主从复制 + 从库备份

这是生产环境最推荐的方案,对主库零影响。

主库(负责读写)
  └── 从库1(只读)
      └── 从库1 执行 BGSAVE
          └── 备份文件复制到异地

配置步骤:

# Step 1: 在从库上执行 BGSAVE
redis-cli -p 6380 BGSAVE  # 从库端口 6380

# Step 2: 确保从库数据与主库一致
redis-cli -p 6380 INFO replication
# master_link_status:up
# master_last_io_seconds_ago:0

# Step 3: 从从库复制 RDB 文件
scp /var/lib/redis/dump.rdb backup-server:/backup/redis/

# Step 4: 可选 - 短暂暂停从库复制,获得更一致的快照
redis-cli -p 6380 SLAVEOF NO ONE   # 断开复制
# 此时从库数据冻结在断开的时刻
redis-cli -p 6380 BGSAVE            # 备份
cp dump.rdb /backup/                # 复制
redis-cli -p 6380 SLAVEOF master-ip 6379  # 重新建立复制

方案三:Redis-Shake 在线同步

# 使用 Redis-Shake 从源同步到备份实例
# 对源库几乎没有影响
redisshake -type sync \
           -source redis://source-host:6379 \
           -target redis://backup-host:6379

BGSAVE 对服务的影响分析

1. fork 耗时

fork 时间与内存大小相关:

内存大小 fork 耗时 服务影响
1GB ~10ms 几乎无感
10GB ~100ms 轻微延迟
50GB ~1s 可见卡顿
100GB+ ~数秒 明显影响

2. COW 内存增长

COW 内存 ≈ 备份期间写入数据量 × 2

假设:
- 1 小时内写入 100MB 数据
- COW 额外内存 ≈ 几十 MB
- fork 时总内存 ≈ 10GB + 几十 MB

3. 子进程 CPU 占用

# 查看子进程
ps aux | grep redis
# redis    12345  cpu占用  表示子进程正在写入 RDB

子进程在写 RDB 期间会占用一个 CPU 核心 100%。

最优备份策略:分层备份

备份层级                       | 频率      | 方式         | 对服务影响
───────────────────────────────┼───────────┼──────────────┼──────────
L1 - 主库持久化(AOF)         | 实时       | everysec    | 几乎无
L2 - 从库全量 RDB              | 每小时     | BGSAVE      | 从库影响
L3 - 异地备份                  | 每天       | scp/rsync   | 无

定时备份脚本示例

#!/bin/bash
# 每小时从从库备份一次

SLAVE_PORT=6380
BACKUP_DIR="/backup/redis"
DATE=$(date +%Y%m%d_%H%M%S)

# 在从库触发 BGSAVE
redis-cli -p $SLAVE_PORT BGSAVE

# 等待完成
while [ "$(redis-cli -p $SLAVE_PORT INFO Persistence | grep rdb_bgsave_in_progress | cut -d: -f2)" = "1" ]; do
    sleep 1
done

# 获取 RDB 文件路径
RDB_PATH=$(redis-cli -p $SLAVE_PORT CONFIG GET dir | tail -1)/dump.rdb

# 复制并压缩
cp "$RDB_PATH" "$BACKUP_DIR/online_backup_$DATE.rdb"
gzip "$BACKUP_DIR/online_backup_$DATE.rdb"

# 保留最近 24 小时的备份
find "$BACKUP_DIR" -name "*.rdb.gz" -mtime +1 -delete

# 记录日志
echo "$(date) - 备份完成,文件: online_backup_$DATE.rdb.gz" >> /var/log/redis-backup.log

监控指标

# 关键监控项
1. rdb_bgsave_in_progress     # 是否正在备份
2. rdb_last_bgsave_time_sec   # 上次备份耗时
3. rdb_last_cow_size          # COW 内存大小
4. rdb_current_bgsave_time_sec # 当前备份进行时间
5. latest_fork_usec           # fork 耗时

面试要点

  • BGSAVE ≠ SAVE:BGSAVE 异步,SAVE 同步阻塞
  • fork + COW 是不影响服务的关键机制
  • 从库备份是生产环境标准做法,对主库零影响
  • fork 耗时与内存大小正相关,大实例需注意
  • COW 内存:备份期间写入越多,额外内存越多
  • 监控指标:fork 耗时、COW 大小、备份状态
  • 实践建议:至少 2G 可用内存,避免在高峰期 BGSAVE

Redis 数据备份方式详解

Redis 数据备份方式详解

概述

Redis 提供了两种持久化机制:RDB(快照)和 AOF(追加文件),以及它们的组合方式。数据备份是数据安全保障的最后一道防线,理解备份方式对于生产环境至关重要。

备份方式总览

Redis 数据备份
├── RDB 快照备份
│   ├── SAVE(同步阻塞)
│   └── BGSAVE(异步后台)
├── AOF 日志备份
│   └── 记录所有写操作命令
└── 混合持久化(Redis 4.0+)
    └── RDB + AOF 日志增量

RDB 快照备份

工作原理

Redis 进程 → fork()
    ↓
子进程 → 将内存数据写入临时 RDB 文件
    ↓
写入完成 → 替换旧 RDB 文件
    ↓
父进程继续处理请求(通过 COW 机制)

配置方式

# redis.conf 配置
save 900 1        # 900 秒内至少 1 个 key 变化,触发保存
save 300 10       # 300 秒内至少 10 个 key 变化
save 60 10000     # 60 秒内至少 10000 个 key 变化

# 关闭 RDB
save ""

# RDB 文件配置
dbfilename dump.rdb
dir /var/lib/redis
rdbcompression yes    # 压缩
rdbchecksum  yes      # 校验和

RDB 备份的操作

# 手动触发 RDB 备份
redis-cli BGSAVE        # 异步(推荐)
redis-cli SAVE          # 同步(阻塞,不推荐生产环境使用)

# 查看 BGSAVE 状态
redis-cli LASTSAVE      # 最后一次成功保存的时间戳

# 查看 RDB 文件
ls -lh /var/lib/redis/dump.rdb

AOF 备份

工作原理

写操作命令 → Redis 协议格式 → AOF 缓冲区 → fsync → AOF 文件

配置方式

# redis.conf 配置
appendonly yes             # 开启 AOF
appendfilename "appendonly.aof"

# fsync 策略
appendfsync always         # 每次写入都 fsync(最安全,最慢)
appendfsync everysec       # 每秒 fsync(平衡,推荐)
appendfsync no             # 由操作系统决定 fsync(最快,最不安全)

# AOF 重写
auto-aof-rewrite-percentage 100   # 增长 100% 时触发重写
auto-aof-rewrite-min-size 64mb    # 至少 64MB 才触发重写

AOF 备份的操作

# 手动触发 AOF 重写
redis-cli BGREWRITEAOF

# 查看 AOF 状态
redis-cli INFO Persistence | grep aof

混合持久化(Redis 4.0+)

配置

aof-use-rdb-preamble yes  # 开启混合持久化

混合格式

[AOF 文件结构]
┌─────────────────────────┐
│ RDB 格式全量数据        │ ← 快速的加载起点
├─────────────────────────┤
│ AOF 格式增量命令        │ ← 精确的增量记录
│ *2\n$3\nSET\n...        │
│ *2\n$3\nSET\n...        │
└─────────────────────────┘

优点:RDB 的快速加载 + AOF 的数据安全性

备份策略对比

特性 RDB AOF 混合
文件大小 小(压缩) 大(可重写) 最小
恢复速度 最快
数据安全性 可能丢最后一次 1-2 秒数据 最优
内存占用 fork 时翻倍 fork 时翻倍
适用场景 冷备份、迁移 实时热备份 生产环境推荐

外置备份方案

1. 定时备份脚本

#!/bin/bash
# 每天凌晨 3 点备份 Redis RDB

BACKUP_DIR="/backup/redis"
DATE=$(date +%Y%m%d_%H%M%S)
REDIS_DATA="/var/lib/redis"

# 触发 RDB 快照
redis-cli BGSAVE

# 等待 BGSAVE 完成
while [ "$(redis-cli INFO Persistence | grep rdb_bgsave_in_progress | cut -d: -f2)" = "1" ]; do
    sleep 1
done

# 复制 RDB 文件到备份目录
cp $REDIS_DATA/dump.rdb $BACKUP_DIR/redis_backup_$DATE.rdb

# 压缩备份
gzip $BACKUP_DIR/redis_backup_$DATE.rdb

# 保留最近 7 天的备份
find $BACKUP_DIR -name "*.rdb.gz" -mtime +7 -delete

# 可选:同步到远程服务器
rsync -avz $BACKUP_DIR/ user@backup-server:/backup/redis/

2. RedisShake 备份同步

# 使用 RedisShake 做增量数据同步
redisshake -type sync -source redis://source-host:6379 -target redis://backup-host:6379

备份验证

备份后一定要验证可用性:

# 将备份文件启动到一个临时 Redis 实例
redis-server --port 6380 --dir /tmp/restore_test --dbfilename test.rdb

# 检查 key 数量和完整性
redis-cli -p 6380 DBSIZE
redis-cli -p 6380 INFO Keyspace

# 关闭测试实例
redis-cli -p 6380 SHUTDOWN

备份的最佳实践

  1. 定期备份:至少每日一次 RDB 冷备份
  2. 异地备份:备份文件同步到不同的机房或云存储
  3. 备份验证:定期测试备份文件的可恢复性
  4. 多版本保留:保留最近 N 天的备份
  5. 备份加密:敏感数据备份需要加密存储
  6. 备份监控:监控备份失败和备份文件大小异常

面试要点

  • RDB vs AOF:完整备份 vs 实时日志,各有优劣
  • 混合持久化:Redis 4.0 的推荐方式,兼顾恢复速度和安全性
  • 备份 ≠ 持久化:持久化防止进程崩溃,备份防止灾难性故障
  • 备份验证:备份的价值在于能恢复,必须验证
  • BGSAVE 的 fork 代价:大实例 fork 可能卡顿,注意内存配置
© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容