K8s单节点集群准不停服迁移至阿里云ECS与压测实战
2026/9/18 2:53:46 网站建设 项目流程

前两篇我们把K8s集群从零搭起来,也把应用跑起来了。这一篇聊点更刺激的:把一个跑在单节点K8s上的若依微服务整套环境,准不停服、不丢数据地迁移到阿里云ECS,迁移完还要用jmeter做高并发压测验证承载能力。这个过程我踩了不少坑,也攒了不少心得,这次一次性倒出来。

先交代一下背景。我这里说的“单节点K8s”,就是一台机器上同时做了master和worker,etcd、kube-apiserver、kubelet、容器运行时全挤在一台机器里。若依微服务(RuoYi-Cloud)大家应该不陌生,Spring Cloud Alibaba那一套,nacos做注册中心和配置中心,gateway做网关,后面挂着auth、system、job、file、monitor等一堆服务,底下撑着MySQL和Redis。这套东西在开发环境跑得很欢,但真要上云、要面对压测,问题就一个接一个浮出来了。

1. 迁移前的整体设计与思路拆解

1.1 “准不停服、不丢数据”到底怎么理解

我特别较真地抠过“准不停服”这几个字。先说结论:不是说不允许一秒钟的服务不可用,而是说在迁移窗口内,存量连接尽量不断、增量数据一条不丢、对外提供服务的入口尽可能平滑切换。真正不停服的迁移要么靠双活流量调度,要么靠数据同步热切换,对一个单节点K8s环境来说,做到“准不停服”比“绝对不停服”更实在,也更符合实际工程要求。

所以这次迁移的核心指标拆成三个:第一,数据零丢失,MySQL和Redis里的数据不允许丢一条;第二,业务中断窗口控制在分钟级,最好是流量切换到新环境时只产生几秒钟的连接中断,而不是整个停机重来;第三,迁移完成后能扛住jmeter高并发压测,不能一压就雪崩。

有了这三个指标,方案选型就好办了。数据层面走“备份+增量同步”的组合,业务层面走“新集群部署+旧集群缩容+入口切换”,整个流程是可控的、可回退的。

1.2 为什么选阿里云ECS而不是托管K8s

可能有人会问,既然要上云,直接买阿里云的ACK托管集群不香吗?说实话,对生产环境来说ACK确实省心,但这次场景有它的特殊性:一是原环境是自建的单节点K8s,很多配置、存储、网络策略都是按自建方式做的,迁移到ACK要额外适配一套云厂商的机制;二是团队希望保留对集群的完全控制权,尤其是后面要压测、要调优、要看各种系统参数,自建ECS上想怎么看就怎么看;三是单节点环境本身负载有限,先上ECS自建K8s集群,跑通迁移流程和压测验证,性价比更高。

所以在阿里云ECS上重新搭一套单节点K8s,把镜像、配置、数据、服务编排全部迁过去,是我这次选的方案。实际操作下来,这个思路是对的,因为整个过程可控性极强,出了问题可以随时从旧环境切回来。

1.3 链路拆解:这次迁移涉及哪几层

我把整个迁移拆成了五层:镜像层、配置层、数据层、服务编排层、流量入口层。

镜像层要解决的是旧环境里的镜像怎么搬到新ECS上,本质是选择镜像仓库和镜像tag的规划。配置层要解决的是ConfigMap、Secret、nacos配置中心里的配置项怎么迁移,这一步最容易遗漏。数据层是重头戏,涉及MySQL和Redis的数据同步,后面会专门讲。服务编排层要把Deployment、Service、PVC这些资源的YAML导出、改造、再应用,顺序很重要。流量入口层则是从访问入口逐步切到新环境,实现对外服务的平滑过渡。

这样拆完以后,整个迁移就变成了一条清晰的执行流水线,每一步都有明确的输入和输出,踩坑也能准确定位到层。

2. K8s与Docker的分工,以及单节点集群的典型资源布局

2.1 先理清Docker和K8s到底差在哪

这个热搜词几乎每次都会出现,但大多数人还是似懂非懂。我用一个容易理解的类比:Docker是集装箱的标准,它解决的是“怎么把一个应用连同环境装进一个标准箱子里”;K8s是整个港口的管理系统,它解决的是“这个箱子放到哪台吊机上、什么时候放、箱子之间怎么通信、箱子坏了怎么处理”。Docker管单机单容器,K8s管跨机器集群的编排调度。

这个认知直接决定了迁移的姿势。如果不懂这一点,你很容易把迁移做成“把容器export成一个tar包,然后搬到新机器docker load再docker run”——这是最典型的错误做法。K8s迁移的正解是:把应用打成镜像推到仓库,在新集群上用YAML声明期望状态,让控制器自己去调度和拉起Pod。这样你的迁移对象不是一台台机器上的容器,而是一整套声明式配置,到了新环境apply一下就完事。

2.2 单节点K8s的资识布局和注意事项

单节点K8s最特殊的地方在于,默认情况下控制面节点是带污点(taint)的,Pod是不会被调度到master节点上的。所以你在单节点上用kubeadm装完集群后,通常会执行一条命令把污点去掉:

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

否则你会看到所有Deployment创建的Pod都卡在Pending状态,Event里一直报“0/1 nodes are available: 1 node(s) had untolerated taint”。这个坑尤其对初次搭单节点集群的人特别不友好,因为从集群状态看节点是Ready的,查来查去才发现是污点问题。

单节点集群的第二个特点是没有真正意义上的高可用,etcd只有单副本,master挂了集群就挂了。这在迁移验证阶段可以接受,但压测前一定要确认ECS的规格。我之前试过2C4G的机器跑若依微服务全套,结果是各个服务互相抢内存,还没压测自己先把OOM触发了一遍。后来老老实实升到4C8G,才算是把整套环境稳定跑起来。如果还要跑jmeter压测,建议直接上8C16G,因为压测机本身也吃资源,而且你要在压测的同时观察Pod状态和监控指标。

2.3 迁移场景必备的K8s常用命令

这里把我这次迁移过程中用得最多的命令整理成一张速查表,全部是实操验证过的,建议直接收藏:

场景命令说明
查看集群节点kubectl get nodes -o wide看节点状态、IP、kubelet版本
查看所有资源kubectl get all -n ruoyi一次性列出该命名空间下的deploy/pod/svc
导出资源YAMLkubectl get deploy nginx -n ruoyi -o yaml > nginx.yaml备份和迁移的必备操作
实时看Pod状态kubectl get pods -n ruoyi -w-w持续监听变化
查Pod明细kubectl describe pod <pod名> -n ruoyiEvent里有大部分问题的真相
看日志kubectl logs -f <pod名> -n ruoyi加--tail=100看最近100行
进容器调试kubectl exec -it <pod名> -n ruoyi -- /bin/sh有些问题只能在容器里查
应用YAMLkubectl apply -f xxx.yaml声明式应用,可重复执行
查看PVCkubectl get pvc -n ruoyi确认存储绑定状态

实战里最常用的组合拳是:先get pods看到CrashLoopBackOff或者Pending,马上describe pod看Event,再不行就logs看应用日志。把这个流程练熟了,排查效率能提高一大截。

3. 数据迁移是核心:如何做到不丢数据

3.1 先定方案:MySQL和Redis怎么迁

数据迁移是整个项目里风险最高的一环,没有之一。我原先是打算直接在旧机器上打包MySQL的数据目录,然后传到新ECS上解压。但仔细一想这个方案有个致命问题:MySQL的数据目录里包含binlog、undo log、redo log,直接拷贝必须保证实例是干净停掉的,否则拷贝出来的文件可能处于不一致状态,恢复出来数据可能丢。而“准不停服”要求MySQL最好一直能提供写服务。

所以我换成了更稳妥的组合方案:全量备份加增量同步。具体地说,MySQL这边先在新旧两个环境之间搭一个主从关系,或者至少用mysqldump做一次全量备份,后面再补binlog。若依微服务的库不算大,我直接用mysqldump解决全量,然后持续追binlog到切换之前。

Redis这边更简单,因为缓存数据即使有一点点丢失,理论上也可以通过缓存回源重新加载。但为了保险起见,我还是采用了“RDB加AOF双开”的方式,先把旧Redis的RDB文件拷到新环境启动,再通过AOF补最近一段时间的增量写命令。

3.2 MySQL全量备份与增量同步实操

先说全量备份。考虑到表结构里可能有外键关联,库表之间数据有依赖,我建议用--single-transaction参数做一致性快照,避免中途有人写库导致备份出来的数据前后不一致。典型的命令是:

mysqldump -uroot -p密码 --single-transaction --set-gtid-purged=OFF --all-databases > all.sql

然后把这个SQL文件传到新ECS上,用source all.sql导入。

增量部分,我用的是MySQL主从复制的方式。在旧库上先创建专用的复制账号,然后记录当前binlog的文件名和位置:

CREATE USER 'repl'@'%' IDENTIFIED BY '密码'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; SHOW MASTER STATUS;

新库导入完全量备份后,直接配置主从:

CHANGE MASTER TO MASTER_HOST='旧库IP', MASTER_USER='repl', MASTER_PASSWORD='密码', MASTER_LOG_FILE='mysql-bin.000012', MASTER_LOG_POS=1847; START SLAVE;

配置好之后,用SHOW SLAVE STATUS\G看两个关键字段:Slave_IO_Running: YesSlave_SQL_Running: Yes。这两个都是Yes了,增量同步才算建立成功。在正式切换之前,新库会一直通过主从复制保持跟旧库的同步,这样切换那一瞬间,数据只差最后几秒的写入量,业务中断窗口被压缩到极小。

3.3 数据一致性校验与切换前的收尾

单纯靠复制状态里的Running字段不够,我额外做了一次数据一致性校验。最简单的方法是分别在新旧两个库上执行相同的SELECT聚合查询,对比关键表的数据行数和求和结果。若依里的sys_usersys_rolesys_menu这些核心表建议重点核对。

更严格的方案是直接用pt-table-checksum这类工具去校验主从数据一致性,它会逐行计算checksum然后对比。不过对于小数据量的场景,手动比对行数加几个关键查询基本就够了。我在实际操作中发现,主从复制偶尔会因为网络抖动卡住,Seconds_Behind_Master这个值会一直涨,所以切换前一定要确认这个值归零,也就是从库已经完全追上主库。

Redis那边的操作就没有这么复杂,把旧环境的dump.rdb拷贝到新环境配置好路径,启动Redis自动加载就行了。启动后可以用redis-cli --scan抽样看一下,确认缓存key存在且格式没变。

4. 迁移实操:从旧集群到阿里云ECS的完整流程

4.1 第一步:全量导出资源清单

在动新集群之前,先把旧集群上跑着的资源全部摸清楚。这一步的核心是导出一份完整的清单,确保任何一个服务都不会漏迁。我在旧集群上执行了这样一串命令,把所有关键资源的YAML都备份下来:

kubectl get ns ruoyi -o yaml > ns.yaml kubectl get deploy -n ruoyi -o yaml > deploy-backup.yaml kubectl get statefulset -n ruoyi -o yaml > sts-backup.yaml kubectl get svc -n ruoyi -o yaml > svc-backup.yaml kubectl get cm -n ruoyi -o yaml > cm-backup.yaml kubectl get secret -n ruoyi -o yaml > secret-backup.yaml kubectl get pvc -n ruoyi -o yaml > pvc-backup.yaml

导出之后不要直接拿到新集群去apply,因为YAML里会带着旧集群专属的信息,比如uid、resourceVersion、clusterIP、nodeName之类的字段,这些字段在新集群里要么冲突要么无效。我实际做的时候,是写了一个小脚本,用yq工具把不需要的元数据字段过滤掉,只保留真正声明性的内容,比如apiVersion、kind、metadata.name、spec这些。这个环节很费时间,但也最值得花时间,过滤干净了后面apply基本一遍过。

4.2 第二步:镜像推送与拉取策略

单节点旧集群里的镜像都是本地构建、本地使用的,没有推到任何仓库。到了新ECS上,第一件事就是让新集群能拉到这些镜像。我的做法是把旧机器上所有相关镜像打tag推到阿里云容器镜像服务ACR的个人版仓库,然后在Deployment里修改image字段,指向ACR的完整地址。

docker tag ruoyi/ruoyi-gateway:latest registry.cn-hangzhou.aliyuncs.com/xxx/ruoyi-gateway:latest docker push registry.cn-hangzhou.aliyuncs.com/xxx/ruoyi-gateway:latest

镜像tag的规划这里特别多说一句。很多人在迁移时会顺手把所有镜像都打成latest,这是隐患。因为拉取策略如果设置了Always,kubelet每次创建Pod都会去仓库检查latest是否有更新,一旦网络波动就会导致镜像拉取超时,Pod反复拉不起来。我建议每个服务都打一个固定版本号,比如ruoyi-gateway:2025.06.01,Deployment里imagePullPolicy设为IfNotPresent。这样可以保证Pod一定用的是你迁过去的那一版镜像,也方便回滚。

4.3 第三步:ConfigMap和Secret的适配

配置这块很容易被忽视,却是迁移失败的第一大原因。若依微服务里,数据库连接串、Redis地址这些通常放在nacos配置中心,但部署K8s时,很多环境变量、启动参数又是在ConfigMap和Secret里管理的。我在导出YAML之后,逐个检查了其中有没有写死旧集群IP的地方,比如nacos地址、数据库地址、Redis地址,所有这些都要改写成新ECS的IP,或者改成K8s集群内的Service名。

Secret的处理更敏感。直接导出Secret会带着base64编码的敏感数据,这个过程注意控制范围,迁移完成后旧集群的Secret建议立即清理或轮换。我用的是kubectl get secret xxx -n ruoyi -o yaml导出,然后新集群apply之前重新生成了一遍密码并同步改到nacos配置中心的数据库配置里。虽然麻烦一点,但安全性有保障。

4.4 第四步:服务编排与启动顺序

若依微服务虽然拆成了很多个模块,但启动顺序是有强依赖的,乱序启动会导致一堆服务注册不上。按我这次成功的经验,推荐的顺序是:先基础设施(MySQL、Redis、nacos),再基础服务(auth、system、file、job),然后网关(gateway),最后才是其他依赖较多的服务。MySQL和Redis我用的是StatefulSet加PVC,保证稳定的主机名和存储;nacos用Deployment加Headless Service,让其他服务通过nacos的Service名访问。

在写Deployment的YAML时,有两个细节要特别留意。一个是探针,若依的服务默认情况下可以不配liveness,但readinessProbe建议一定要配,因为K8s要等到readiness探针通过后才会把Pod所在的Service Endpoint摘掉,没有探针的话,切换流量时新Pod还没准备好就被打上流量,直接导致503。另一个是terminationGracePeriodSeconds,我给每个服务设置了60秒以上的优雅退出时间,让Spring Boot处理完存量请求再退出,避免迁移切换时把正在处理的请求硬掐断。

4.5 第五步:流量切换与回退机制

所有服务在新集群上跑起来之后,进入流量切换环节。我采用的策略是“先验证内网端口,再切对外入口”。先用kubectl port-forward方式或者直接在ECS上用curl验证各个服务的内部端口,例如nacos的8848、gateway的8080,确认能拉到注册的服务列表。然后再改对外入口。

单节点K8s环境下,对外暴露方式一般有两种:NodePort或Ingress。如果用的是NodePort,切换方式很简单,在公网负载均衡里把后端转发的节点端口指向新ECS的IP和NodePort。如果用了Ingress Controller,那就把Ingress的YAML在新集群apply一份,调整DNS解析或负载均衡监听指向新入口即可。这一步我强烈建议先在压测环境预演一遍,真的切生产入口时,整个人工流程控制在5分钟以内是没问题的。

回退机制同样重要。我在新集群上跑通后的24小时内,并没有直接把旧集群销毁,而是保留了旧环境的完整可用状态,只把入口切走了。如果新环境出现重大问题,随时可以把入口切回旧集群,数据因为主从关系已经追平,业务不会受影响。切换完成并且稳定运行一天后,再拆除旧环境。

5. 迁移后的压测验证:jmeter高并发场景实测

5.1 压测脚本的准备与参数设计

迁移完成只是上半场,下半场才是决定这套云上环境能不能真正承载业务的关键:压测。这里我用的是jmeter,配套的压测脚本需要根据若依微服务的接口结构做适配。若依的接口通常都在gateway后面,统一走/prod-api/前缀路由,所以jmeter脚本里先加一个HTTP请求默认值,把协议、主机、端口和路径前缀都配置好。

需要注意的一点是认证。若依微服务默认走JWT或者自定义token,jmeter脚本里要先从登录接口获取token,然后通过HTTP Header Manager加到后续请求里。我用的是登录后从返回结果里提取token,再用JSON提取器或者正则表达式提取器,把token保存到jmeter变量里,供后续请求引用。

线程组设计上,我建议不要一上来就全量高并发。先做小规模验证,比如30个线程跑1分钟,确认接口都能通、token没问题,再逐步加压。正式压测我通常设置150个线程,Ramp-Up 30秒,循环10次,这个量级对一个4C8G的单节点K8s环境已经很有压力了。如果要测极限承载,再翻倍到300个线程观察系统的表现。

5.2 压测执行过程记录

压测执行过程中,我同时盯三块数据:jmeter的聚合报告、ECS的系统负载、K8s里的Pod资源消耗。

jmeter聚合报告重点看吞吐量(Throughput,单位是req/s)、90%和95%响应时间、错误率(Error%)。我第一次压的时候,150个线程一上去,gateway的Pod CPU直接打满,95%响应时间飙到3000毫秒以上,错误率接近10%,一看就是网关成了瓶颈。后来在gateway的Deployment里加了HPA,让Pod数量根据CPU使用率自动扩展到3个副本,再压的时候吞吐量明显提升,错误率归零。

ECS系统层用topvmstatfree -h实时观察CPU、内存、IO情况。K8s侧用kubectl top nodekubectl top pods看资源占用。我记录了一组比较典型的数据:4C8G的ECS上,MySQL和Redis分别吃掉了1.5G左右内存,nacos和gateway各占800M,剩余业务服务加起来吃2G左右,跑150并发时CPU总使用率在70%左右,内存快接近上限但没触发Swap。这个数据说明单节点在150并发下属于“有点紧但能扛住”的状态。

5.3 压测中的瓶颈定位与扩容策略

压测除了验证承载能力,更大的价值在于发现瓶颈。我这次压测中暴露出的问题主要有三个:

第一个是gateway的压力最大,这是请求入口,所有流量都经过它,单副本必然扛不住。解法就是给gateway配置HPA,按CPU使用率或请求QPS自动扩缩容。第二个是数据库连接池,若依默认的Druid连接池配置在压测时会快速耗尽连接,导致业务报“获取连接超时”。这个要在nacos配置中心里把最大连接数调大,同时注意ECS文件描述符和MySQL的max_connections参数同步调整。第三个是sentinel限流,若依集成了sentinel之后,一些接口在压测时会触发限流规则,返回“Blocked by Sentinel”,这个不一定是系统扛不住,反而是保护机制生效了,需要结合限流阈值判断是调大阈值还是调整框架策略。

5.4 启动Prometheus监控,让压测数据可视化

压测期间全靠命令行看数据太费劲,而且不够直观。我在新集群上顺手搭了一套Prometheus加Grafana监控,这也算是补全了单节点K8s环境的基本观测能力。采集指标分别由node-exporter提供ECS的节点指标,kube-state-metrics提供K8s对象指标,cAdvisor提供容器指标。Grafana里直接导入node-exporter和Kubernetes的相关面板,压测时大屏一开,哪里是瓶颈一眼就能看出来。

部署方式不复杂,用Helm Charts安装这几套组件最省事:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm install prometheus prometheus-community/kube-prometheus-stack -n monitoring --create-namespace

装完以后,Prometheus的默认告警规则也会跟着生效,比如Pod频繁重启、节点CPU过高、PVC使用率超过阈值,都会自动报警。压测过程中我实际收到了几次告警,帮助我及时调整线程数和分流策略。

6. 常见问题与避坑实录

6.1 单节点K8s迁移中的高频坑

我把自己和周围人踩过的坑做了一个汇总,这些问题在单节点K8s环境里特别容易出现:

问题现象根因解决方案
Pod一直Pendingmaster节点有taint未去除执行kubectl taint nodes --all node-role.kubernetes.io/control-plane-
镜像拉取超时镜像tag为latest且策略为Always固定版本号,imagePullPolicy设为IfNotPresent
PVC无法绑定local PV有节点亲和,跨机器失效新环境重新申请PV/PVC,不要迁移原PV
NodePort端口冲突服务之间没有规划好端口段提前规划端口分配,避免随机撞车
域名解析切不过来DNS缓存或负载均衡权重未调整降低TTL,逐步切权重并观察灰度
Spring Boot启动很慢依赖nacos和数据库未就绪readinessProbe配置足够长的initialDelaySeconds

6.2 若依微服务迁移中的特殊问题

若依微服务这套框架在迁移中有些独特的地方,需要单独拿出来说。nacos作为注册中心和配置中心,迁移后第一件事检查namespace,若依默认的namespace是public,但如果你用了自定义namespace,各服务的配置和注册列表就都对不上,表现为服务之间互相找不到。还有nacos里的配置数据,如果用的是MySQL存储,要确认nacos库的表也一起迁移了,否则配置中心里所有配置都会丢失。

gateway的服务名和路由配置也要重点检查。若依的路由配置在nacos里存着gateway这个dataId,迁移后如果这个配置为空,gateway启动后连转发入口都没有。我遇到过页面能打开但所有接口都报404的情况,查到最后就是路由配置没同步过来。

Redis方面的一个坑是key序列化问题。若依默认用的序列化方式是JDK序列化或Jackson,如果你在迁移前后改了Redis的序列化配置,缓存字符可能对不上,导致登录态失效、验证码校验不过。迁移时保持旧环境Redis的序列化配置不要动,等业务稳定后再考虑渐进式改造。

6.3 面试高频题快答:K8s与Docker、常用命令、核心概念

既然热搜里出现了大量k8s面试相关词汇,我也把自己带队面试时经常问的几道高频题顺手答一遍,方便大家对照自查。

问:Docker和K8s什么关系?答:Docker是容器运行时,负责构建和运行容器,K8s是容器编排系统,负责管理大量容器的调度、扩缩容、服务发现和自愈。两者不是替代关系,而是互补关系,K8s的底层节点完全可以由Docker或containerd来运行容器。

问:Pod是什么?为什么K8s最小调度单位是Pod而不是单个容器?答:因为有些场景下多个容器需要共享网络命名空间、共享存储卷、部署在同一台节点上,把它们放在同一个Pod里才能保证这种“超亲密”关系。Pod是逻辑上的主机,容器是挂在这台主机上的进程。

问:Deployment和StatefulSet的区别?答:Deployment适合无状态服务,Pod之间没有身份差异,无序扩缩容;StatefulSet适合有状态服务,每个Pod有稳定的网络标识和稳定的存储,适合MySQL、Redis、etcd这类需要稳定身份的组件。

问:Service是怎么做服务发现和负载均衡的?答:Service通过label selector匹配一组Pod,然后交给kube-proxy生成规则,访问Service的ClusterIP和端口时会负载均衡到后面的Pod上。对于有状态服务,还可以配合Headless Service让每个Pod拥有独立的DNS域名。

问:kubectl get pods看到CrashLoopBackOff怎么办?答:kubectl logs看应用日志确认报错,kubectl describe pod看Event确认容器启动失败的原因,常见原因有镜像不存在、启动命令错误、环境变量缺失、资源配额不够。先修启动层,再查应用层。

我在带人的过程中总结出来一个经验:面试时大家K8s概念都背得滚瓜烂熟,但一放到迁移这种实战场景里就露馅。因为书本知识不会告诉你,PVC是有节点亲和性的,镜像tag是必须固定版本的,nacos的配置是比Deployment更容易遗漏的迁移重点。这些都是只有真正做过一遍完整迁移,才会刻在脑子里的东西。

最后再分享一个个人体会。像这种“准不停服迁移上云”的活儿,最考验人的不是技术深浅,而是流程设计和风险控制。先把每一步拆细,数据有备份、资源有清单、切换有回退、验证有指标,哪怕中途出了幺蛾子,你手里永远有第二条路可以走。迁移类的项目,永远要把“回得去”当成第一原则,能做到这一点,整个操作就稳了一大半。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询