☰
K8S实战:Vue+SpringBoot前后端分离项目部署全解析
2026/10/19 6:01:37 网站建设 项目流程

K8S部署vue、springboot前后端分离项目

1. 整体架构梳理与方案权衡

手上这套前后端分离项目,前端用vue,后端用springboot,在一台机器上直接跑其实挺顺的,但一旦涉及多环境发布、流量增长、版本回滚这些事,单机部署就开始让人头疼了。我跟朋友搭这套K8S环境的时候,最大的感受就是:K8S解决的不是“能不能跑起来”的问题,而是“能不能优雅地持续跑下去”的问题。

先说为什么选K8S而不是直接用docker compose或者干脆上云主机。docker compose当然简单,一条命令把所有容器拉起来,但它的编排能力停留在“单机多容器”这个层面。你要做灰度发布、根据负载自动扩缩容、节点挂了自动调度,这些compose都给不了。而K8S天生就是把“整个集群当一台电脑”来用,你只管声明想要的状态,至于它怎么把Pod调度到哪个节点、怎么重启失败的容器、怎么分配资源,全是它自己操心。做前后端分离项目,最典型的需求就是前端静态资源和后端接口服务可以独立扩容,比如双十一期间接口压力大,我只需要把后端副本数从2调成5,前端保持不动,这在K8S里就是一条命令的事。

架构设计上,我一般把整个项目拆成三个核心部分:前端vue应用、后端springboot应用、中间件依赖。中间件这块通常包括MySQL和Redis,但开发和生产的处理方式完全不同。开发环境我倾向于把MySQL、Redis也容器化,一条docker-compose就能搞定;生产环境则更推荐使用云厂商提供的托管数据库和缓存服务,稳定性有保证不说,备份、监控都省心。当然,如果是内网环境必须自建,那数据库这类有状态服务就不建议放在K8S里管了,用单独的虚拟机部署会更稳,因为K8S对有状态服务的支持虽然一直在进步,但运维成本确实比无状态应用高不少。

组件拆分这块要重点说。前后端分离项目在K8S里部署,最忌讳的就是“一个大Pod塞所有东西”。我见过有人把nginx、前端静态文件、后端jar包、甚至数据库客户端全塞进一个Pod里,这种做法完全失去了K8S的优势。正确的做法是前端一个Deployment,后端一个Deployment,各自管理生命周期,通过Service进行服务发现和负载均衡。前端的Pod里只跑nginx,负责托管静态资源并反向代理API请求;后端的Pod里只跑java进程,通过环境变量或配置文件读取数据库地址。这样设计的好处非常明显:前端静态资源更新了,只需要重新构建前端镜像滚动升级,后端服务完全不受影响;后端某个接口出现内存泄漏导致OOM,也只会杀死后端Pod,前端用户无感知。

网络模型也值得提前想清楚。K8S集群内部有一套自己的DNS和服务发现机制,前端Pod要通过Service名访问后端,比如http://backend-service:8080,而不是通过IP或宿主机端口。很多人第一次部署时习惯用IP直连,结果Pod一重启IP就变了,整个链路就断了。所以从一开始就要养成“内部调用走Service名”的习惯。

2. 容器化前的镜像构建细节

2.1 前端镜像的多阶段构建

vue项目打包成镜像,核心痛点在于“构建环境”和“运行环境”的分离。vue的构建需要Node.js环境,跑npm install和npm run build会生成大量依赖和中间产物,但最终线上运行只需要dist目录里的静态文件。如果这些构建工具链、node_modules全塞进最终镜像里,镜像体积会膨胀到好几个G,拉取和启动都会变慢。

多阶段构建是业界标准解法。第一阶段用node镜像作为构建环境,执行npm安装和打包;第二阶段用nginx镜像作为运行环境,只把dist目录拷贝进来。话不多说,直接看一个我实战验证过的Dockerfile:

# 第一阶段:构建 FROM node:16-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install --registry=https://registry.npmmirror.com COPY . . RUN npm run build # 第二阶段:运行 FROM nginx:1.24-alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

有几个细节值得注意。npm install建议锁定镜像源,国内网络环境下用默认源经常卡在安装这一步,我在Dockerfile里直接指定了国内镜像源,构建速度提升非常明显。依赖安装用的是npm install而不是npm ci,虽然npm ci更快且严格遵循lock文件,但如果你的package-lock.json和package.json偶发不一致,npm ci会直接报错中断构建,npm install会自动修复。纯CI环境我更推荐npm ci,但要确保lock文件是提交到代码仓库里的。

nginx.conf的配置也是前端容器化的关键。你需要同时处理静态资源托管和API反向代理两件事。一个比较标准的配置长这样:

server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; # 前端history路由需要try_files重写 location / { try_files $uri $uri/ /index.html; } # API反向代理到后端Service location /api/ { proxy_pass http://backend-service:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

很多人在这一步会踩坑:vue-router如果用的history模式,刷新页面就会出现404。原因很简单,nginx只按实际的静态文件路径去找资源,而前端路由是浏览器端的虚拟路径,比如/user/info这个路径在后端文件系统里根本不存在。try_files $uri $uri/ /index.html就是干这个事的,所有匹配不到的文件请求全部回退到index.html,由vue-router接管路由分发。

2.2 后端镜像的JVM参数与时区处理

springboot项目的镜像构建相对简单,因为它本身就是可执行的fat jar。但有几个细节不处理干净,上了生产就会出幺蛾子。

基础镜像的选择上,我实验过alpine和标准JDK镜像。alpine体积小,但有些底层依赖缺失,跑某些Java框架会出奇怪的问题。更稳妥的做法是直接用带瘦身功能的官方镜像,或者使用带jre的发行版。我后来固定用了eclipse-temurin:17-jre-alpine,体积和稳定性取得了不错的平衡。如果你对镜像体积非常敏感,也可以用openjdk:11-jre-slim之类的,但一定要先在测试环境充分验证。

时区问题是我踩过最隐蔽的坑。默认的JDK镜像时区是UTC,而业务数据要求的是北京时间,这会导致所有时间字段偏差8小时。尤其是财务对账、日志时间戳这种场景,差8小时就是事故了。解决办法是在Dockerfile里硬性设置:

FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY target/*.jar app.jar RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo 'Asia/Shanghai' >/etc/timezone EXPOSE 8080 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

JVM参数这块,我强烈建议不要在Dockerfile里写死,而是通过环境变量传入。因为同一个镜像可能要部署到开发、测试、生产多个环境,各个环境的堆内存配置不一样。比如开发环境只有1G内存限制,生产环境有4G,如果你在Dockerfile里写了-Xmx2g,开发环境就炸了。我在entrypoint里通过$JAVA_OPTS环境变量注入,K8S的Deployment编排时再针对不同环境配置不同的值。

还有一个国内团队容易忽略的点:Java进程在容器里的CPU和内存感知问题。旧版本的JVM在容器里跑,经常识别不到cgroup的资源限制,导致分配的内存超过容器限制被kernel杀死。JDK 8u191之后的版本已经默认支持容器感知,但如果你用的老版本,一定要在JAVA_OPTS里加-XX:+UseContainerSupport,否则Pod会莫名其妙地被OOMKilled。

3. K8S资源编排实战

3.1 命名空间隔离与资源配额

刚上手K8S时,我也犯过一个典型的错误:什么资源都不分命名空间,全部丢到default里。这样短期内看着方便,但项目一多就全乱套了,而且相互之间没有隔离,误操作删错资源是分分钟的事。

规范的做法是按环境或按项目创建命名空间。比如开发环境叫namespace-dev,生产环境叫namespace-prod。命名空间之间是逻辑隔离的,里面的Deployment、Service、ConfigMap互相不可见,但不会提供网络层面的硬隔离。做网络隔离要额外装网络策略插件,一般中小项目没这个必要,逻辑隔离就够了。

创建命名空间后,我还建议顺手配一个ResourceQuota,控制资源的分配上限:

apiVersion: v1 kind: ResourceQuota metadata: name: dev-quota namespace: namespace-dev spec: hard: requests.cpu: "4" requests.memory: "8Gi" limits.cpu: "8" limits.memory: "16Gi"

这个配额的作用是防止“某个环境把集群资源全抢走”。我之前遇到过一个情况,测试同学在测试环境里疯狂压测,把Pod副本数调得特别高,结果把生产环境的节点资源给挤占了,导致生产服务响应变慢。有了ResourceQuota,每个环境最多用多少资源是硬限制,谁也越不过去。

3.2 后端Deployment与Service配置

后端服务的Deployment是整个编排的核心。先把YAML拿出来,然后逐个字段讲为什么这么配:

apiVersion: apps/v1 kind: Deployment metadata: name: backend-service namespace: namespace-prod labels: app: backend-service spec: replicas: 2 selector: matchLabels: app: backend-service strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: backend-service spec: containers: - name: backend image: registry.example.com/project/backend:20240601-v1.2.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: "prod" - name: JAVA_OPTS value: "-Xms512m -Xmx2g -XX:+UseG1GC" - name: DB_HOST valueFrom: configMapKeyRef: name: app-config key: db.host - name: DB_PASSWORD valueFrom: secretKeyRef: name: app-secret key: db.password resources: requests: cpu: "500m" memory: "512Mi" limits: cpu: "2" memory: "2Gi" readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 60 periodSeconds: 20

几个关键点逐个说。

镜像tag必须带上版本号。不要用latest标签,这是我踩过最深的一个坑。用latest看起来很省事,但到了回滚的时候就傻眼了,因为latest的指向一直在变,你根本不知道当前跑的是哪个版本。采用日期+版本号的组合,比如20240601-v1.2.0,一眼就能看出部署时间和代码版本。

滚动更新策略这块,maxUnavailable: 0和maxSurge: 1组合意味着:每次发布先起一个新Pod,等新Pod就绪后再关掉一个旧Pod。这样整个发布过程中始终有Pod在对外提供服务,请求不会中断。如果发布后发现问题想回滚,一条kubectl rollout undo deployment/backend-service就回到上一个版本了。

探针配置直接关系到系统的自愈能力。readinessProbe告诉K8S“这个Pod准备好接收流量了吗”,没准备好的时候,Service不会往这个Pod转发请求。livenessProbe告诉K8S“这个Pod还活着吗”,如果探针失败,K8S会杀掉容器重启。SpringBoot项目只要引入了actuator依赖,/actuator/health这个端点就有了。initialDelaySeconds一定要给足,Java应用启动慢是出了名的,尤其生产环境加载配置、建立数据库连接池都需要时间。我见过有人initialDelaySeconds设置成5秒,结果应用还没起来就被K8S杀了,反复重启,形成CrashLoopBackOff。

Service的配置相对简单:

apiVersion: v1 kind: Service metadata: name: backend-service namespace: namespace-prod spec: selector: app: backend-service ports: - port: 8080 targetPort: 8080

记住clusterIP默认即可,不需要改成NodePort。内部调用直接用Service名,集群DNS会自动解析到后端Pod的IP列表,并且自带负载均衡。如果你想在集群外调试后端接口,用kubectl port-forward或者临时建一个NodePort类型的Service,千万不要直接改生产Service类型。

3.3 前端Deployment与共享卷方案

前端Deployment的整体框架跟后端类似,但有一个值得深入考虑的点:前端镜像里的nginx配置,以及静态资源更新的频率问题。

apiVersion: apps/v1 kind: Deployment metadata: name: frontend-service namespace: namespace-prod spec: replicas: 2 selector: matchLabels: app: frontend-service template: metadata: labels: app: frontend-service spec: containers: - name: frontend image: registry.example.com/project/frontend:20240601-v1.2.0 imagePullPolicy: IfNotPresent ports: - containerPort: 80 volumeMounts: - name: nginx-conf mountPath: /etc/nginx/conf.d/default.conf subPath: default.conf volumes: - name: nginx-conf configMap: name: frontend-nginx-config

这里有个设计决策值得讨论:我把nginx.conf做成了ConfigMap挂载,而不是直接烧在镜像里。这么做的动机是,前端在不同环境可能要用不同的后端Service地址,比如开发环境后端的Service名叫backend-service-dev,生产环境叫backend-service-prod。nginx配置里如果写死了后端地址,换个环境就得重新构建镜像。用ConfigMap挂载的话,只需要改一下ConfigMap里的内容,再滚动重启一下前端Pod即可,镜像完全不用动。

但有些团队的运维习惯是“镜像即产物,部署内容完全一致”,那也可以把nginx.conf烧进镜像里。两种方案各有取舍,我个人更倾向用ConfigMap,因为环境差异用配置去适配,比用镜像去适配要灵活得多。这也是12-Factor App里提倡的“环境配置与代码分离”精神。

3.4 配置文件与敏感信息的处理

配置管理这块,最容易出问题的不是功能问题,而是敏感信息泄露。很多项目在初期图省事,把数据库密码、Redis密码直接明文写在ConfigMap里,这在K8S上是不推荐的。

正确的姿势是分层管理:非敏感配置放ConfigMap,敏感信息放Secret。比如数据库地址、Redis连接信息这些都是半公开的,放ConfigMap没问题;但数据库密码、第三方接口密钥这类必须进Secret。Secret在etcd里是base64编码存储的,虽然不是加密,但至少不会明文出现在YAML文件里。需要加密的话,可以给etcd开启encryption at rest,生产环境建议开启。

举一个实际的数据组织例子。ConfigMap里可以集中管理:

apiVersion: v1 kind: ConfigMap metadata: name: app-config namespace: namespace-prod data: db.host: "mysql.internal.example.com" db.port: "3306" db.name: "business_db" redis.host: "redis.internal.example.com" redis.port: "6379"

Secret则包含真正的敏感项:

kubectl create secret generic app-secret \ --from-literal=db.password='S3cure_P@ssw0rd' \ --from-literal=redis.password='R3d1s_P@ss' \ -n namespace-prod

为什么要硬性要求ConfigMap和Secret分离?因为实际工作中,ConfigMap经常要进Git仓库方便版本管理,但Secret如果也进Git,就等于把生产密码暴露给所有有代码仓库权限的人。而且很多公司还会有审计合规的要求,敏感信息必须加密存储,分开之后,审计的时候只需要盯着Secret就行。

4. 网关入口与前端路由策略

4.1 单一入口Ingress还是多Service

前后端分离项目真正对外暴露的入口,可以有两种方案:一种是用Ingress把所有流量统一收口,另一种是前端Service用NodePort暴露,后端Service只内部访问。我在生产环境强烈推荐Ingress方案。

原因很简单:Ingress可以基于域名和路径做更精细的路由。比如你有个域名example.com,可以配置example.com/转发给前端Service,example.com/api/转发给后端Service。这样前端请求后端的API时,使用的是同域名的/api路径,天然避开了跨域问题,浏览器不会触发CORS拦截。

Ingress YAML的核心配置长这样:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: frontend-ingress namespace: namespace-prod annotations: nginx.ingress.kubernetes.io/rewrite-target: /$2 spec: ingressClassName: nginx rules: - host: example.com http: paths: - path: /api(/|$)(.*) pathType: ImplementationSpecific backend: service: name: backend-service port: number: 8080 - path: /(/|$)(.*) pathType: ImplementationSpecific backend: service: name: frontend-service port: number: 80

这段配置有两个关键点。

第一个是rewrite-target。前端请求的路径是/api/user/list,Ingress转发到后端Service时,如果不去掉/api前缀,后端Controller里配置的/user/list接口就对不上。rewrite-target: /$2这个注解会把匹配到的路径重写,把/api/前缀剥掉,后端收到的请求就是/user/list了。

第二个是pathType。我用了ImplementationSpecific,这是因为我的路径里带了正则表达式(/|$)(.*),不是标准的前缀匹配或精确匹配。不同Ingress Controller对正则的支持不一样,nginx ingress是支持这个语法的,但如果用的是云厂商的Ingress,可能只支持Prefix和Exact,这时候就要换一种写法了。

4.2 跨域问题的根治方案

前后端分离项目里,跨域问题几乎是绕不开的。但有一个事实要先说清楚:只要用了Ingress统一入口,且前端的nginx把API请求代理到后端,前端代码里其实根本不用处理跨域,因为浏览器看到的请求都是同源的。问题出在两种场景:第一,你直接从前端容器通过Service名访问后端Service;第二,你在本地开发时,vue的devServer直接请求生产环境的API。

第一种场景要用Nginx层代理处理,已经在前面nginx.conf里写过了,location /api/ { proxy_pass http://backend-service:8080; }。

第二种场景,通常是开发完成后联调阶段出现的。前端跑在本地8080端口,API在后端服务,浏览器一定会拦截跨域请求。解决办法有两个:要么在后端SpringBoot里配置CORS允许指定来源,要么在vue.config.js里配置devServer代理:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

用了代理之后,前端本地请求也是同源的,浏览器就不会拦截了。开发环境的事,不要拿生产配置去凑合,环境分离要贯彻到底。

4.3 前端路由模式与刷新404问题

前面提到过history路由刷新404的问题,这里把原理再讲透一点,因为它是前端容器化部署中最常见的“滑铁卢”。

vue默认的路由模式是hash,URL里带一个#,比如example.com/#/user/list。hash模式下,刷新页面时请求的是example.com/,nginx一定能返回index.html,所以不会出现404。但hash模式有两个缺点:URL不美观,而且不利于SEO。所以大部分正式项目都会切换成history模式。

一旦切到history模式,URL变成example.com/user/list,刷新时浏览器会真的向服务器发起GET /user/list的请求。nginx在这个路径下找不到对应的静态文件,就会返回404。所以必须写明try_files $uri $uri/ /index.html;,把所有访问不到的路径统一回退到index.html,让vue-router接管路由。这个坑我见得太多了,一定不能在nginx配置里漏掉。

5. 可观测性与稳定性保障

5.1 容器资源限制与自动扩缩容

K8S里不写resources的Pod,就像不系安全带上高速,出了事才后悔。有些Pod在没有limits限制的情况下,内存无限上涨,一直涨到把节点内存占满,触发系统OOM,把其他正常的Pod也连累了。

所以每个Deployment的容器定义里,requests和limits必须同时写清楚。requests是K8S调度Pod时用来选节点的“最低需求”,limits是Pod能使用的“最高上限”。当Pod内存达到limit时,容器会被Kill或重启;CPU达到limit时,只会被限流,不会Kill。

实际配置时要注意requests和limits的合理比例。我一般按CPU 1:4、内存1:4来设置,比如requests 500m/512Mi对应limits 2/2Gi。但如果你的业务对延迟特别敏感,limits可以适当放宽,避免被限流导致响应变慢。

把这些配置好后,就可以开HorizontalPodAutoscaler了:

kubectl autoscale deployment backend-service \ --cpu-percent=60 \ --min=2 \ --max=8 \ -n namespace-prod

这个HPA会在CPU使用率超过60%时自动增加副本数,最多加到8个。高峰期扛流量,低谷期节省资源,完全不用人工干预。但要注意:HPA只根据现网流量做水平伸缩,对突发流量的反应有滞后性,如果业务有秒杀这类场景,还是得配合Cluster Autoscaler或提前扩容策略。

5.2 滚动更新与优雅停机

SpringBoot应用在K8S滚动更新时,出现过“请求还打到正在下线的Pod上”的情况吗?这个问题根因在“优雅停机”没做好。

K8S滚动更新时,会先给旧Pod发送SIGTERM信号,然后等待Pod优雅终止。但nginx转发请求到后端靠的是K8S Service的Endpoints列表,如果Pod还挂在Service的Endpoints里,但进程已经被SIGTERM关闭了,那请求就会打到已不可用的Pod上,报连接拒绝。

解决办法有两层。第一层,SpringBoot项目要开启优雅停机:

server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 20s

第二层,利用readinessProbe的机制。Pod收到SIGTERM后,K8S会将其状态变为Terminating,但Service的Endpoints不会立刻摘除。这段时间内,新请求还是可能转发进来的。更好的做法是利用preStop钩子,提前把Pod从Endpoints摘除,再进入终止流程:

lifecycle: preStop: exec: command: ["sh", "-c", "sleep 15"]

这个sleep 15是为了等待Service的Endpoint控制器把Pod摘掉,再让Java进程处理完存量请求后退出。实测下来,这个组合拳能有效避免发布期间的连接中断。

5.3 日志采集方案

K8S里的Pod是“朝生暮死”的,日志如果只写在容器内部,Pod一删除日志就没了。生产环境的日志必须采集到集中式平台。

最简单的方案是给集群部署一套轻量级的日志采集器,用DaemonSet在每个节点上跑一个日志采集Pod,把容器日志采集到ElasticSearch或对象存储里,配合Kibana做查看和分析。日志路径不用改代码,K8S会把容器的stdout和stderr写到节点上的/var/log/containers目录,日志采集器只需要监控这个目录就行。

有一点要注意:应用打日志时,尽量使用stdout输出,而不要写log文件。K8S生态默认认为应用日志应该输出到标准输出,然后由平台层统一采集。如果你在yml里配置了log文件输出路径,日志就只能存在Pod的文件系统里,Pod销毁后日志就丢了,非常可惜。

6. 实战排错实录与常见问题速查

6.1 镜像拉取失败

镜像拉取失败是最高频出现的问题。常见的失败信息是ImagePullBackOff或ErrImagePull。排查思路分几步走:先确认镜像名和tag是否写错,比如snake_case写成了CamelCase,这个错误低级但出现过很多次。再确认镜像是否存在,如果用的是私有仓库,还要确认Pod所在的节点有没有配置imagePullSecret。

私有仓库的认证配置是这样:

apiVersion: v1 kind: Secret metadata: name: registry-key type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: <base64之后的内容>

然后在Deployment的template里加imagePullSecrets:

spec: imagePullSecrets: - name: registry-key

如果是在云平台自建集群,节点角色是Master的话,默认有污点,不加容忍的Pod是调度不到Master节点上的,也会出现镜像拉取不了或Pending的状态。

6.2 CrashLoopBackOff排查

看到CrashLoopBackOff,说明Pod一直在“启动-崩溃-重启”的死循环里。第一件事不是看代码,而是看日志:

kubectl logs -f backend-service-xxxx -n namespace-prod --previous

加上--previous参数可以查看上次崩溃时容器输出的日志,很多时候崩溃发生得太快,当前容器还没输出日志就被Killed了。

CrashLoopBackOff常见的几个原因依次是:配置错误导致应用启动失败,比如数据库地址连不上;探针配置不合理,应用还在启动中就被livenessProbe判定为不健康并杀死;启动时依赖的Secret或ConfigMap不存在。逐个排查时可以用kubectl describe pod看事件信息,重点看Events部分,里面有详细的出错提示。

6.3 Pod一直Pending

如果Pod一直是Pending状态,说明调度器根本没能把Pod放到任何节点上。常见的两个原因:节点资源不足,插不进新的Pod;或者Pod指定的资源requests过大,超出了所有节点剩余的可用资源。

用kubectl describe pod查看事件信息是最快的定位方式。如果是资源不足,事件里会写Insufficient cpu或Insufficient memory。此时要么扩节点,要么调低Deployment的requests。还有一种情况是节点有污点而Pod没加容忍,事件里会写Untolerated taint,这类问题在把集群划分成不同用途(比如GPU节点、SSD节点)时常见。

6.4 常见问题速查表

现象可能原因排查方法
ImagePullBackOff镜像名错误、私库未认证kubectl describe pod 查看事件
CrashLoopBackOff启动失败、探针误杀看崩溃前日志--previous
Pod Pending资源不足、污点不容忍看describe事件中的调度信息
服务间调用不通命名空间不同、Service名写错确认DNS是服务名.命名空间.svc.cluster.local
前端刷新404没有配置try_filesnginx配置补try_files $uri $uri/ /index.html
发布期间出现连接拒绝优雅停机没配置开启graceful shutdown + preStop sleep
启动极慢反复被杀探针initialDelaySeconds过短增大initialDelaySeconds到30秒以上

7. 实际部署后的运维经验与后续扩展

整套系统在K8S上跑稳定之后,有几个运维习惯我建议一定要养成。

第一,发布流程固定化。先在测试环境验证镜像,通过后再打tag推到生产仓库,然后用同一份YAML文件部署到生产。保证测试环境和生产环境用的镜像完全一致,只是配置不同。K8S的声明式API让这套流程变得极其自然,因为YAML本身就是“配置即代码”。

第二,给Deployment加版本注释。每次发布时,在template的annotations里记录提交ID或功能摘要,比如:

template: metadata: annotations: deployment.kubernetes.io/revision: "1.2.0" deployer: "ops-user" description: "修复订单超时问题"

这样一条kubectl rollout history deployment/backend-service就能看到完整发布历史,排障时会方便很多。

第三,备份K8S资源定义。Deployment、Service、ConfigMap、Ingress这些资源定义,全部放在Git仓库里管理,用GitOps的思维去做。改动必须走代码评审,由负责同事审批后合并,然后再让CI自动执行kubectl apply。意外删错资源这种事,经历过一次就会明白这套流程有多重要。

我个人在实际操作中最深的体会是:K8S这套东西,门槛不在“把YAML写出来”,而在“把边界想清楚”——什么时候该用Deployment、什么时候该用StatefulSet、什么时候直接走外部云服务,这些决策直接影响未来几个月的运维生活质量。前后端分离项目本身是标准的无状态应用,部署到K8S是性价比最高的选择。把镜像、配置、探针、资源限制这几件基础工作做扎实,后面再复杂的发布需求都能顺藤摸瓜找到解法。

最后再分享一个工具技巧:用k9s这个终端工具管理集群,比纯用kubectl效率高非常多。它可以把Pod状态、日志、事件、资源占用率都集中在一个界面里,按数字键就能切换资源类型,按l键直接看日志,排障体验提升一个档次。属于用过就回不去的效率工具。

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

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

立即咨询