Vue2+SpringBoot+Nacos上K8s:StatefulSet部署与避坑实录

📅 发布时间:2026/10/20 7:03:21
Vue2+SpringBoot+Nacos上K8s:StatefulSet部署与避坑实录
简介面向在阿里云Kubernetes中搭建前后端分离应用的开发与运维人员这份资源提供了Vue2NginxSpring Boot 2.5Nacos 2.0.3的整套部署参考。包内共16个文件核心为8个YAML编排文件另含2个Shell部署脚本、2个Dockerfile、1个SQL初始化脚本、1个Nginx配置、1个可执行Jar包和1个前端静态资源压缩包整体约34.69MB各模块按组件分目录存放便于快速定位。已有1341人参与学习下载。方案覆盖Nacos有状态集群的StatefulSet部署、MySQL数据初始化、前端Vue项目镜像构建、后端Spring Boot服务接入以及Ingress统一网关配置其中deploy.sh可串联镜像构建与资源下发nacos-mysqlConfig.yaml等文件细化了配置中心所需的数据库准备。通过这套资源读者能直接获得从容器镜像到K8s资源调度的完整链路减少在云上逐个调试YAML和Deployment的时间成本适合作为K8s落地项目的初始模板或排错参考也便于按业务需求二次扩展。1. 把 Vue2 SpringBoot 2.5 Nacos 2.0.3 搬上托管 K8s这份资源解决的不是部署命令而是微服务的整合链路让一套老技术栈完整落到 K8s 上真正的难点从来不是单个组件怎么打镜像。Vue2 前端、Nginx、SpringBoot 2.5、Nacos 2.0.3四个组件单独看都有成熟方案一旦放进同一个集群就开始互相纠缠Nacos 节点在 Pod 里找不到彼此、SpringBoot 注册地址错乱、前端刷新路由白屏。这份资源就是围绕这条完整链路整理的——前端多阶段构建、Nacos 集群的 StatefulSet 编排、SpringBoot 配置注入、Ingress 与 Nginx 路由规则外加我在某云厂商托管容器服务上实测踩过的坑。适合已经会用 kubectl 基础操作、但第一次做微服务容器化的后端或运维/DevOps 开发照着能一条链路跑通遇到问题也知道去哪找原因。2. 镜像仓库与集群规划动手写 YAML 前先把边界定死2.1 命名空间与镜像仓库环境隔离的第一道闸我接手的这类项目第一件事从来不是写 deployment而是先把命名空间和镜像仓库的边界定义清楚否则后面每次发布都在和「配错环境」「拉错镜像」作斗争。集群侧我会按环境拆三套命名空间dev、staging、prod。每套环境的 Nacos、SpringBoot、前端都在各自命名空间里跑资源配额按环境单独设置staging 里有人压测不会抢走生产环境的 CPU。很多团队用一套命名空间加 label 区分环境省事是省事但遇到 NetworkPolicy 和配额调优时就很别扭——label 拦不住资源竞争。镜像仓库侧同理仓库路径按「项目名/组件名」组织tag 带上明确的版本标识。我习惯的规则是dev 环境用 latest 或日期 tagstaging 和 prod 用三段式版本号。这样生产部署前必须显式指定版本杜绝「全环境都拉 latest」的隐患。登录、构建、推送的完整命令如下docker login --username某账号 registry.example.com docker build -t registry.example.com/模拟项目X/vue2-web:1.0.0 ./frontend docker push registry.example.com/模拟项目X/vue2-web:1.0.0 docker build -t registry.example.com/模拟项目X/springboot-api:1.0.0 ./backend docker push registry.example.com/模拟项目X/springboot-api:1.0.0tag 里的三段版本号逻辑是主版本对应架构或接口变更次版本对应功能迭代构建号对应每次构建递增。构建号要能和 CI 流水线的运行序号对应上出问题后才能直接定位「是哪一次构建的产物」比时间戳精确得多。这里的registry.example.com是占位符实际换成你所在云厂商的镜像仓库地址即可。另外镜像仓库的访问凭证建议单独配置不要直接写死在 CI 脚本里。K8s 侧用 ImagePullSecret 引用仓库凭证部署时 Pod 才能拉取私有镜像。这一步漏掉Pod 会一直停在 ImagePullBackOff日志只说拉取失败不细看你根本想不到是凭证问题。2.2 Vue2 前端镜像多阶段构建与控制 50MB 以内前端镜像最常见的问题是直接在 node 基础镜像里跑 Nginx构建工具、依赖、源码全堆在一起镜像体积轻松超过 1GB拉取慢、启动也慢。正确做法是构建阶段和运行阶段分开构建阶段用 node 环境出 dist 产物运行阶段只留 Nginx 加静态文件。# 阶段一构建 Vue2 产物 FROM node:16-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install --registryhttps://registry.npmmirror.com COPY . . RUN npm run build # 阶段二Nginx 运行时 FROM nginx:1.23-alpine COPY --frombuild /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80AS build给第一阶段起了名字第二阶段的COPY --frombuild只把/app/dist目录拷过来。node 镜像、node_modules、源码在最终镜像里全部不存在体积实测能压到 50MB 以内。npm install 用的是国内镜像源首次构建时间能从十分钟级别降到两三分钟。这个 Dockerfile 有两个翻车点。第一COPY package*.json和COPY . .分两步写是为了利用 Docker 的层缓存依赖没变时npm install 那一层直接命中缓存不用每次重装。提示层缓存的前提是 package.json 和 package-lock.json 没有变化。依赖一旦变更npm install 那层必然会重新执行这是 Docker 缓存的正常机制不是 bug。第二如果你在前端构建脚本里读取环境变量要记住 Vue2 的 .env 是构建期读取的运行时传给容器的环境变量进不了打包后的 JS。这个问题我在第五章踩坑实录里单独讲。2.3 SpringBoot 2.5 后端镜像JDK 基础镜像与 jar 包拷贝SpringBoot 镜像相对简单核心是把构建好的 jar 包放进 JDK 镜像里跑。这里有个选型细节SpringBoot 2.5 默认支持 Java 8 和 Java 11我用 openjdk:8 的 JRE 镜像而不是完整 JDK 镜像体积能小一半运行阶段不需要编译器JRE 足够。FROM openjdk:8u342-jre WORKDIR /app COPY target/模拟项目X-api.jar /app/api.jar ENV JAVA_OPTS-Xms512m -Xmx512m -XX:MaxMetaspaceSize256m ENV SPRING_PROFILES_ACTIVEprod EXPOSE 8080 ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar /app/api.jar]JAVA_OPTS和SPRING_PROFILES_ACTIVE都设成环境变量而不是写死是为了在 deployment 的 env 里按环境覆盖。SpringBoot 2.5 原生支持SPRING_PROFILES_ACTIVE环境变量对应 spring.profiles.active不需要在代码里做任何加工。最后一行ENTRYPOINT用sh -c包一层是因为${JAVA_OPTS}里的变量需要 Shell 展开。直接写ENTRYPOINT [java, -jar]这种 exec 形式运行期就没办法注入额外 JVM 参数了这也是很多人改了 JAVA_OPTS 却不生效的原因。3. Nacos 2.0.3 集群化部署为什么 StatefulSet 是必选项参数又该怎么定3.1 选型理由Deployment 跑 Nacos 的问题在哪如果你搜过 Nacos 的 K8s 部署方案会看到有人用 Deployment 跑单机 Nacos也有人用官方 Helm Chart 一把梭。单机模式适合本地联调生产环境用 Deployment 跑单机 Nacos 有两个致命问题一是 Pod 重建后数据丢失——内置 Derby 把数据写在容器文件系统里Pod 一删全没二是没有集群容错Nacos 挂了整个微服务调用链都跟着瘫。集群模式必须上 StatefulSet。原因在于 Nacos 节点之间需要稳定的网络标识StatefulSet 给每个 Pod 分配固定有序的 hostnamenacos-0、nacos-1、nacos-2配合 Headless Service每个节点都能用固定域名被其他节点和客户端找到。Deployment 的 Pod 名字带随机后缀每次重建就变一次集群节点互相注册时拿到的都是临时地址根本组不成稳定集群。3.2 Headless Service 与 StatefulSet核心 YAML 拆解先看 Service。Nacos 2.0.3 对外要暴露的端口不止 8848还有 9848 的 gRPC 端口这两个都是客户端通信的命脉。apiVersion: v1 kind: Service metadata: name: nacos-headless namespace: prod labels: app: nacos spec: type: ClusterIP clusterIP: None ports: - port: 8848 name: http - port: 9848 name: rpc - port: 9849 name: old-rpc selector: app: nacosclusterIP: None是 Headless Service 的标识。普通 Service 会分配一个虚拟 IP 做负载均衡Headless Service 不给虚拟 IPDNS 查询直接返回所有后端 Pod 的 IP 列表。Nacos 集群节点通过nacos-0.nacos-headless.prod.svc.cluster.local这类域名互相访问Pod 重建、IP 变化都不影响集群关系。端口方面8848 是控制台和 HTTP API9848 是 Nacos 2.0 的 gRPC 主通信端口9849 是兼容旧客户端的端口。客户端连接时写 8848服务端会自动协商到 9848但 Service、安全组、网络策略必须把 9848 一并放行少一个端口就会出现「控制台正常、客户端连不上」的怪现象这个坑在 5.4 节详细展开。再看 StatefulSet 的核心配置apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos namespace: prod spec: serviceName: nacos-headless replicas: 3 selector: matchLabels: app: nacos template: metadata: labels: app: nacos spec: containers: - name: nacos image: nacos/nacos-server:v2.0.3 ports: - containerPort: 8848 - containerPort: 9848 env: - name: NACOS_SERVERS value: nacos-0.nacos-headless.prod.svc.cluster.local:8848,nacos-1.nacos-headless.prod.svc.cluster.local:8848,nacos-2.nacos-headless.prod.svc.cluster.local:8848 - name: NACOS_REPLICAS value: 3 - name: SPRING_DATASOURCE_PLATFORM value: mysql - name: MYSQL_SERVICE_HOST value: mysql-svc.prod.svc.cluster.local - name: MYSQL_SERVICE_DB_NAME value: nacos_config - name: MYSQL_SERVICE_USER value: nacos - name: MYSQL_SERVICE_PASSWORD value: 改成实际密码 - name: JVM_XMS value: 1g - name: JVM_XMX value: 1g - name: JVM_XMN value: 512m volumeMounts: - name: data mountPath: /home/nacos/data volumeClaimTemplates: - metadata: name: data spec: accessModes: [ ReadWriteOnce ] resources: requests: storage: 20GiserviceName: nacos-headless把 StatefulSet 和前面的 Headless Service 绑定这是 Pod 能拿到稳定域名的前提。NACOS_SERVERS是集群节点发现的关键参数三个节点的地址完全由 Headless Service 的 DNS 域名组成而不是 Pod IP。当某个节点重建后K8s 会重新分配 IP但域名不变Nacos 集群能自动恢复。SPRING_DATASOURCE_PLATFORMmysql和 MYSQL 系列变量告诉 Nacos 把配置数据存到外部 MySQL 而不是内置 Derby。注意集群模式下 Derby 不支持多节点共享数据外部 MySQL 是生产环境硬性要求不是可选优化。不配的话三个节点各自维护一份数据配置写进去互相看不见。volumeClaimTemplates给每个 Pod 自动创建独立的 PVC这是 StatefulSet 和 Deployment 在存储上的本质区别。storageClassName 我用了云厂商的 SSD 类型PV 和 PVC 由平台自动绑定。Nacos 的 home 目录挂了 20Gi实际用不了这么多但预留多点总比扩容时停服务强。3.3 MySQL 初始化Nacos 2.0.3 集群模式的隐藏前置条件接上文这里有个特别容易漏的环节Nacos 启动之前MySQL 里必须已经有 Nacos 的表结构。官方镜像不自带初始化脚本直接启动会看到日志里一堆「表不存在」的报错。常见做法是先把 Nacos 官方包里的nacos-mysql.sql导入目标库mysql -h mysql-svc -u nacos -p nacos_config nacos-mysql.sql导入后确认几张关键表存在比如config_info、config_info_aggr、his_config_info这些是配置中心最核心的数据表。我见过有人跳过这步直接起集群日志里全是报错排查了半天以为是端口或者内存问题。另外MySQL 字符集要设成 utf8mb4配置内容里一旦有特殊字符默认字符集会直接写入失败。这步做好Nacos 集群启动才会干净。4. SpringBoot 2.5 与前端 Nginx 的部署编排配置注入和路由回退是重头戏4.1 版本搭配Spring Cloud Alibaba 与 SpringBoot 2.5 怎么对齐SpringBoot 2.5 跑在 Nacos 2.0.3 上中间隔着一层 Spring Cloud Alibaba。版本不对是启动阶段最隐蔽的问题。我在模拟项目X里用的是 Spring Cloud Alibaba 2021.0.x 对应的 starter在 pom.xml 里固定版本dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId version2021.1/version /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId version2021.1/version /dependency这套组合对应 Nacos 客户端 1.x 的通信能力和 Nacos 服务端 2.0.3 实测稳定。如果换成更新的版本线客户端和服务端的 gRPC 兼容性会有细微差异表现是「服务注册成功但配置中心拉取超时」。这种问题最磨人它不直接报错只是行为异常日志看起来一切都是好的。建议在 pom 里显式写版本而不是继承 Spring Cloud Alibaba 的 BOM。显式版本的好处是排查时能一眼确定客户端版本BOM 隐式管理会让依赖树变得难追踪。SpringBoot 2.5 的父 POM 不动单独引入这两个 starter 就行两者不会冲突。4.2 Deployment 环境变量把 Nacos 地址和 Pod IP 传进去SpringBoot 服务的 Deployment核心不是镜像是环境变量。Nacos 地址、环境 profile、JVM 参数全部通过 env 注入配套的 Service 负责让前端 Nginx 和微服务之间互相解析。apiVersion: v1 kind: Service metadata: name: springboot-api namespace: prod spec: selector: app: springboot-api ports: - port: 8080 targetPort: 8080 --- apiVersion: apps/v1 kind: Deployment metadata: name: springboot-api namespace: prod spec: replicas: 2 selector: matchLabels: app: springboot-api template: metadata: labels: app: springboot-api spec: containers: - name: api image: registry.example.com/模拟项目X/springboot-api:1.0.0 ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: prod - name: SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR value: nacos-headless.prod.svc.cluster.local:8848 - name: SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR value: nacos-headless.prod.svc.cluster.local:8848 - name: JAVA_OPTS value: -Xms512m -Xmx512m -XX:MaxMetaspaceSize256mSPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR和SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR是 Spring Cloud Alibaba 自动读取的标准环境变量分别对应注册中心和配置中心地址。这里填的是 Headless Service 的集群内域名不是某个 Nacos Pod 的 IP。域名解析由 K8s 内部 DNS 负责任何时刻都能解析到存活的 Nacos 节点比写死 IP 稳妥得多。Service 和 Deployment 写在同一个 YAML 里用---分隔kubectl apply一次就能两件资源都建出来。Service 的 selector 只按app: springboot-api选后端 Pod集群内其他服务访问springboot-api.prod.svc.cluster.local:8080就能打到后端的两个副本上。如果服务之间需要互相调用注册进 Nacos 的地址必须是调用方可达的地址。默认情况下Spring Cloud Alibaba 用 Pod 的容器 IP 注册同一个命名空间内没问题跨命名空间调用就得用 Service 域名或者额外配置。我一般让一组微服务待在同一命名空间内互相访问从源头规避掉大部分网络平面问题。4.3 Vue2 history 路由回退Nginx try_files 与接口代理前端部署到 K8s 时Nginx 配置里最关键的是try_files那一行。Vue2 开了 history 模式路由后浏览器地址栏里是真实 URL但静态目录里没有对应文件Nginx 默认行为是找文件、失败、返回 404。刷新二级页面白屏就是这里来的。Nginx 配置长这样server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://springboot-api.prod.svc.cluster.local:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html的含义是按顺序尝试先找真实文件$uri找不到找目录$uri/还找不到就回退到/index.html路由交给 Vue Router 接管。这行配置解决了 history 模式的刷新 404。代价是「路径不存在也返回 200」这对前端单页应用是期望行为但后端接口路径不要被这层接住所以接口单独放在 location /api/ 下。proxy_pass指向 SpringBoot 的 K8s Service 域名。这里有个细节proxy_pass 末尾带/api/时转发保留完整路径后端 Controller 的 RequestMapping 也要带 /api 前缀如果改成http://springboot-api.prod.svc.cluster.local:8080/会把/api前缀剥掉再转发。两种写法对应两种后端设计经常见到前后端约定不一致导致 404先和后端确认接口路径再定这个配置。这个 nginx.conf 要在构建镜像时一并打进去也就是 2.2 节 Dockerfile 里的COPY nginx.conf /etc/nginx/conf.d/default.conf。改了配置必须重新构建并推送镜像光改本地文件不重新打包等于白改。5. 避坑实录Nacos 反复重启、注册地址错乱、前端 404、gRPC 断连5.1 Nacos 节点反复 CrashLoopBackOff现象Nacos 三个 Pod 全部进入 CrashLoopBackOffkubectl logs里看到 Derby 数据库文件锁冲突或者 JVM 内存溢出的 OutOfMemoryError。原因最常见的是没接 MySQL。Nacos 2.0.3 集群模式强制要求数据持久化到共享存储内置 Derby 只允许单节点访问多节点同时操作就会互相锁文件。另一个常见原因是 JVM 堆给太小Nacos 2.0 的 gRPC 服务在启动阶段会额外占用堆外内存512M 堆在小规格节点上很容易触发 OOM。解决先确认环境变量里SPRING_DATASOURCE_PLATFORMmysql和 MySQL 地址是否已配置没配就补上并把 nacos-mysql.sql 初始化脚本导入目标库。内存方面把 JVM_XMS 和 JVM_XMX 至少调到 1g同时保证 Pod 的resources.limits.memory大于「堆 元空间 堆外」的总量一般留 50% 余量堆设满的话 Pod 会被整体 OOMKilled。5.2 SpringBoot 服务注册到 Nacos 的 IP 不对现象Nacos 控制台能看到服务列表但服务间调用报 Connection timed out日志里显示注册 IP 和实际访问目标对不上。原因Spring Cloud Alibaba 默认用 Pod 的容器 IP 做注册地址。如果微服务分布在多个命名空间或者集群启用了 NetworkPolicyPod IP 之间不一定互通。还有一种情况是节点有多块网卡自动探测拿到了错误的网卡地址。解决在 application.yml 里显式指定注册 IP配合 downward API 注入 Pod IPspring: cloud: nacos: discovery: server-addr: ${SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR} ip: ${MY_POD_IP}Deployment 里补上下面的 envPod IP 就会注入到环境变量env: - name: MY_POD_IP valueFrom: fieldRef: fieldPath: status.podIPfieldRef.fieldPath: status.podIP是 K8s 标准做法把 Pod 自己的 IP 写进环境变量。指定 ip 之后注册到 Nacos 的地址一定是服务可达的。如果你用的是 Calico 这类网络插件Pod IP 在集群内本来就互通这个问题基本不会出现跨命名空间访问时还是要靠 Service 域名而不是裸 Pod IP。5.3 Vue2 前端刷新页面 404现象用户从首页跳转到子页面正常按 F5 刷新就白屏控制台显示 404。原因Nginx 没配try_files拿着 URL 路径去静态目录找文件找不到就返回 404而 Vue2 的 history 模式路由根本没有对应的真实文件。解决在 Nginx 的 location / 里加上try_files $uri $uri/ /index.html;完整写法见 4.3 节。注意别手抖写成try_files $uri $uri/ index.html;也不能漏掉空格Nginx 对路径解析非常敏感。改完配置记得重新构建镜像并推送我见过有人本地改了配置忘了重新打包排查半天才发现 Pod 里跑的还是旧镜像。5.4 Nacos 2.0 的 gRPC 端口被挡现象Nacos 控制台能打开SpringBoot 也显示注册成功但客户端日志里反复出现 gRPC 连接失败的报错配置中心的配置始终拉不下来。原因Nacos 2.0 的客户端通信走 9848 端口的 gRPC不是 8848 的 HTTP。只放行 8848 的话客户端无法建立长连接。更隐蔽的是Nacos 服务端的 gRPC 端口会基于 8848 自动偏移计算如果 8848 被别的进程占用导致服务端实际监听端口偏移9848 也会跟着偏移客户端用默认端口自然连不上。解决把 8848、9848、9849 三个端口在 Service、负载均衡、安全组三层全部放行。排查时在 Nacos Pod 里执行ss -lntp看服务端实际监听的端口是不是 9848别只信文档默认值。客户端配置仍然写 8848服务端会协商到实际 gRPC 端口只要 9848 可达就行。6. 验证与调优三条命令加一组资源参数把部署闭环6.1 三条命令验证完整链路部署顺序建议是 Nacos → SpringBoot → 前端每层验证通过再往下走。kubectl get pods -n prod -o wide kubectl logs -f nacos-0 -n prod | tail -50 kubectl exec -it springboot-api-任意Pod名 -n prod -- curl http://nacos-headless.prod.svc.cluster.local:8848/nacos/v1/ns/service/list?pageNo1pageSize100第一条看所有 Pod 的状态和重启次数第二条确认 Nacos 节点日志里出现集群节点互相感知的记录第三条从 SpringBoot Pod 内部请求 Nacos 的服务列表接口验证注册链路真正打通——这一步能确认服务发现是通的而不是只在控制台上看着正常。6.2 资源限额request 和 limit 必须分开设稳定运行之后重点是资源配额。只写 request 不写 limitCPU 会被无限制抢占流量一上来 Nacos 心跳超时服务被误判为不健康。我最终的配置习惯是resources: requests: cpu: 500m memory: 1Gi limits: cpu: 2 memory: 2GiJVM 堆要小于 Pod 内存 limit并留出元空间和堆外内存的余量——limit 2Gi 时堆最大给到 1.5G。另外Nginx 的 worker_processes 默认是 auto在 K8s 里可能拿到宿主机一样的核数内存占用被放大建议在 nginx.conf 里固定写成worker_processes 2;。从那以后我每次在托管 K8s 上部署一套新的微服务都会强制走一遍同一个收尾动作先看 Nacos 三个 Pod 是不是 Ready再进 SpringBoot Pod 里 curl 一次服务列表最后在前端页面刷新三次确认路由没有 404。这套动作五分钟能定位八成部署问题。希望帮到你。本文还有配套的精品资源点击获取