系列导航:Kubernetes 重建迁移教程系列。所有数据库名、账号、节点和存储地址均为占位符。
有状态服务迁移最危险的误区,是以为 Pod 处于 Running 就说明数据已经迁移完成。实际上,一个全新的空数据库同样可以保持 Running,甚至健康检查也全部通过。
先定义每个服务的数据边界
建立清单:
| 服务 | 权威数据 | 可接受丢失量 RPO | 可中断时间 RTO | 恢复工具 | 验证负责人 |
|---|---|---|---|---|---|
| MySQL | 业务表、账号、binlog | <rpo> | <rto> | 物理/逻辑备份 | DBA |
| Redis | Session、缓存或业务数据 | <rpo> | <rto> | RDB/AOF | 应用团队 |
| Kafka | Topic、消息、offset | <rpo> | <rto> | 复制/同步工具 | 消息团队 |
| RabbitMQ | definitions、消息、用户 | <rpo> | <rto> | definitions + 数据方案 | 消息团队 |
| Zookeeper | znodes、ACL、事务日志 | <rpo> | <rto> | snapshot/log | 平台团队 |
缓存 Redis 和业务主数据 Redis 的迁移标准完全不同。没有 RPO/RTO,就无法确定该选择复制、停写后导出还是重新生成。
总体迁移顺序
flowchart LR
A["盘点数据和依赖"] --> B["恢复演练"]
B --> C["新集群零副本预部署"]
C --> D["全量备份与恢复"]
D --> E["停止写入"]
E --> F["增量追平"]
F --> G["服务端验证"]
G --> H["客户端切换"]
H --> I["观察与回滚窗口"]
不要到迁移当天才第一次尝试备份恢复。
第 1 步:盘点真实存储位置
kubectl get statefulset -A -o wide
kubectl get pvc,pv -A
kubectl get storageclass
kubectl get pod -A -o custom-columns='NS:.metadata.namespace,POD:.metadata.name,NODE:.spec.nodeName'
对每个 PVC 记录:
- StorageClass 和回收策略。
- 底层卷、NFS 目录或本地路径。
- Pod 与卷的 nodeAffinity。
- 文件所有者 UID/GID。
- 当前数据量和增长速度。
- 快照、备份和恢复耗时。
Kubernetes StatefulSet 提供稳定身份和持久卷关联,但它不会自动把旧集群数据复制到新集群。StatefulSets 也明确说明,缩容或删除 StatefulSet 默认不会删除关联卷,这是数据安全设计,不是完整备份。
第 2 步:先部署零副本对象
新集群预先创建:
- Namespace、Service、Headless Service。
- Secret 和 ConfigMap。
- StorageClass、PV、PVC。
- StatefulSet,但副本为 0。
- NetworkPolicy、ServiceMonitor 和告警。
kubectl apply --server-side --dry-run=server -f manifests/data-services/
kubectl apply -f manifests/data-services/
kubectl -n <namespace> scale statefulset/<service> --replicas=0
这样可以提前发现 API、存储、调度和 Secret 问题,又不会让空服务实例启动后接收连接。
第 3 步:MySQL 迁移
逻辑备份示例
mysqldump --single-transaction --routines --triggers --events \
--set-gtid-purged=OFF \
-h <old-mysql-host> -u <backup-user> -p \
--databases <database-list> \
> backup/mysql-full.sql
sha256sum backup/mysql-full.sql > backup/mysql-full.sql.sha256
对于大库,应评估物理备份、复制或专业迁移工具。MySQL 官方将逻辑/物理、全量/增量和基于 binlog 的时间点恢复视为不同的恢复策略,不能只保存一份 SQL 就宣称可恢复。MySQL Backup and Recovery
恢复到隔离实例并校验:
mysql -h <restore-host> -u <restore-user> -p < backup/mysql-full.sql
至少比较:
SELECT COUNT(*) FROM <critical-table>;
SHOW INDEX FROM <critical-table>;
SHOW GRANTS FOR '<application-user>'@'%';
SELECT @@character_set_server, @@collation_server, @@sql_mode;
正式切换时停止写入、记录 binlog/GTID 位置、追平增量,再切换应用连接。空库能连接不算成功。
第 4 步:Redis 迁移
先判断 Redis 属于哪一类:
- 可丢弃缓存。
- Session 存储。
- 分布式锁。
- 业务主数据。
查看持久化状态:
redis-cli -h <old-redis-host> INFO persistence
redis-cli -h <old-redis-host> INFO keyspace
redis-cli -h <old-redis-host> CONFIG GET appendonly
redis-cli -h <old-redis-host> CONFIG GET dir
RDB 是时间点快照,AOF 记录写操作;两者同时启用时,Redis 重启会优先使用更完整的 AOF。迁移前按Redis Persistence确认版本对应的文件结构,尤其注意 Redis 7 及之后版本的多文件 AOF。
恢复后验证:
redis-cli -h <new-redis-service> PING
redis-cli -h <new-redis-service> INFO keyspace
redis-cli -h <new-redis-service> DBSIZE
然后从真实应用 Pod 访问 Service DNS。PING 成功只证明 Redis 进程可用,不证明 key、TTL、数据库编号和序列化格式正确。
第 5 步:Kafka 迁移
Kafka 迁移不能只把某个 broker 的数据目录复制到三台新节点就算完成。需要盘点:
Topic 与 partition
replication factor
min.insync.replicas
consumer group offsets
ACL
broker.id 或 node.id
listener / advertised.listener
迁移策略通常选择:
- 在新集群创建同构 Topic。
- 使用经过验证的跨集群复制工具持续同步。
- 验证生产和消费。
- 短暂停写或接受已定义的 RPO。
- 追平 lag,切换生产者,再切换消费者。
Kubernetes 内部 listener 可以使用稳定 DNS,但客户端最终会直连 broker 返回的 advertised address。不能把三个 broker 简单替换成一个普通 ClusterIP。
验证:
kafka-topics.sh --bootstrap-server <new-bootstrap> --describe
kafka-consumer-groups.sh --bootstrap-server <new-bootstrap> --all-groups --describe
再发送带唯一 ID 的消息,确认目标消费端具备只处理一次或幂等消费的能力。
第 6 步:RabbitMQ 迁移
分别保护:
- 用户、角色、vhost、policy、exchange、queue、binding。
- 尚未消费的持久消息。
- TLS 与 Erlang cookie 等集群材料。
先导出 definitions:
rabbitmqctl export_definitions /backup/definitions.json
definitions 不包含队列中的消息。若必须保留消息,应根据队列类型、版本和拓扑选择官方支持的复制、备份或停机迁移方案。
恢复后检查:
rabbitmq-diagnostics check_running
rabbitmq-diagnostics check_local_alarms
rabbitmqctl list_vhosts
rabbitmqctl list_queues name durable messages_ready messages_unacknowledged
最后使用测试 exchange/queue 完成一次生产和消费。
第 7 步:Zookeeper 迁移
盘点:
dataDir、snapshot 和 transaction log。myid与成员列表。- ACL 和业务根路径。
- 当前 Leader/Follower 状态。
三个副本必须分散到不同节点和物理故障域:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: zookeeper
topologyKey: kubernetes.io/hostname
恢复时不要让多个节点使用相同 myid。启动后检查成员角色、znode 和 ACL,再让 Kafka 等依赖接入。
迁移完成后,如果 ZooKeeper Pod 的内存长期接近 request 值,不要直接判定为泄漏;可以按ZooKeeper 固定堆与 Kubernetes 资源收缩继续检查 Xms/Xmx、mntr、资源预设和 PVC。
第 8 步:按依赖顺序启动
PVC 与存储
→ MySQL / Redis
→ Zookeeper
→ Kafka / RabbitMQ
→ Nacos
→ 平台依赖
→ 业务服务
每启动一个服务,都先验证 Endpoint:
kubectl -n <namespace> get pod,service,endpointslice -o wide
kubectl -n <namespace> get events --sort-by=.lastTimestamp
第 9 步:切换客户端
配置优先使用 Service DNS:
spring.datasource.url=jdbc:mysql://mysql.<namespace>.svc.cluster.local:3306/<database>
spring.data.redis.host=redis.<namespace>.svc.cluster.local
spring.kafka.bootstrap-servers=<kafka-bootstrap-dns>:9092
spring.rabbitmq.host=rabbitmq.<namespace>.svc.cluster.local
Kafka broker advertised listener、跨集群数据库和外部消费者必须单独设计,不能机械套用。
第 10 步:真实业务验收
至少包含:
- MySQL 真实应用账号读写和事务。
- Redis Session、TTL 和锁。
- Kafka 生产、消费、offset 与 lag。
- RabbitMQ 发布、确认、消费和积压。
- Zookeeper 节点读取、写入和 ACL。
- 删除或重建一个非关键副本后的恢复。
回滚边界
将入口切回旧集群只适用于新环境尚未产生权威写入的阶段。一旦新集群开始接收写入,就必须提前设计好:
- 反向增量同步。
- 双写一致性与去重。
- 写入冻结点。
- 数据冲突处理。
没有这些机制,就不能把“改回 DNS”描述为完整回滚。
完成定义
- 每个服务都有经验证的 RPO、RTO 和恢复步骤。
- 备份在隔离环境成功恢复,不只是文件存在。
- 数据量、关键行/key、权限、索引、Topic、队列和 ACL 与基线一致。
- 三副本跨节点并尽量跨物理故障域。
- 应用通过 Service DNS 访问依赖。
- 真实读写、消息生产消费和故障重建通过。
- 旧环境和备份在观察期结束前保持只读。
数据服务迁移的完成标准不是所有 StatefulSet 都 Ready,而是业务能够证明数据完整、可继续写入且仍可回滚。
DISCUSSION
讨论与反馈