系列导航:Kubernetes 重建迁移教程系列。所有数据库名、账号、节点和存储地址均为占位符。

有状态服务迁移最危险的误区,是以为 Pod 处于 Running 就说明数据已经迁移完成。实际上,一个全新的空数据库同样可以保持 Running,甚至健康检查也全部通过。

先定义每个服务的数据边界

建立清单:

服务权威数据可接受丢失量 RPO可中断时间 RTO恢复工具验证负责人
MySQL业务表、账号、binlog<rpo><rto>物理/逻辑备份DBA
RedisSession、缓存或业务数据<rpo><rto>RDB/AOF应用团队
KafkaTopic、消息、offset<rpo><rto>复制/同步工具消息团队
RabbitMQdefinitions、消息、用户<rpo><rto>definitions + 数据方案消息团队
Zookeeperznodes、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

迁移策略通常选择:

  1. 在新集群创建同构 Topic。
  2. 使用经过验证的跨集群复制工具持续同步。
  3. 验证生产和消费。
  4. 短暂停写或接受已定义的 RPO。
  5. 追平 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,而是业务能够证明数据完整、可继续写入且仍可回滚。