尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

K8s 部署 SpringBoot + Vue 微服务:容器化到排障实战

K8s 部署 SpringBoot + Vue 微服务:容器化到排障实战 这套 SpringBoot Vue 的前后端分离项目从最早的单机 docker-compose 挪到 K8s 集群上我前后折腾了差不多两个月。中间经历过镜像拉不下来、Pod 起来又挂、前端 Nginx 转发 502、跨服务调用超时这一连串问题也亲眼见过同事因为探针配错导致整个服务被反复重启。所以这篇不想写成一份冷冰冰的官方文档翻译而是想把这套K8s 部署 SpringBoot Vue 微服务的完整路径从架构选型、镜像打包、清单编写到上线排障按我自己踩过的顺序讲一遍。适合已经在写 SpringBoot 和 Vue、但对 K8s 编排还停留在kubectl get pods阶段的开发者也适合准备把现有项目容器化上云的团队做一次对照参考。读完之后你至少能独立把一套前后端项目跑在集群里并且知道出问题时该往哪儿查。1. 整体架构设计与部署思路拆解1.1 为什么微服务最终要落到 K8s 上先聊个很多人没想清楚的问题项目本来用 docker-compose 跑得好好的为什么要换成 K8s我刚开始也犹豫过直到服务数量涨到七八个、每天要发好几次版本才发现 docker-compose 的短板特别明显。它适合单机编排但没有自愈能力容器挂了得靠人手动restart没有滚动更新发版基本等于短暂停机服务发现还得靠容器名硬编码扩缩容以后 IP 一变配置全废。K8s 解决的恰恰是这几件事。它把每个 SpringBoot 服务抽象成一个 Deployment副本数、探针、资源限制都写进声明式清单集群会持续把实际状态往你声明的状态上拉。某个 Pod 挂了ReplicaSet 立刻拉起新的要发新版本rolling update 会一个一个替换配合 readiness 探针把流量切过去实现准不停服。这里要区分清楚 K8s 和 Docker 的关系Docker 是容器运行时和镜像格式负责把应用和依赖打包成一个可运行单元K8s 是编排层负责把这些单元调度到哪台机器、怎么联网、怎么扩缩容、怎么自愈。两者不是替代关系是上下层配合。对微服务来说真正值钱的是 K8s 的服务发现和负载均衡。每个服务建一个 Service集群内部用服务名.命名空间.svc.cluster.local就能访问Pod 扩到几个、IP 怎么变调用方都不用改配置。这套机制配合 SpringCloud 或直接 RestTemplate/WebClient 调用都很顺。所以我的结论是服务超过三个、需要频繁发布、需要弹性扩缩的时候上 K8s 的收益就明显盖过学习成本了。1.2 前后端分离场景下的部署边界划分SpringBoot Vue 这种前后端分离项目上 K8s第一件要想明白的事是哪些东西该进集群哪些不该。我的划分是这样的后端 SpringBoot 每个微服务一个 Deployment 加一个 Service前端 Vue 打包成静态文件后用 Nginx 镜像托管同样是一个 Deployment 加一个 Service对外暴露统一走 Ingress。数据库、Redis 这类有状态服务测试环境可以也放进集群但生产环境我建议优先用云上的托管实例理由后面细说。为什么不把前端和后端塞进同一个 Pod因为它们的生命周期和扩缩容需求不一样。后端受 CPU 和并发影响大可能要多副本前端 Nginx 基本只吃内存副本需求小。混在一起会互相拖累扩副本时浪费资源更新时还得整体重建。分开部署以后各自独立滚动边界清晰。还有一层边界是网关放哪。如果项目本来就有 SpringCloud Gateway那它可以继续作为集群内的一个服务Ingress 只负责把外部流量导到网关网关再做业务路由。如果没有网关也可以直接用 Ingress 做路径路由比如/api转发到后端 Service/转发到前端 Nginx。这个选择后面在 Ingress 章节会展开讲。1.3 命名空间与环境规划命名空间Namespace是很多人会忽略但特别省事的一个设计。我一般按环境划分dev、test、prod三个命名空间同一套清单通过kubectl apply -n指定不同环境镜像 tag 和配置用不同值区分。这样带来的好处是资源隔离、RBAC 权限好控制名称也不会互相冲突——比如dev和prod可以各有一个叫user-service的 Deployment。命名规范我也踩过坑。早期我图省事用user、order这种短名后来服务一多就分不清哪个是网关、哪个是聚合层。现在统一用业务域-服务类型user-service、order-api、web-frontend。Service 名和 Deployment 名保持一致后面 Ingress 和调用方引用的时候不容易搞混。标签label也要一开始就定好至少带上app、tier、version三个app用来做 Service 选择器tier区分前后端和数据库version给滚动更新和灰度用。这些标签在第一版清单里写好后面排查问题时kubectl get pods -l appuser-service一把就能筛出来效率差很多。2. 容器化改造把 SpringBoot 和 Vue 分别打包成镜像2.1 SpringBoot 后端的 Dockerfile 设计与多阶段构建后端镜像是最容易做重的地方。我见过不少团队打出来的 SpringBoot 镜像 800M 起步推送到仓库要好几分钟节点拉取也慢。根因通常是两点基础镜像用了带完整 JDK 的操作系统镜像以及把 Maven 本地仓库、源码全塞进去了。我的做法是多阶段构建构建阶段用 Maven 拉依赖编译运行阶段只拷贝 jar 到一个精简的 JRE 基础镜像里。下面这份 Dockerfile 是我目前用得最稳的版本你可以直接抄# 构建阶段 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests -B # 运行阶段 FROM eclipse-temurin:17-jre-jammy WORKDIR /app COPY --frombuilder /build/target/*.jar app.jar ENV TZAsia/Shanghai EXPOSE 8080 ENTRYPOINT [java, -XX:MaxRAMPercentage75.0, -XX:UseG1GC, \ -Djava.security.egdfile:/dev/./urandom, -jar, app.jar]这里有几个点必须解释清楚。第一dependency:go-offline单独一层是为了利用 Docker 层缓存——只要pom.xml不变重新构建时依赖层不会重下构建速度能从几分钟降到几十秒。第二基础镜像选eclipse-temurin而不是openjdk是因为前者更新更活跃JRE 版本也齐全尤其要注意 SpringBoot 3.x 要求 JDK 17 起步用 JDK 8 的基础镜像会直接启动报UnsupportedClassVersionError这是SpringBoot 版本太高这类问题的典型表现。第三MaxRAMPercentage75.0是关键参数容器里 JVM 默认看不到 cgroup 的内存限制老版本容易把堆开到物理机内存导致 Pod 被 OOMKilled。设成 75% 是给堆外内存、线程栈、元空间留出余量。假设你给 Pod 配了limits.memory: 1Gi那堆上限大约就是 768Mi剩下的留给 JVM 自身开销实测下来这个比例比较稳。2.2 Vue 前端的构建与 Nginx 静态托管前端这条链路的核心思路是构建产出纯静态文件运行时用 Nginx 托管。Vue 本身在浏览器跑不需要 Node 运行环境所以容器里放一个 Nginx 加一堆 html/js/css 就够了。同样是多阶段构建# 构建阶段 FROM node:20-alpine AS builder WORKDIR /build COPY package*.json ./ RUN npm install --registryhttps://registry.npmmirror.com COPY . . RUN npm run build # 运行阶段 FROM nginx:1.25-alpine COPY --frombuilder /build/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80npm install单独一层也是缓存考虑package.json不变就不会重装。这里有个坑要提醒Vue 项目里如果用了import.meta.env.VITE_xxx这类环境变量它们是在构建时注入的不是运行时。也就是说你打包成镜像以后想改后端接口地址光改 K8s 的 ConfigMap 没用得重新构建镜像。想要运行时可变得靠 Nginx 反向代理把/api转发到后端 Service让浏览器请求同源的/api这样接口地址就跟环境无关了。配套的nginx.conf我一般这么写server { listen 80; location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://gateway-service:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files ... /index.html是给 Vue Router 的 history 模式兜底否则刷新子路由会 404这个坑几乎人人踩过。proxy_pass指向的是 K8s 内部的 Service 名注意如果目标是集群内 DNS得写全gateway-service.命名空间.svc.cluster.local或者依赖同命名空间下的短名解析。最后那个结尾的/也有讲究proxy_pass http://x/会替换掉/api前缀不带斜杠则保留路由对不对全看这一下实测时要拿浏览器 F12 看实际请求路径。2.3 镜像分层与体积优化镜像优化不是洁癖它直接影响部署效率。节点拉镜像慢滚动更新时新 Pod 的ContainerCreating就会卡很久看着像卡死。我的一般目标是把后端镜像压到 200M 以内、前端压到 50M 以内。手段有三个换 Alpine 或 slim 基础镜像、合并 RUN 减少层数、用.dockerignore排除node_modules、target、.git、日志等无关文件。MaxRAMPercentage、时区、编码这些都要在 ENTRYPOINT 里固化不要依赖运行时环境。特别提一下-Djava.security.egdfile:/dev/./urandom在容器里/dev/random熵不足时会导致启动变慢加上这个参数能明显改善冷启动。改完以后重新构建用docker images看体积再用dive工具看每一层多大找出没必要的层删掉。3. K8s 核心资源清单编写与参数调优3.1 Deployment 与探针配置的细节Deployment 是每个服务的骨架。我写清单时最关注三块副本数、资源限制、探针。资源这块必须配requests和limitsrequests决定调度器把它放到哪台节点limits决定它最多能用多少两者都别留空否则一个服务吃满内存会拖垮整台节点。探针则决定 K8s 怎么判断 Pod 的健康状态。apiVersion: apps/v1 kind: Deployment metadata: name: user-service labels: app: user-service tier: backend spec: replicas: 2 selector: matchLabels: app: user-service template: metadata: labels: app: user-service tier: backend spec: containers: - name: user-service image: registry.example.com/demo/user-service:1.0.0 ports: - containerPort: 8080 resources: requests: cpu: 250m memory: 512Mi limits: cpu: 1 memory: 1Gi readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 20 periodSeconds: 10 failureThreshold: 3 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 40 periodSeconds: 15 failureThreshold: 3探针这里必须区分清楚readiness决定要不要给它导流量失败时 Pod 从 Service 端点里摘掉但不重启liveness决定要不要重启它失败一定次数后 kubelet 会杀掉容器重建。SpringBoot 项目要暴露这两个端点得引入spring-boot-starter-actuator并且在配置里开启management: endpoint: health: probes: enabled: true endpoints: web: exposure: include: health,info,metricsinitialDelaySeconds一定要结合你服务的启动时间给够。SpringBoot 冷启动几秒钟到几十秒都有可能如果liveness的初始延迟比启动时间短就会出现服务还在启动探针已经判死容器被杀死重来的死循环日志里表现为容器反复重启、RESTARTS数一直涨。我一般先给一个偏大的值观察启动日志里 Started Application 的时间点再往下调。另外注意探针的路径别和业务鉴权冲突/actuator/health这类端点一般要放行别被你的 token 校验拦住否则探针永远失败。3.2 Service 与 Ingress 流量入口Service 负责集群内的稳定访问入口。后端每个服务建一个 ClusterIP 类型的 Service 就够了只有需要对外暴露时才考虑 NodePort 或直接走 Ingress。这里有个常见误区把数据库也做成 ClusterIP Service 然后跨命名空间硬编码访问结果命名空间一改全崩。我的建议是同命名空间内用短名访问跨命名空间一定写全限定名。apiVersion: v1 kind: Service metadata: name: user-service spec: selector: app: user-service ports: - port: 8080 targetPort: 8080 type: ClusterIPport是 Service 暴露的端口targetPort是容器实际监听的端口两者可以不一致但千万别写反否则 Service 通了却连不上。对外入口用 Ingress它会依赖一个 Ingress Controller常见的有 Nginx Ingress、Traefik没有 Controller 时 Ingress 资源只是静态配置不会真的生效。下面这个 Ingress 把前端和后端分开路由apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-ingress annotations: nginx.ingress.kubernetes.io/proxy-body-size: 20m spec: ingressClassName: nginx rules: - host: demo.example.com http: paths: - path: /api pathType: Prefix backend: service: name: gateway-service port: number: 8080 - path: / pathType: Prefix backend: service: name: web-frontend port: number: 80注意pathType用Prefix跟路径路由才如预期。proxy-body-size这个注解特别重要Nginx Ingress 默认请求体只有 1M文件上传一超过就 413别等上线才发现。3.3 ConfigMap 与 Secret 管理配置配置千万别硬编码进镜像。SpringBoot 的application.yml在镜像里作为默认值就行环境相关的数据库地址、Redis 地址、第三方密钥都通过 ConfigMap 和 Secret 注入。这样同一镜像能在多环境复用改配置不用重新构建。常用做法是挂成环境变量或者挂成文件让 SpringBoot 用spring.config.additional-location加载。apiVersion: v1 kind: ConfigMap metadata: name: user-service-config data: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql.prod.svc.cluster.local REDIS_HOST: redis.prod.svc.cluster.local --- apiVersion: v1 kind: Secret metadata: name: user-service-secret type: Opaque stringData: DB_PASSWORD: your-passwordSecret 默认只是 base64 编码不是加密别把它当成保险箱。生产环境要配合 RBAC 限制谁能读 Secret敏感项目还可以接 KMS 或外部密钥管理。环境变量注入以后SpringBoot 里用${DB_HOST}就能读到。注意配置变更后环境变量方式不会自动生效得kubectl rollout restart deployment/user-service触发滚动重建挂载成文件的方式则有延迟同步看具体挂载策略。4. 实操部署从零到服务跑起来4.1 集群准备与镜像推送假设你已经有一个可用的 K8s 集群单节点用 k3s、minikube 也够练手正式环境至少三节点第一步是准备好能访问的镜像仓库。私有仓库要在集群里配置imagePullSecrets否则节点拉镜像时 401。我一般先在本地或 CI 里构建好镜像再推送到仓库docker build -t registry.example.com/demo/user-service:1.0.0 . docker push registry.example.com/demo/user-service:1.0.0镜像 tag 千万别用latest。latest配合imagePullPolicy: Always会让每次重建都去拉同一标签滚动更新时新旧镜像看起来一样排查起来很痛苦。用语义化版本或 Git commit hash 做 tag回滚时kubectl rollout undo才有明确的目标。如果集群节点拉不到外网记得配好镜像仓库的地址和凭据把imagePullSecrets加到 Deployment 的 spec 里。4.2 按顺序 apply 资源清单资源的应用有个合理顺序先建命名空间再建 ConfigMap/Secret然后建 Deployment 和 Service最后建 Ingress。顺序反了不一定报错但排查时容易乱。我习惯把每类资源分成独立文件用kubectl apply -f批量执行kubectl create namespace prod kubectl apply -f config/ -n prod kubectl apply -f deployments/ -n prod kubectl apply -f services/ -n prod kubectl apply -f ingress/ -n prod应用完以后逐个确认状态。我的检查顺序是先kubectl get pods -n prod看是否 Running、RESTARTS是否为 0再kubectl describe pod看有没有异常事件然后kubectl logs看应用日志有没有起来接着kubectl get svc确认 ClusterIP 分配正常最后从集群内部或浏览器访问域名验证链路。滚动更新的状态可以用kubectl rollout status deployment/user-service -n prod盯卡住时它会告诉你卡在哪一步。4.3 联调与验证全部 Pod 起来不代表服务能用。真正的验证要打通这条链路外部请求 - Ingress - 前端 Nginx - 后端网关 - 具体微服务 - 数据库/Redis。我的做法是从里往外逐段测。先在集群内起个临时 Pod用curl直接请求后端 Servicekubectl run test-pod --rm -it --imagecurlimages/curl -n prod -- sh curl http://user-service:8080/actuator/health这一步通了说明 Service 到 Pod 的链路没问题。再测网关能否转发到后端最后从公网域名访问前端看接口请求是否 200。如果前端页面出来了但接口 502问题多半在 Nginx 转发地址或网关路由如果接口 401/403往鉴权配置上查。数据库连通性可以用同样的临时 Pod 测一下端口避免把网络问题和配置问题混在一起。5. 常见问题排查与避坑实录5.1 镜像与启动阶段的典型故障镜像拉取失败是新手最常见的第一道坎。kubectl describe pod里会显示Failed to pull image或ImagePullBackOff原因通常是仓库地址写错、镜像不存在、私有仓库没配凭据、或者节点网络访问不到仓库。逐项对照事件里的报错就能定位。另一个高频问题是CrashLoopBackOff容器起来了又退出这时不要急着改代码先kubectl logs --previous看上一次崩溃的日志往往是配置文件读不到、数据库连不上、或者端口被占用。还有一个特别隐蔽的坑是 OOMKilled。Pod 状态显示OOMKilled的时候说明容器内存超了limits。要么调大 limits要么回头检查 JVM 参数是不是没限制堆大小。Java 服务在容器里如果不设MaxRAMPercentage很容易吃满节点内存。我在实践里会先看kubectl top pod的实际内存占用再决定是调参数还是调 limits别盲目往上加。5.2 服务间调用与网络排查微服务之间调用失败排查顺序我一般从 DNS 开始。集群内首先要确认服务名解析对不对nslookup user-service.prod.svc.cluster.local或者在临时 Pod 里直接curl。如果 DNS 解析不了检查 Service 是否存在、命名空间是否对得上。解析通了但连接被拒看目标 Pod 的端口是不是targetPort写错了或者应用根本没监听在那个端口。跨命名空间调用时尤其容易出事短名只在同命名空间内有效。我吃过一次亏prod命名空间的服务去调test命名空间的服务只写了短名解析到了一个不存在的地址报的是连接超时而不是 DNS 错误查了很久。后来统一规定跨命名空间必须写全限定名问题再没出现过。另外 NetworkPolicy 如果开了也要确认规则允许了相应的流量默认拒绝的策略会静默拦截日志里什么都有就是连不通。5.3 前端联调与网关路由的坑前端这类问题我总结下来就三种静态资源 404、接口 404、接口跨域。静态资源 404 通常是 Nginx 的 root 路径不对或者构建产物目录拷错了Vue Router history 模式下刷新 404 一定要try_files兜底。接口 404 多半是 Ingress 路径和后端路由不匹配比如 Ingress 配了/api前缀但网关没做前缀剥离请求就带着/api到了后端自然对不上。前缀剥离在 Ingress 和 Nginx 里各有各的写法必须实测确认。跨域问题要优先考虑同源思路即让前端请求自己的域名下的/api由 Nginx 或 Ingress 反代到后端这样浏览器根本不触发跨域。如果绕不开跨域后端要在网关或每个服务上配 CORS注意Access-Control-Allow-Origin不能用*同时带credentials浏览器会拒绝。这个细节我在联调时踩过报的错误信息很误导实际是配置组合不合法。5.4 常见问题速查表现象常见原因排查手段ImagePullBackOff仓库地址错、无凭据、网络不通kubectl describe pod看事件CrashLoopBackOff配置缺失、DB 连不上、端口占用kubectl logs --previousOOMKilled堆未限制、limits 太小kubectl top pod、查 JVM 参数Pod Running 但不通探针、targetPort、SecurityGroup临时 Pod 内 curl 目标 Service接口 502上游未就绪、Nginx 地址错看 Ingress 和 Nginx 日志接口 404路径前缀不匹配对比 Ingress path 与后端路由刷新页面 404history 模式未兜底加try_files ... /index.html上传 413请求体超限调proxy-body-size注解跨域被拦CORS 配置组合错误F12 看响应头与预检请求DNS 解析失败跨命名空间用了短名改用全限定名提示排查时养成从里往外的习惯先确认 Pod 自身健康再确认 Service 到 Pod最后确认 Ingress 到 Service。逐层收敛比在一堆日志里乱翻快得多。6. 上线之后可观测性与资源治理服务跑起来只是开始真正让团队睡得着觉的是可观测性。我一般三件套一起上日志集中收集、指标监控、链路追踪。日志用kubectl logs只能看单个 Pod多副本时根本追不上生产环境建议接一套日志系统把所有 Pod 的 stdout 收走按app标签分类检索。指标这块SpringBoot Actuator 已经暴露了 Prometheus 格式的端点集群里部署 Prometheus 和 Grafana就能看到每个服务的 QPS、延迟、JVM 内存、GC 情况出问题时不再靠猜。资源治理是另一个容易被忽视的点。我见过不少人给服务配了requests和limits之后就不管了结果长期浪费或者频繁被限流。建议每月做一次资源审视用kubectl top和监控面板看实际使用率把requests调到略高于日常峰值的水平limits给足安全边界。CPU 是压缩资源超了会被限速但不会杀内存是非压缩资源超了直接 OOM。搞清楚这个区别配置时心里就有底了。压测这块也顺带提一句。上线前用 JMeter 之类的工具对核心接口做一轮高并发验证观察在目标并发下 Pod 是否自动扩容、响应时间是否达标、有没有触发限流。压测时记得把 HPA自动扩缩容打开基于 CPU 或自定义指标扩副本这样容量验证才有意义。压测数据最好和生产环境隔离别在生产集群上打容易被误判为攻击。最后分享一个小习惯把每次部署用到的镜像 tag、清单变更、配置变更记录在案配合kubectl rollout history随时能回滚。K8s 的回滚是声明式的只要你不删历史版本一条命令就能退回去这是比传统部署省心太多的地方。真正上线过几轮之后你会发现把清单写规范、把探针配对、把配置外置这三件事做到位后面 90% 的问题都能提前躲开。
返回列表