etcd 备份方案(K8s 1.28 二进制集群 / arm64)
适用集群:三 master 二进制部署的 K8s 1.28(etcd 3.5.21,arm64),节点 IP 192.168.120.51/52/53
适用场景:测试环境。生产环境建议频率收紧为每 6 小时一次、备份结果接告警。
一、方案总览
| 项目 | 值 |
|---|---|
| 备份方式 | etcdctl snapshot save 快照(先落本地 → 校验 → 转储 NAS) |
| 执行节点 | host51 / host52 / host53 三台各自执行 |
| 频率 | 每台每 8 小时一次,三台错峰,全集群每天 9 个均匀分布的恢复点 |
| 保留策略 | 本地 1 天兜底,NAS 7 天 |
| NAS 路径 | 192.168.111.1:/volume1/k8s/etcd-backup,按节点分子目录 host51/52/53(各节点 hostname) |
| 本地挂载点 | /k8s-nas/backup/etcd |
| 最大数据丢失窗口 | 约 2 小时 40 分钟 |
三节点快照内容等价,恢复按时间点选择
etcdctl snapshot save 在任何一台 healthy 的 member 上执行,打出来的都是整个集群当前时刻的一致性数据快照(内部走 raft 拿到最新 committed 数据),不是该节点本地的落后副本。因此:
host51/、host52/、host53/三个目录里同时刻的快照内容完全一样;- 三台都做备份是为了冗余和错峰覆盖,不是每台存不同的数据;
- 恢复时选哪个备份的唯一标准是时间点,不是节点。
二、NAS 端准备(一次性)
在群晖管理界面(或任意已挂载 /volume1/k8s 的机器上)创建备份子目录:
# NFS v3 挂载共享内子目录时,子目录必须先存在,否则 mount 直接报错
mkdir -p /volume1/k8s/etcd-backup
三、各 master 挂载 NFS(三台都执行)
mkdir -p /k8s-nas/backup/etcd
# 挂载参数与 nfs-subdir-external-provisioner 的 mountOptions 保持一致
cat >> /etc/fstab << "EOF"
192.168.111.1:/volume1/k8s/etcd-backup /k8s-nas/backup/etcd nfs vers=3,nolock,tcp,noresvport,_netdev 0 0
EOF
mount -a
# 验证挂载和写权限(NAS 端如果开了 root squash 会在这一步暴露)
mountpoint /k8s-nas/backup/etcd
touch /k8s-nas/backup/etcd/.write-test && rm -f /k8s-nas/backup/etcd/.write-test && echo "写入正常"
四、备份脚本(三台都部署,内容完全相同)
创建 /root/etcd-backup/etcd-backup.sh(脚本已同步抽离到同目录 etcd-backup.sh,便于直接 scp 到三台部署;两边内容保持一致,改动需同步):
#!/bin/bash
# etcd 快照备份:本地落盘 -> 校验 -> 转储 NAS
# 快照从本机 member 读取,内容是整个集群的一致性副本
# 本地保留 1 天兜底(NAS 故障时不至于断档),NAS 保留 7 天
# cron 环境 PATH 极简(默认仅 /usr/bin:/bin),必须显式补上 /usr/sbin /sbin,
# 否则 ip 命令找不到 → 取不到本机 IP → 被健康检查误判为"etcd 不健康"
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME_SHORT=$(hostname -s)
LOCAL_DIR="/data/backup/etcd-local"
NAS_MOUNT="/k8s-nas/backup/etcd"
NAS_DIR="${NAS_MOUNT}/${HOSTNAME_SHORT}" # 按节点分子目录,清理互不干扰
DATE=$(date +%Y%m%d-%H%M%S)
SNAPSHOT_FILE="${LOCAL_DIR}/etcd-${HOSTNAME_SHORT}-${DATE}.db"
LOG_FILE="${LOCAL_DIR}/backup.log"
# 取本机 IP 作为端点,脚本三台通用不用改;扩节点时记得改这里的正则
# grep 用 -oE(ERE)即可,无需 -oP(PCRE),兼容性更好
LOCAL_IP=$(ip -4 addr show | grep -oE '192\.168\.120\.5[123]' | head -1)
# 取不到 IP 必须直接退出,否则端点为空会让 etcdctl 报错、被下方健康检查
# 误判成"etcd 不健康",掩盖真正的根因(如 PATH 异常导致 ip 缺失)
[ -n "${LOCAL_IP}" ] || { echo "$(date '+%F %T') [ERROR] 未取到本机 IP,检查 ip 命令与 PATH" >> "${LOG_FILE}"; exit 1; }
ENDPOINT="https://${LOCAL_IP}:2379"
ETCD_CA="/etc/etcd/ssl/ca.pem"
ETCD_CERT="/etc/etcd/ssl/etcd.pem"
ETCD_KEY="/etc/etcd/ssl/etcd-key.pem"
mkdir -p "${LOCAL_DIR}" "${NAS_DIR}"
log() { echo "$(date '+%F %T') $1" >> "${LOG_FILE}"; }
# 备份前先确认本机 etcd 健康,不健康的 member 上打快照可能拿到落后数据
ETCDCTL_API=3 /usr/local/bin/etcdctl --endpoints="${ENDPOINT}" \
--cacert="${ETCD_CA}" --cert="${ETCD_CERT}" --key="${ETCD_KEY}" \
endpoint health >/dev/null 2>&1 || { log "[ERROR] etcd 不健康,跳过本次备份"; exit 1; }
# 快照到本地磁盘(不直接写 NFS,避免 NAS 抖动导致快照写一半失败)
ETCDCTL_API=3 /usr/local/bin/etcdctl --endpoints="${ENDPOINT}" \
--cacert="${ETCD_CA}" --cert="${ETCD_CERT}" --key="${ETCD_KEY}" \
snapshot save "${SNAPSHOT_FILE}" >> "${LOG_FILE}" 2>&1 \
|| { log "[ERROR] 快照失败"; rm -f "${SNAPSHOT_FILE}"; exit 1; }
log "[OK] 本地快照完成: ${SNAPSHOT_FILE} ($(du -h ${SNAPSHOT_FILE} | awk '{print $1}'))"
# 校验快照完整性(3.5 中 snapshot status 已移交 etcdutl,etcdctl 仍可用只是有弃用告警)
ETCDCTL_API=3 /usr/local/bin/etcdctl snapshot status "${SNAPSHOT_FILE}" -w table >> "${LOG_FILE}" 2>&1
# 转储 NAS:先确认挂载点在线再拷,避免写进本地空目录
# cp 套 timeout 兜底,防止 NAS 卡死(挂载在但 IO 阻塞)时脚本挂住
if mountpoint -q "${NAS_MOUNT}"; then
if timeout 300 cp "${SNAPSHOT_FILE}" "${NAS_DIR}/" && \
[ "$(md5sum < ${SNAPSHOT_FILE})" = "$(md5sum < ${NAS_DIR}/$(basename ${SNAPSHOT_FILE}))" ]; then
log "[OK] 已转储 NAS 并校验通过"
else
log "[ERROR] NAS 转储或校验失败,本次仅保留本地副本"
fi
else
log "[ERROR] NAS 未挂载,本次仅保留本地副本"
fi
# 清理:本地留 1 天,NAS 留 7 天(只清理本节点子目录,三台互不干扰)
find "${LOCAL_DIR}" -name "etcd-*.db" -mtime +1 -delete
mountpoint -q "${NAS_MOUNT}" && find "${NAS_DIR}" -name "etcd-*.db" -mtime +7 -delete
log "[OK] 过期备份清理完成"
chmod +x /root/etcd-backup/etcd-backup.sh
# 备份内容
[root@host51 etcd]# ls -l
总用量 0
drwxrwxrwx 1 root root 300 8月 6 08:15 host51
drwxrwxrwx 1 root root 300 8月 6 02:45 host52
drwxrwxrwx 1 root root 300 8月 6 05:25 host53
[root@host51 etcd]# tree
.
├── host51
│ ├── etcd-host51-20260804-142922.db
│ ├── etcd-host51-20260805-161253.db
│ ├── etcd-host51-20260805-164415.db
│ ├── etcd-host51-20260806-001501.db
│ └── etcd-host51-20260806-081501.db
├── host52
│ ├── etcd-host52-20260804-142851.db
│ ├── etcd-host52-20260805-161250.db
│ ├── etcd-host52-20260805-164406.db
│ ├── etcd-host52-20260805-184501.db
│ └── etcd-host52-20260806-024502.db
└── host53
├── etcd-host53-20260804-140832.db
├── etcd-host53-20260805-161107.db
├── etcd-host53-20260805-164320.db
├── etcd-host53-20260805-212501.db
└── etcd-host53-20260806-052501.db
3 directories, 15 files
# 定期执行日志内容:
{"level":"info","ts":"2026-08-06T00:15:01.513010+0800","caller":"snapshot/v3_snapshot.go:65","msg":"created temporary db file","path":"/data/backup/et
cd-local/etcd-host51-20260806-001501.db.part"}
{"level":"info","ts":"2026-08-06T00:15:01.523306+0800","logger":"client","caller":"v3@v3.5.21/maintenance.go:212","msg":"opened snapshot stream; downl
oading"}
{"level":"info","ts":"2026-08-06T00:15:01.523392+0800","caller":"snapshot/v3_snapshot.go:73","msg":"fetching snapshot","endpoint":"https://192.168.120
.51:2379"}
{"level":"info","ts":"2026-08-06T00:15:03.508914+0800","logger":"client","caller":"v3@v3.5.21/maintenance.go:220","msg":"completed snapshot read; clos
ing"}
{"level":"info","ts":"2026-08-06T00:15:03.566640+0800","caller":"snapshot/v3_snapshot.go:88","msg":"fetched snapshot","endpoint":"https://192.168.120.
51:2379","size":"88 MB","took":"2 seconds ago"}
{"level":"info","ts":"2026-08-06T00:15:03.566787+0800","caller":"snapshot/v3_snapshot.go:97","msg":"saved","path":"/data/backup/etcd-local/etcd-host51
-20260806-001501.db"}
Snapshot saved at /data/backup/etcd-local/etcd-host51-20260806-001501.db
2026-08-06 00:15:03 [OK] 本地快照完成: /data/backup/etcd-local/etcd-host51-20260806-001501.db (85M)
Deprecated: Use `etcdutl snapshot status` instead.
+----------+-----------+------------+------------+
| HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
+----------+-----------+------------+------------+
| 188b70c9 | 120023700 | 4037 | 88 MB |
+----------+-----------+------------+------------+
2026-08-06 00:15:04 [OK] 已转储 NAS 并校验通过
2026-08-06 00:15:04 [OK] 过期备份清理完成
{"level":"info","ts":"2026-08-06T08:15:01.441981+0800","caller":"snapshot/v3_snapshot.go:65","msg":"created temporary db file","path":"/data/backup/et
cd-local/etcd-host51-20260806-081501.db.part"}
{"level":"info","ts":"2026-08-06T08:15:01.452632+0800","logger":"client","caller":"v3@v3.5.21/maintenance.go:212","msg":"opened snapshot stream; downl
oading"}
{"level":"info","ts":"2026-08-06T08:15:01.452827+0800","caller":"snapshot/v3_snapshot.go:73","msg":"fetching snapshot","endpoint":"https://192.168.120
.51:2379"}
{"level":"info","ts":"2026-08-06T08:15:02.365189+0800","logger":"client","caller":"v3@v3.5.21/maintenance.go:220","msg":"completed snapshot read; clos
ing"}
{"level":"info","ts":"2026-08-06T08:15:02.505621+0800","caller":"snapshot/v3_snapshot.go:88","msg":"fetched snapshot","endpoint":"https://192.168.120.
51:2379","size":"88 MB","took":"1 second ago"}
{"level":"info","ts":"2026-08-06T08:15:02.505741+0800","caller":"snapshot/v3_snapshot.go:97","msg":"saved","path":"/data/backup/etcd-local/etcd-host51
-20260806-081501.db"}
Snapshot saved at /data/backup/etcd-local/etcd-host51-20260806-081501.db
2026-08-06 08:15:02 [OK] 本地快照完成: /data/backup/etcd-local/etcd-host51-20260806-081501.db (85M)
Deprecated: Use `etcdutl snapshot status` instead.
+----------+-----------+------------+------------+
| HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
+----------+-----------+------------+------------+
| a056d285 | 120147033 | 4020 | 88 MB |
+----------+-----------+------------+------------+
2026-08-06 08:15:03 [OK] 已转储 NAS 并校验通过
2026-08-06 08:15:03 [OK] 过期备份清理完成
五、三台 crontab 错峰
# master1(crontab -e 添加):00:15 / 08:15 / 16:15
15 0,8,16 * * * /root/etcd-backup/etcd-backup.sh
# master2:02:45 / 10:45 / 18:45
45 2,10,18 * * * /root/etcd-backup/etcd-backup.sh
# master3:05:25 / 13:25 / 21:25
25 5,13,21 * * * /root/etcd-backup/etcd-backup.sh
三台如果同一时刻执行,等于同一秒打 3 份一样的数据,覆盖密度没有提升。错开跑,全集群每 2~3 小时就有一个新恢复点。
六、一次性附加备份(快照的另一半)
快照恢复时必须用同一套 CA/证书,只有快照没有证书是恢复不了的。证书和配置不常变,手动备份一次到 NAS 即可:
# 在 master1 上执行一次
mkdir -p /k8s-nas/backup/etcd/etc-backup
tar czf /k8s-nas/backup/etcd/etc-backup/etcd-etc-$(date +%Y%m%d).tar.gz \
/etc/etcd/ /usr/lib/systemd/system/etcd.service
另外把 etcdutl 补到三台的 /usr/local/bin(恢复时要用,当时解压 etcd-v3.5.21 只拷了 etcd/etcdctl):
# 在留有安装包的机器上解压后分发
scp etcd-v3.5.21-linux-arm64/etcdutl root@host51:/usr/local/bin/
scp etcd-v3.5.21-linux-arm64/etcdutl root@host52:/usr/local/bin/
scp etcd-v3.5.21-linux-arm64/etcdutl root@host53:/usr/local/bin/
七、部署后验证清单
# 1. 三台各手动跑一次
/root/etcd-backup/etcd-backup.sh
# 2. 确认 NAS 上出现三个节点子目录且有快照
ls -lhR /k8s-nas/backup/etcd/
# 3. 查看备份日志无 ERROR
grep ERROR /data/backup/etcd-local/backup.log || echo "无异常"
# 4. 随机抽一个快照做完整性校验
ETCDCTL_API=3 etcdctl snapshot status /k8s-nas/backup/etcd/host51/etcd-*.db -w table
全部通过后再上 crontab。
八、恢复流程(灾难时使用,建议先在测试环境演练)
先判断场景再动手:大多数 etcd 故障是”个别节点坏了”,此时用快照反而会把整个集群回退、丢掉快照之后的写入。只有”演练 / 全集群数据损坏 / 需要回退到历史点”才走全量恢复。
| 场景 | 处置 |
|---|---|
| 演练 / 整个集群数据损坏或需回退到快照点 | 全量恢复:8.1 ~ 8.2 |
| 只坏一台,集群仍有 quorum(能正常服务) | 单点修复:8.3(不丢数据) |
| 坏两台剩一台健康(已丢 quorum) | 优先保留健康节点,按 8.3 重新加入;健康节点数据也坏时才全量恢复 |
8.1 场景一:全量恢复(演练 / 集群回退),选择要恢复的快照
三节点快照内容等价,按时间点选择:
# 看各目录里的快照时间点
ls -l --time-style='+%F %T' /k8s-nas/backup/etcd/*/
- 想恢复到故障发生前的最近时刻 → 取时间戳最大且早于故障时间的那一份(绝不选故障后的快照,可能包含坏数据)
- 想回滚到更早的历史点 → 直接挑对应日期的文件
- 担心坏文件 → 先
etcdctl snapshot status校验,选 revision 最大且能正常读取的
8.2 场景一(续):全量恢复操作步骤(命令级)
以下命令基于本方案集群实际参数(etcd 3.5.21,节点 etcd1/2/3 = 192.168.120.51/52/53,数据目录
/var/lib/etcd/default.etcd)。恢复期间集群不可用(apiserver 连不上 etcd 会 503),选维护窗口操作,并提前确认快照之后的变更可以接受丢失。
前置检查(3 台)
ls -l /usr/local/bin/etcdutl # 未安装则先补装(见第六节 scp 命令)
演练标记(可选,用于验证恢复点)
# 快照时刻前已存在的资源,恢复后应仍在;快照后的新资源,恢复后应消失
kubectl create cm restore-test-before -n default --from-literal=ts="$(date '+%F %T')"
kubectl create cm restore-test-after -n default # 恢复后应 Not found
步骤 1:三台停 etcd(apiserver 一并停,更干净)
systemctl stop kube-apiserver etcd
systemctl is-active etcd # 三台都应输出 inactive
步骤 2:三台移走旧数据目录(.bak 即回滚凭证)
mv /var/lib/etcd/default.etcd /var/lib/etcd/default.etcd.bak
restore 要求目标数据目录不存在或为空,所以必须移走;
.bak保留现场,用于回滚。
步骤 3:选一份快照,校验后分发到三台
# 在任意有 NAS 挂载的节点执行,选哪份见 8.1(按时间点)
SNAP=/k8s-nas/backup/etcd/host53/etcd-host53-20260804-140832.db
etcdutl snapshot status "$SNAP" -w table # 先校验快照
scp "$SNAP" root@host51:/tmp/restore.db
scp "$SNAP" root@host52:/tmp/restore.db
cp "$SNAP" /tmp/restore.db # 本机直接拷贝
步骤 4:三台各自 restore(同一快照,仅 --name 和 --initial-advertise-peer-urls 不同)
host51(etcd1):
etcdutl snapshot restore /tmp/restore.db \
--name etcd1 \
--data-dir /var/lib/etcd/default.etcd \
--initial-cluster "etcd1=https://192.168.120.51:2380,etcd2=https://192.168.120.52:2380,etcd3=https://192.168.120.53:2380" \
--initial-cluster-token etcd-cluster \
--initial-advertise-peer-urls https://192.168.120.51:2380
host52(etcd2)把 --name etcd2、--initial-advertise-peer-urls https://192.168.120.52:2380;host53(etcd3)同理。
三台的
--initial-cluster与--initial-cluster-token必须完全一致,否则三台无法组成一个集群。restore 后是全新集群 ID、member ID 全变,属正常。restore 产物为 root 属主,与现有 systemd(默认 root 运行)一致,无需 chown。
步骤 5:三台启动 etcd,验证集群健康
systemctl start etcd
# 三台都 start 后,在任意一台执行:
ETCDCTL_API=3 etcdctl --endpoints=https://192.168.120.51:2379 \
--cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem \
endpoint health --cluster
ETCDCTL_API=3 etcdctl --endpoints=https://192.168.120.51:2379 \
--cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem \
member list
步骤 6:恢复 apiserver,验证集群与恢复点
systemctl start kube-apiserver # 三台
kubectl get node
kubectl get pod -A
kubectl get cm restore-test-before -n default # 演练时:应存在
kubectl get cm restore-test-after -n default # 演练时:应 Not found(数据已回到快照时刻)
回滚预案(恢复失败或需放弃演练)
systemctl stop etcd
mv /var/lib/etcd/default.etcd /var/lib/etcd/default.etcd.restored
mv /var/lib/etcd/default.etcd.bak /var/lib/etcd/default.etcd
systemctl start etcd # 三台,确认 endpoint health 全部 healthy
8.3 场景二:单点修复(只坏一台,集群还有 quorum)
场景:只有一台 etcd 数据目录损坏或节点故障,其余两台健康、集群仍在服务。此时不要用快照。快照是全集群回退,会丢掉快照之后的写入。正确做法是移除坏成员,让它以 existing 状态重新加入,数据从健康节点自动同步追平。
以 host53(etcd3)数据目录损坏为例:
# 1. 先在坏节点 host53 上停掉 etcd,避免它继续参与投票
systemctl stop etcd
# 2. 在健康节点 host51 上查看成员,找到 etcd3 的 member ID
ETCDCTL_API=3 etcdctl --endpoints=https://192.168.120.51:2379 \
--cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem \
member list
# 3. 在 host51 上移除坏成员(<etcd3-id> 用上一步查到的 ID)
ETCDCTL_API=3 etcdctl --endpoints=https://192.168.120.51:2379 \
--cacert=/etc/etcd/ssl/ca.pem --cert=/etc/etcd/ssl/etcd.pem --key=/etc/etcd/ssl/etcd-key.pem \
member remove <etcd3-id>
# 4. 在 host53 上清空数据目录,把 initial-cluster-state 改为 existing 后重新加入
rm -rf /var/lib/etcd/default.etcd
sed -i 's/ETCD_INITIAL_CLUSTER_STATE="new"/ETCD_INITIAL_CLUSTER_STATE="existing"/' /etc/etcd/etcd.conf
systemctl start etcd
重新加入后 etcd 自动从 leader 拉取全量数据追平,快照之后的写入不丢失。验证:
member list恢复 3 个成员、endpoint health --cluster全部 healthy。
8.4 场景三:坏两台剩一台健康(已丢 quorum)
- 若剩下那台(如 host51)数据完好:保留它当 leader,不要动它的数据目录。坏的两台按 8.3 流程先
member remove再以 existing 重新加入,数据从 host51 同步,不丢数据。 - 仅当健康节点的数据也受损、或确实需要回到更早的时间点时,才走 8.1 ~ 8.2 的全量恢复。
九、风险与未验证项
- NAS 写权限未实际验证,第三节的
touch测试通过才算走通;root squash 会在这一步暴露。 mountpoint -q判断不了 NAS 卡死(挂载在但 IO 阻塞),已用timeout 300兜底,超时当次备份失败但不会挂死 cron。- 脚本取本机 IP 的正则
192\.168\.120\.5[123]按当前三台 IP 写死,扩容 etcd 节点时要同步改。 - cron 环境坑(已修,重要):cron 默认 PATH 仅
/usr/bin:/bin,不含ip所在的/usr/sbin、/sbin,会导致ip: command not found→ 取不到本机 IP → 健康检查连空端点失败 → 日志误报”etcd 不健康”,手动执行却正常。脚本已用export PATH=...显式补全 + IP 空值校验兜底;自行精简脚本时这两处不可省。
十、Velero 与 etcdctl 的关系(备忘)
- 两者不冲突:etcdctl 快照保”集群本身”,Velero 保”集群里的内容(按 namespace 粒度 + 可选 PV 数据)”。
- 等后续有跨集群迁移、按 namespace 恢复需求时再评估 Velero(需要额外部署 MinIO 之类的 S3 兼容存储)。