上周我接到一个很典型的迁移需求:客户单节点 K8s 上跑着一整套若依微服务,要求不停服、不丢数据地迁到阿里云 ECS,迁移完还要由压测人员用配套 JMeter 脚本做高并发测试,验证 2c4g 的 Pod 能扛多少并发。这种场景在中小企业里越来越常见——容器化早就不是新鲜事,但随着 Rancher 这类容器管理平台普及,K8s 的使用门槛虽然降下来了,真正遇到 Pod 异常、调度失败、外部访问不通时,能把原理讲清楚、把排查思路捋顺的人还是不多。
这篇文章我把两件事串起来讲:一是 Pod 的核心原理,包括它由什么控制、多容器怎么协同工作、调度和抢占的底层机制;二是基于 Rancher 的实战操作,从安装 Rancher Desktop、部署 Nacos 到迁移上云和压测验证。适合正在学 K8s 的开发者、刚接手集群的运维,以及准备把存量业务平滑迁到云上的团队参考,尤其是计划在 2c4g 这种小规格 Pod 上跑业务的同学,看完能少走不少弯路。
1. 先把 Pod 的底层逻辑吃透
1.1 Pod 到底是什么:为什么 K8s 不直接调度 Docker 容器
先回答一个最基础也最容易被忽略的问题:K8s 的最小调度单元为什么是 Pod,而不是直接调度 Docker 容器?因为容器这个概念在“多个进程需要同生共死、共享网络”的场景下是不够用的。
一个典型例子是业务容器加日志采集 sidecar。业务进程把日志写到本地文件,sidecar 容器把文件采集走,两者必须部署在同一台机器上,共享同一个文件系统,最好还共享同一个网络命名空间,这样访问 localhost 就能通信。如果用 Docker 直接管理,它们就是两个孤立的容器,靠端口映射和网络插件去互联,运维成本高,生命周期也没办法强绑定。
Pod 在 K8s 里的设计,本质上是用一个“基础容器”把这些容器包在一起。这个基础容器通常是 pause(也叫 infra)容器,它只负责创建并持有 Pod 的网络命名空间、PID 命名空间和共享卷。后面你真正跑起来的业务容器,比如 Nginx、Java 服务,都挂到这个 pause 容器下面。所以你在节点上用 docker ps 会看到很多名字里带 pause 或 sandbox 的容器,不要以为是异常,那是所有 Pod 的地基。
这里有个非常关键的理解:Pod 是 K8s 的调度单位,但它不是进程,而是一个“共享运行环境的边界”。Pod 里的所有容器共享同一个 Pod IP、同一个 hostname、同一批 volume。反过来,不同 Pod 之间默认是隔离的。这个设计决定了你在使用 K8s 时,不能像写 docker-compose 那样什么服务都塞进一个“Pod”,而是要根据生命周期和共享粒度去划分。
1.2 Pod 由谁控制:控制器模型与声明式 API
热搜问题里有一个特别好的问题:“pod由什么控制”。直接回答:Pod 通常不是你自己直接创建出来的,而是由控制器(Controller)创建和管理的。最常见的控制器是 Deployment,它管理 ReplicaSet,ReplicaSet 再负责维护 Pod 的副本数量。
这个链路的逻辑是这样的:你写一个 Deployment YAML,里面声明副本数是 3,镜像是什么,端口是什么。kube-controller-manager 里的 deployment controller 看到期望状态后,会创建或更新一个 ReplicaSet;ReplicaSet controller 再确保实际运行的 Pod 数量等于期望数量。如果某个 Pod 崩了,控制器会马上创建一个新 Pod 顶上来,目标只有一个:让集群尽可能接近你声明的那份期望状态。
这就是 K8s 最核心的设计哲学:声明式 API 加控制器调谐。你不用告诉它“请你帮我杀掉 2 号 Pod 再启动一个”,你只需要说“我要 3 个副本”,剩下的事控制器自己完成。使用习惯上,除非是调试场景,否则不要用 kubectl run 直接创建裸 Pod。裸 Pod 没有控制器保护,节点出问题它就真的没了,不会自动重建。
除了 Deployment,还有几类控制器要认识。StatefulSet 负责有状态服务,比如 Nacos、MySQL、ZooKeeper,它会给每个 Pod 一个稳定的网络标识和独立的存储卷;DaemonSet 保证每个节点都运行一个指定 Pod,比如日志采集、节点监控;Job 和 CronJob 负责一次性任务和定时任务。搞清楚这些,遇到“Pod 起不来”“Pod 被删了又起来”这类问题时,第一反应就应该是去看它归哪个控制器管。
1.3 多容器 Pod 的同生共死:启动顺序、探针与故障影响
有一个热搜词问得很尖锐:“一个pod中如果包含了多个容器,如果其中一个容器没有成功启动,可能会影响其它容器”。答案是:会影响,而且影响方式要看容器类型和启动阶段。
首先说启动顺序。同一个 Pod 里的容器,如果定义了 initContainers(初始化容器),那么必须等所有 init 容器全部成功退出,业务容器才会开始启动。如果某个 init 容器一直失败,整个 Pod 就卡在 Pending 或者反复创建,业务容器根本不会启动。这就是为什么你把数据库迁移脚本放进 init 容器时,数据库地址写错会导致整个 Pod 起不来。
其次说运行阶段。Pod 有个 restartPolicy,默认是 Always。如果主业务容器崩溃退出,kubelet 会重新拉起容器,这个逻辑对 Pod 里的每个容器都生效。如果 sidecar 容器崩了,它也会被重启,但主容器不一定受影响;反过来,如果主容器一直崩溃,同 Pod 的 sidecar 也会跟着这个“反复重启”的节奏一直重启,共享日志卷的采集任务可能变得断断续续。
同生共死还体现在删除和调度上。你执行 kubectl delete pod,整个 Pod 的所有容器会一起被终止;节点故障时,整个 Pod 被标记为失败,控制器会另起一个新 Pod,之前 Pod 里的所有容器都不会“单独幸存”。所以设计上要记住一条经验:生命周期不同步的服务,不要塞进同一个 Pod。比如应用和数据库,就必须拆开;应用和日志采集,如果只是依赖本地文件,放同一个 Pod 是可以的,但也要能接受 sidecar 跟着应用一起重启。
2. K8s 与 Docker 的区别及日常操作
2.1 K8s 和 Docker 到底差在哪
每次分享会都有人问“k8s和docker区别”,这里给一个最容易记住的类比:Docker 提供的是集装箱,K8s 提供的是港口调度系统。Docker 负责把应用打包成镜像、在单机上把容器跑起来;K8s 负责在多台机器之间统一调度这些容器,包括自动扩容、故障恢复、服务发现、滚动更新。
以前很多人习惯 docker run、docker-compose up,觉得也能管十个八个容器。但当规模到几十上百个容器,还要跨好几台机器部署时,单机 Docker 就变得很难维护:你要自己处理容器在哪台机器上、端口冲突了怎么办、挂掉的容器谁来拉起、流量怎么分发。这些恰恰是 K8s 的核心能力。
还有一个容易混淆的点:K8s 本身不是容器运行时。在早期版本里,K8s 通过 Docker 来启动容器;但从 1.24 版本开始,kubelet 不再直接走 Docker。现在集群底层的容器运行时基本都是 containerd、CRI-O 这类符合 CRI(Container Runtime Interface)的引擎。Rancher Desktop 里专门提供了 dockerd(moby) 模式,是为了给那些还依赖 Docker API 的开发环境做兼容,本质上是两套东西。
所以如果你是刚开始接触容器,建议先把 Docker 的命令学会,会用 Dockerfile 构建镜像、会用 docker run 跑容器,然后再进入 K8s 学习编排。如果你已经有一定经验,遇到“镜像没问题、端口没冲突,但容器就是起不来”的情况,要记得去查节点上的容器运行时日志,而不是只盯着 Docker 日志看。
2.2 高频 kubectl 命令与排查姿势
K8s 的日常操作九成都在 kubectl 上,Rancher UI 只是把命令可视化。我列出常用的几条,按排查流程排:
- kubectl get pods -A:先看全局状态,哪些异常一眼能看到。
- kubectl describe pod -n :看事件,这是排障第一步,很多隐形原因只出现在 Events 里,比如拉镜像失败、持久卷挂载失败、探针失败。
- kubectl logs -n :看业务日志;如果 Pod 里有多个容器,要加 -c 指定容器。
- kubectl exec -it -n -- /bin/sh:进容器调试,查看环境变量、网络连接、文件。
- kubectl get svc -A 和 kubectl get ep -A:检查服务暴露和端点是否正常。
- kubectl top nodes / kubectl top pods:实时看资源使用率,这个需要 metrics-server 已安装。
- kubectl rollout restart deployment/ -n :滚动重启某个工作负载,比手动删 Pod 更优雅。
排查的时候有个顺序误区,很多人一上来就 logs。其实应该先 describe 看事件,再 logs 看应用日志,再 exec 进容器确认内部状态。比如 Pod 一直 Pending,logs 会误导你,因为容器可能从来没有被创建过;describe 会直接告诉你调度失败的原因,比如节点资源不足、PVC 没绑定、镜像拉取认证失败。
2.3 资源限制怎么配:2c4g Pod 的并发极限和 preemption 报错
热搜里“2c4g的pod支持的并发量”是个特别务实的词。先给结论:没有一个放之四海而皆准的数字,因为它取决于应用类型、框架、数据库连接池、JVM 堆配置和流量模型。但可以给两个经验参照:如果只是 Nginx 静态文件服务,2c4g 的 Pod 在优化参数下跑到几百甚至上千 QPS 并不稀奇;如果是一个典型的 Spring Boot 微服务,带 MySQL、Redis 依赖,乐观估计能扛 100-200 个并发用户的稳定访问,再往上就容易出现线程池排队、数据库连接池等待、GC 频繁的连锁问题。
讨论并发之前,先把资源声明说清楚。Deployment 里的 resources.requests 和 limits 是两回事:requests 是调度器为 Pod 预留的资源,Pod 被调度时,节点必须有这么多可用资源;limits 是运行时的硬上限,超过 CPU limit 会被限流,超过内存 limit 会触发 OOM Kill。如果一个容器没写 requests 只有 limits,或者什么都不写,调度和运行时行为会很不一样,这也是很多“看起来配置了资源但还是被 OOM”的原因。
“no preemption victims found for incoming pod”这个报错,常见于集群资源紧张且使用了 PriorityClass 的场景。意思是调度器想把当前要调度的 Pod 排进去,但节点上资源不够,于是按优先级抢占低优先级的 Pod,结果没有找到合适的“牺牲品”。可能原因包括:现有低优先级 Pod 无法被抢占(比如是系统关键组件),或者抢占条件不满足。排查方向有三个:先 kubectl get nodes 看资源余量;再看这个 Pod 的 priorityClassName 是否设置合理;最后检查是否有亲和性或污点约束导致抢占条件不成立。更实际的解法是给节点扩容,或者把 Pod 的 requests 调小一点,因为抢占本身是一种应急手段,别指望它长期兜底。
3. Rancher 实战:从安装到应用发布
3.1 为什么我会推荐 Rancher 做容器管理
先亮明立场:Rancher 不是 K8s 的替代品,它是 K8s 的管理平台。我推荐它的原因很简单——当你同时管理多个集群、需要给团队不同角色开不同权限,或者团队里有人不习惯命令行时,Rancher 的上手成本是最低的。
Rancher 的核心能力包括:多集群统一纳管,你可以通过同一个控制台管理云上云下多套 K8s 集群;内置 RBAC 和认证,对接 LDAP 或 AD 后,开发、测试、运维分权很清晰;提供应用商店,可以一键部署 Nacos、MySQL、监控等常用中间件;还有可视化的工作负载管理,Deployment、Service、Ingress、ConfigMap、Secret 都可以在页面上点出来。
那什么时候不需要 Rancher?如果只是单集群、两三个人、全命令行流,Rancher 反而是额外组件,会增加维护成本。但如果是给客户交付一套环境,或者要团队里非运维角色也能自助发布服务,Rancher 的价值就体现出来了。我这次的迁移场景,客户环境就是单节点 K8s 加 Rancher 管理,迁移后还要给压测人员看服务状态,Rancher 的 UI 让他们不需要碰 kubectl 就能完成基础验证。
3.2 Rancher Desktop 安装与 Docker API 连接报错处理
如果是本地开发机,推荐用 Rancher Desktop 而不是折腾完整的 K8s 集群。它内置 K3s(轻量 K8s 发行版),也能切换成 dockerd(moby) 模式,兼容本地 Docker 开发流程。
安装时最容易踩的坑,就是 Windows 上启动后执行 docker ps 报错 “failed to connect to the docker api at npipe:////./pipe/docker_engine”。这个报错的本质是:Docker CLI 想连接 Windows 下的 Docker Engine 管道,但 Rancher Desktop 当前使用的是 containerd 运行时,或者 dockerd 模式的守护进程没有启动。
解决办法分三步:打开 Rancher Desktop 的 Settings,选择 Container Runtime 为 dockerd(moby);应用后重启 Rancher Desktop;然后执行 docker context ls,确认当前 context 指向了 rancher-desktop,而不是默认的 default,必要时用 docker context use rancher-desktop 手工切换。如果是 macOS,同样的思路,只是管道名变成了 Unix Socket。还有一个隐藏坑:Rancher Desktop 启动需要虚拟化支持,Windows 上要开 WSL2 或 Hyper-V,否则 dockerd 起不来,日志里会看到虚拟机相关的报错。
3.3 用 Rancher 部署 Nacos,并让外部正常访问
Nacos 是热搜里出现频率很高的组件,因为很多微服务环境都依赖它做注册中心和配置中心。用 Rancher 部署 Nacos,我拆成三块:创建 Deployment、创建 Service、配置外部访问。
创建 Deployment 时,镜像用 nacos/nacos-server:v2.3.2 这种明确版本,不要用 latest。环境变量里至少要配 MODE=standalone(单机模式)和 NACOS_AUTH_ENABLE=false(测试环境先关鉴权,生产必须开)。容器端口填 8848 和 9848。这里 9848 是 Nacos 2.x 的 gRPC 端口,很多人只暴露 8848,结果服务能访问网页,客户端注册却一直失败,就是因为 9848 没开。
Service 的类型,单节点 K8s 最方便的是 NodePort,Rancher 页面上创建 Service 时选 NodePort,把 8848 映射到比如 30848,9848 映射到 30948,然后就能通过 http://节点IP:30848/nacos 访问网页了。云上环境如果配了负载均衡,可以直接用 LoadBalancer 类型,给每个端口生成一个 SLB 地址。需要提醒的是,这里有个隐藏问题:Nacos 2.x 客户端会通过 8848 拿服务端地址,然后走 9848 建 gRPC 连接,所以节点防火墙、安全组都要同时放通这两个端口,否则客户端注册仍然不稳定。
4. 单节点 K8s 迁移实战:不停服、不丢数据
4.1 迁移前先想清楚:方案设计与双跑策略
背景是热搜里那条:单节点 K8s 上的若依微服务整套环境,要不停服、不丢数据地迁移到阿里云 ECS,迁移后还要做高并发压测。这类需求的核心难点不在 K8s 迁移本身,而在“不停服、不丢数据”这六个字上。
我给的方案是“双跑 + 切换”。先在阿里云 ECS 上搭一套新的单节点 K8s(同样装 Rancher 或不装都行),把业务以影子模式部署上去,数据库用全量备份加增量同步的方式追到接近实时的状态。应用和数据库都追平后,选一个切换窗口,把流量从旧环境切到新环境,旧环境暂不销毁,留作回滚。
这套方案的关键是不要把切换当成“换线”,而是当成“发布”。先做小流量验证,再逐步放量,随时能回切。对单节点集群来说,没有多节点的高可用可言,所以更要依赖外部负载均衡或 DNS 做流量切换,而不是在集群内部玩文章。
4.2 数据与配置迁移的具体操作
具体操作我按四条线展开。
第一条线是对象导出。在旧集群上把需要迁移的 Deployment、Service、ConfigMap、Secret 全部导出:
kubectl get deploy,svc,cm,secret -n ruoyi -o yaml > ruoyi-backup.yaml
然后在新集群里 apply -f。这里有个细节:Secret 的 value 默认是 base64 编码的,导出后导入一般没问题,但如果你用了 cert-manager 签发的证书,要注意有效期和 CA 链。
第二条线是镜像同步。确认所有镜像都已经 push 到新集群能访问的 registry。如果只有一个节点,最简单是 docker pull 后 docker save,再传到新机器 docker load;如果有 registry,直接 docker tag 加 docker push 更干净。
第三条线是数据库迁移。这往往是最花时间的。MySQL 可以先用 mysqldump 做一次全量备份导入到新库,然后在旧库开启 binlog,用 DTS 工具或者 canal 做增量同步。同步过程中要反复校验两边数据量和关键表的 checksum,确认没有差异。这里一定要记录好 binlog 的位点,回滚时要用。
第四条线是配置适配。新环境的数据库地址、Redis 地址、Nacos 地址都变了,ConfigMap 里的连接串要统一改。我的习惯是提前把 yaml 里的占位符整理成一个清单,apply 前逐个核对,避免漏改导致 Pod 启动后连不上库。
4.3 切换、验证和回滚预案
切换在业务低峰期做。如果旧环境前面有负载均衡或 DNS,先将权重调成新旧 9:1 之类的小比例,选一两个端口或测试账号走到新环境,验证登录、菜单、查询、写库这些核心链路。确认没问题后,再逐渐放大到 5:5、7:3、10:0。
切换完成后,验证清单至少包括:微服务全部注册到新 Nacos;网关能路由到各服务;数据库写入能正常 commit;日志能正常收集;压测环境能连到新环境。一个容易被忽略的点是定时任务,很多项目里 xxl-job 或 Spring 自带的 @Scheduled 会在新旧两个环境同时启动,造成重复执行。迁移时要把旧环境的调度任务先停掉,只保留新环境的。
回滚预案必须提前写。如果切换后发现问题,把流量权重切回旧环境即可,但要注意一个时序问题:如果旧环境数据库已经被新环境的写入覆盖,回滚就不仅仅是切流量,还要处理数据的反向同步。所以切换窗口的“只读检查”阶段很重要,确认新环境没有严重写入问题,再打开完整写入访问。
5. 压测验证与问题排查速查
5.1 用 JMeter 做高并发压测,评估 2c4g Pod 的承载能力
压测人员用的是配套的 JMeter 脚本,我这边配合看集群指标。压测计划我建议分五步:
- 先跑一次基线:用 20-50 并发慢跑 5 分钟,记录平均响应时间、错误率、Pod 的 CPU 和内存基线。
- 阶梯加压:50、100、200、400 并发,每档跑 5-10 分钟,观察吞吐量和错误率拐点。拐点出现的位置就是这个 Pod 当前配置下的承载上限。
- 盯三个资源维度:Pod 的 CPU 和内存(kubectl top pods)、数据库连接数和慢查询、JVM 的 GC 日志和堆使用。很多时候瓶颈不在容器,而在数据库连接池或线程池参数。
- 观察零散的 OOM 和重启:如果 Pod 内存 limit 设得太紧,压到一定阶段会被 OOM Kill,表现为 JMeter 错误率突然抬升,kubectl get pods 看到 RESTARTS 增长。
- 压测完立刻收集结论,写清“2c4g Pod 在 XX 场景下建议单实例承载 XX 并发,超过后推荐扩副本”。如果页面上要支撑更高并发,优先调 HPA(HorizontalPodAutoscaler),而不是盲目调大单 Pod 资源。
我见过很多人只盯着并发数字,实际上调节压测脚本里的 think time、是否带 cookie、是否有写库操作,结果会差很多。所以一定要保证 JMeter 脚本尽量接近真实用户行为,压出来的数据才有参考价值。
5.2 常见报错速查表
把这一两年我见过的高频问题整理成一张表:
| 现象 | 可能原因 | 排查与解法 |
|---|---|---|
| Pod 一直 Pending | 节点资源不足、污点不匹配、PVC 未绑定 | kubectl describe pod 看事件;检查节点容量、Toleration、PV 状态 |
| Pod ContainerCreating 卡住 | 镜像拉取失败、持久卷挂载失败、CNI 网络未就绪 | describe 查事件;docker pull 手动验证;检查 kubelet 日志 |
| CrashLoopBackOff | 容器启动后立刻退出 | kubectl logs 看启动日志;检查启动命令、环境变量、数据库连接 |
| ImagePullBackOff | 镜像不存在、私有仓库认证失败 | docker pull 手动验证;配置 imagePullSecrets |
| no preemption victims found for incoming pod | 高优先级 Pod 抢占失败 | 检查节点容量与 PriorityClass;降低 request 或扩容 |
| Nacos 客户端无法注册 | 9848 gRPC 端口未放通 | 检查 Service 暴露端口、安全组、防火墙 |
| Rancher Desktop 报 Docker API 连接失败 | 运行时不是 dockerd,或 context 不对 | 切换运行时为 dockerd(moby);docker context use rancher-desktop |
| 压测时 Pod OOM 重启 | 内存 limit 设置过低 | kubectl top pods 看实际占用;调整 requests 和 limits;优化 JVM 堆 |
这张表不能只当背题用,要理解背后的排查顺序:先 describe 看事件,再 logs 看应用日志,再思考网络、存储、安全组这些外围因素。
5.3 实操中沉淀下来的几条经验
最后分享几条不太好写进文档、但实际帮了我很多的经验。
第一,单节点集群虽然简单,但资源竞争问题更明显。一个 Pod 没有设置 resources,另一个 Pod 突然把内存吃满,kubelet 会按 QoS 顺序杀掉低优先级的 Pod。所以单节点更要给每个 Deployment 写清 requests 和 limits。
第二,NodePort 用得很爽,但高并发压测时容易撞到系统限制。比如 Linux 的端口范围、conntrack 表、文件句柄数,都要提前检查。可以先执行 sysctl net.ipv4.ip_local_port_range 和 sysctl net.netfilter.nf_conntrack_max 看下默认值,再决定是否需要调大。
第三,Rancher 的 UI 适合浏览和演示,但做变更时最好还是用 kubectl apply 或 Terraform 这类 IaC 工具,把 YAML 存到 Git 里。UI 上点出的配置很容易流失,过几个月没人记得谁改了什么。
第四,也是最重要的,迁移和压测这类动作,一定要写操作单和回滚步骤。你面对的不是“K8s 容器管理”这一个孤立任务,而是一整个业务系统的可靠性承诺。有了明确步骤和回滚预案,出问题时至少不会慌。我个人的体会是,容器技术本身并不复杂,复杂的是在真实场景里做取舍,而取舍的底气,就来自你对 Pod 原理和 Rancher 这类工具的熟悉程度。