
SpringBoot Kubernetes Helm 这套组合最近几年基本是云原生微服务部署的默认答案。我自己在好几个项目里从裸机 jar 包部署迁到容器集群最深的感受是如果只把 Kubernetes 当成一台能跑容器的服务器那真正的压力全留给了运维但配合 Helm 把部署模板化再加上 HPA 自动扩缩容整个交付节奏会快很多。这篇文章就完整记录一次从零开始把 SpringBoot 微服务部署到 Kubernetes并用 Helm 管理版本、实现弹性扩缩容的实战过程。适合刚接触云原生、想落地微服务部署的同学参考也适合已经上手 K8s 但还没理顺 Helm 和 HPA 配合方式的工程师。1. 云原生微服务部署的整体思路与方案选型1.1 微服务上云先解决部署方式的问题很多团队的微服务架构演进到了一定阶段都逃不开一个问题服务越来越多环境越来越复杂人工维护部署配置的成本开始指数级上升。SpringBoot 本身是一个很好的应用开发框架内嵌 Tomcat、自动装配、健康检查这些设计让应用很容易被容器化。但容器化之后怎么调度、怎么扩缩容、怎么管理多套环境又是另一层问题。docker-compose 可以用来做单机多容器编排做本地开发非常合适但在生产环境里它不具备故障自愈、节点调度、自动伸缩这些能力也不适合管理跨多台机器的服务集群。Kubernetes 的价值在于它把“怎么跑”“跑几个”“坏了怎么办”这些逻辑统一纳管起来Pod 崩溃了会自动重启节点出问题可以把负载调度走配合 Service 做服务发现配合 Ingress 做流量入口。这些能力对微服务来说不是锦上添花而是刚需。所以整体思路是SpringBoot 负责业务和 APIDocker 负责把应用打包成不可变镜像Kubernetes 负责运行和调度Helm 负责把 Kubernetes 资源清单变成可配置、可版本化的模板最后用 HorizontalPodAutoscaler 让副本数跟着负载自动缩放。这个链路里每一层都有明确职责替换成本也低。1.2 Helm 解决了什么问题直接写 kubectl apply -f deployment.yaml、service.yaml 也不是不能用但一旦服务数量上来YAML 里的重复字段、环境差异、版本管理就非常痛苦。Helm 的核心贡献是把一组 Kubernetes 资源打包成一个 Chart用模板语法把可变参数抽到 values.yaml 里然后通过 helm install、helm upgrade、helm rollback 管理整个发布过程。我选 Helm 还有一个很实际的理由回滚。手写 YAML 的时候出了问题要么手动改再 apply要么靠 git 回溯过程又慢又容易出错。Helm 每次升级都会生成一个 release 版本号可以随时 helm rollback 回到上一个稳定版本。这在业务高峰期出错时是真正的救命功能。1.3 整体架构预览整个交付链路可以分成三段构建阶段SpringBoot 项目用 Maven/Gradle 打成 jar再由 Dockerfile 构建成 OCI 镜像推送到镜像仓库。部署阶段编写 Helm Chart里面包含 Deployment、Service、HPA 等资源模板通过 helm install 一次性部署。运行阶段Kubernetes 根据资源 requests/limits 进行调度HPA 周期采集指标动态调整副本数Service 负责流量负载均衡。这篇文章的实战部分会按这个链路一步步走。为了避免篇幅太散下面统一用一个虚构的订单服务演示它只提供一个 HTTP 接口但技术栈和部署方式足够代表大多数 SpringBoot 微服务。2. 环境准备与基础设施搭建2.1 本地 Kubernetes 环境选型先说环境。生产环境通常有云厂商托管 K8s但学习阶段不建议直接拿生产集群练手。本地搭建 K8s 最常用的三个方案是 minikube、kind、k3s我对比过它们的侧重点方案适用场景启动成本资源占用minikube完整 K8s 功能体验适合模拟生产中中kind轻量适合 CI 和快速测试低低k3s边缘节点、资源受限环境低低我的建议是直接用 minikube。它对 Kubernetes 核心功能支持最完整自带附加组件管理比如后面需要的 metrics-server 可以直接 addons 开启。资源方面至少给 4 核 CPU 和 8GB 内存否则跑两个副本再压测很容易因为节点资源不足导致 Pod 一直 Pending。启动命令minikube start --cpus4 --memory8192 --driverdocker启动后确认节点状态kubectl get nodes如果看到 STATUS 是 Ready说明集群可用。2.2 安装 kubectl 和 Helmkubectl 是操作集群的命令行工具。安装细节我就不展开了直接说一个经常踩的坑kubectl 的客户端版本和集群版本不要差太多。用kubectl version --client可以看客户端版本用kubectl version --server可以看服务端版本两边保持在 minor 版本相差不超过一个比较稳妥。Helm 的安装更简单。Helm 3 早就移除了 Tiller 服务端组件只在本地安装一个客户端就行。Mac 上用 Homebrewbrew install helmLinux 环境可以用官方脚本curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash安装完验证一下helm version另外需要确认本地能访问 Helm 仓库。生产环境经常需要添加私有仓库但这次实战不需要额外仓库直接使用本地 Chart 目录即可。2.3 镜像仓库准备Kubernetes 从节点上拉镜像需要从一个 registry 获取。本地演示有几种做法最简单的其实是复用 Docker Hub把镜像推上去然后 deployment 里写完整镜像地址。但如果网络条件有限也可以让 minikube 直接复用本地 Docker 守护进程eval $(minikube docker-env)这条命令之后你执行的 docker build 会构建到 minikube 的虚拟机里集群节点可以直接使用该镜像前提是 imagePullPolicy 设置为 IfNotPresent 或 Never。这样可以跳过推送远程仓库的步骤非常适合本地调试。注意如果切换了终端记得重新执行 eval 命令。平时我在一处终端里 build 完镜像换到另一个终端去 apply 发现镜像拉不到多半就是因为忘了设置 docker-env。3. SpringBoot 应用改造与 Docker 镜像制作3.1 准备一个最小的 SpringBoot 服务创建一个 SpringBoot 项目建议依赖只加 Web 和 Actuator。Web 提供接口Actuator 提供健康检查端点这在后面配置 K8s 探针时非常关键。项目里写一个简单的 ControllerRestController public class OrderController { GetMapping(/api/order/health) public String health() { return order-ok; } }同时配置 application.ymlserver: port: 8080 shutdown: graceful spring: application: name: order-service lifecycle: timeout-per-shutdown-phase: 30s management: endpoints: web: exposure: include: health,infoserver.shutdown: graceful是 SpringBoot 2.3 引入的优雅停机机制。应用收到 SIGTERM 后会先停止接受新请求等待存量请求处理完再退出。对 Kubernetes 来说这个配置配合 Pod 生命周期钩子能大大减少滚动更新时的请求错误。Actuator 默认的健康检查路径是/actuator/health但为了区分存活探针和就绪探针我通常会额外配置management: endpoint: health: probes: enabled: true这样 SpringBoot 2.2 会提供/actuator/health/liveness和/actuator/health/readiness两个子端点语义上和 K8s 的 livenessProbe、readinessProbe 正好对应。3.2 编写多阶段构建的 DockerfileSpringBoot 应用容器化最容易踩的坑是 JVM 内存参数。默认情况下 JVM 会认为容器有多少内存就用多少导致它直接向容器申请超过 limits 的内存被 Kubernetes 判定为 OOMKilled。所以我在 Dockerfile 里显式控制 JVM 内存并且选用多阶段构建来减小最终镜像体积。FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests FROM eclipse-temurin:17-jre WORKDIR /app COPY --frombuilder /app/target/order-service.jar app.jar EXPOSE 8080 ENV JAVA_OPTS-XX:MaxRAMPercentage75.0 -Djava.security.egdfile:/dev/./urandom ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar app.jar]这里用MaxRAMPercentage75.0而不是固定的 -Xmx是为了让 JVM 根据容器句柄自动计算内存上限保证在 256Mi、512Mi 不同规格下都能合理分配。urandom配置可以加快 JVM 启动时随机数的初始化速度对大量 Pod 并发启动的场景有实际帮助。如果你用的是 Spring Boot 2.3还可以利用 jar 自带的分层结构COPY --frombuilder /app/target/order-service.jar app.jar RUN java -Djarmodelayertools -jar app.jar extract分层构建的好处是依赖、Spring Boot 框架、应用业务代码分别放在不同层后续业务代码变动时只需要推送最后一层镜像下载体积小很多。不过这会改变 Dockerfile 的启动方式我建议新手先用简单版本跑通了再优化。3.3 构建镜像并验证构建前先确认 docker-env 已经设置好。执行docker build -t order-service:1.0.0 .构建完成后可以在本机直接启动容器验证接口docker run --rm -p 8080:8080 order-service:1.0.0访问http://localhost:8080/api/order/health能看到返回内容说明镜像没问题。这一步看起来简单但能帮你把应用本身的问题和后续 K8s 配置问题隔离开。4. 编写 Helm Chart 并部署应用4.1 Chart 目录结构Helm 一个 Chart 就是一个目录。使用helm create可以快速生成模板helm create order-service生成之后把不需要的文件删掉一个精简的 Chart 结构是这样的order-service/ ├── Chart.yaml ├── values.yaml └── templates/ ├── deployment.yaml ├── service.yaml ├── hpa.yaml └── _helpers.tplChart.yaml 里设置 name 和 versionapiVersion: v2 name: order-service description: A SpringBoot microservice type: application version: 0.1.0 appVersion: 1.0.04.2 values.yaml 参数设计values.yaml 是整个 Chart 的可调参数中心。我把部署相关、资源、自动扩缩容、探针都拆成字段这样不同环境可以用不同 values 文件覆盖replicaCount: 2 image: repository: order-service tag: 1.0.0 pullPolicy: IfNotPresent springProfile: k8s service: type: ClusterIP port: 80 targetPort: 8080 resources: requests: cpu: 200m memory: 256Mi limits: cpu: 1 memory: 512Mi autoscaling: enabled: true minReplicas: 2 maxReplicas: 10 targetCPUUtilizationPercentage: 60 livenessProbe: path: /actuator/health/liveness initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: path: /actuator/health/readiness initialDelaySeconds: 5 periodSeconds: 5 rollingUpdate: maxSurge: 1 maxUnavailable: 0这里springProfile可以用于环境区分。生产环境你可能设置成 prod测试环境设置成 test通过SPRING_PROFILES_ACTIVE注入到 Pod 环境变量里。4.3 Deployment 模板写法Deployment 模板是最核心的。先定义 metadata 和 selectorapiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-{{ .Chart.Name }} labels: app: {{ .Release.Name }}-{{ .Chart.Name }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: app: {{ .Release.Name }}-{{ .Chart.Name }} template: metadata: labels: app: {{ .Release.Name }}-{{ .Chart.Name }} spec: containers: - name: {{ .Chart.Name }} image: {{ .Values.image.repository }}:{{ .Values.image.tag }} imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - containerPort: 8080 name: http env: - name: SPRING_PROFILES_ACTIVE value: {{ .Values.springProfile }}资源声明和探针非常重要直接决定 Pod 的调度和滚动更新表现resources: requests: cpu: {{ .Values.resources.requests.cpu }} memory: {{ .Values.resources.requests.memory }} limits: cpu: {{ .Values.resources.limits.cpu }} memory: {{ .Values.resources.limits.memory }} livenessProbe: httpGet: path: {{ .Values.livenessProbe.path }} port: 8080 initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} periodSeconds: {{ .Values.livenessProbe.periodSeconds }} readinessProbe: httpGet: path: {{ .Values.readinessProbe.path }} port: 8080 initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} periodSeconds: {{ .Values.readinessProbe.periodSeconds }}这里解释一下两种探针的区别liveness 探针失败Kubernetes 会杀掉容器并重启readiness 探针失败则会把 Pod 从 Service 的 Endpoint 列表里摘掉不让流量继续打到这个 Pod。SpringBoot 应用启动通常需要几秒到几十秒所以 liveness 的 initialDelaySeconds 要留足否则应用还没起来就被反复重启readiness 初始延迟可以短一点只要接口通了就对外提供服务。4.4 Service 和辅助资源Service 负责把一组 Pod 的端口统一暴露出来。模板apiVersion: v1 kind: Service metadata: name: {{ .Release.Name }}-{{ .Chart.Name }} spec: type: {{ .Values.service.type }} selector: app: {{ .Release.Name }}-{{ .Chart.Name }} ports: - port: {{ .Values.service.port }} targetPort: {{ .Values.service.targetPort }}Service 类型这里先用 ClusterIP只在集群内部访问。如果需要从外部访问可以在外面再加一层 Ingress这里不展开。4.5 执行部署在 Chart 目录下先做静态检查helm lint order-service没有问题后安装helm install order-release ./order-service --namespace demo --create-namespace查看 release 和 Pod 状态helm list -n demo kubectl get pods -n demo -w如果看到 Pod 进入 Running 且 READY 显示 2/2说明部署成功。再验证服务kubectl run curl-test --imagecurlimages/curl --rm -it --restartNever -- curl http://order-release-order-service.demo.svc.cluster.local/api/order/health能返回 order-ok说明从集群内部访问服务链路是通的。5. 弹性扩缩容实战5.1 HPA 的工作原理HorizontalPodAutoscaler 会根据 CPU、内存指标或自定义指标自动调整 Deployment 的副本数。它不是一个实时机器默认的指标同步周期在 kube-controller-manager 里通常是 15 秒一次加上指标采集延迟扩容动作会有一定滞后。HPA 计算期望副本数的公式大致是期望副本数 ceil(当前副本数 × (当前指标值 / 目标指标值))举个例子如果当前副本数是 2目标 CPU 利用率是 60%实际 CPU 利用率是 120%那期望副本数就是 ceil(2 × 2) 4。扩缩容策略还有冷却时间避免指标抖动导致副本数频繁变化。5.2 在 Chart 中启用 HPA 模板为了保证每个环境都有独立开关我在 values 里通过autoscaling.enabled控制。HPA 模板{{- if .Values.autoscaling.enabled }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: {{ .Release.Name }}-{{ .Chart.Name }} labels: app: {{ .Release.Name }}-{{ .Chart.Name }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: {{ .Release.Name }}-{{ .Chart.Name }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} {{- end }}注意Kubernetes 1.26 版本上建议使用autoscaling/v2如果你的集群版本较老需要改成autoscaling/v2beta2。5.3 压测触发扩容HPA 需要 metrics-server 提供指标。minikube 默认没有启用它先执行minikube addons enable metrics-server稍等片刻确认能看到指标kubectl top nodes kubectl top pods -n demo然后查询 HPA 状态kubectl get hpa -n demo现在给服务施加压力。可以在集群内部跑一个请求循环或者用 hey 压测工具。最简单的方式是在一个临时 Pod 里循环请求kubectl run -it --rm load-generator --imagebusybox -- /bin/sh进入容器后执行while true; do wget -q -O- http://order-release-order-service.demo.svc.cluster.local/api/order/health /dev/null; done过一两分钟再观察kubectl get hpa -n demo kubectl get pods -n demo正常情况下副本数会从 2 逐步变多。但这里有个特别容易误判的地方单个请求对 CPU 的影响极小如果你的接口只是返回字符串并发循环也不一定能触发扩容。建议在接口里加一点有意义的 CPU 操作比如综合计算或者提高并发压测的数量。也可以用hey -z 3m -c 100 http://...这类工具来制造更真实的负载。再看 HPA 详细状态kubectl describe hpa -n demo order-release-order-serviceEvents 里能看到New size: 4; reason: Cpu resource utilization reached 125% of request这类信息这就是扩容触发记录。5.4 手动验证缩容与新版本发布当压力停止指标下降后HPA 会在冷却时间过后减少副本数最终回到 minReplicas。配合 Helm 做一次版本升级helm upgrade order-release ./order-service --namespace demo --set image.tag1.0.1升级时 Deployment 会执行滚动更新。values 里设置了maxUnavailable: 0和maxSurge: 1含义是滚动过程中不会出现少于期望副本数的可用 Pod但可以多出一个临时 Pod保证整个更新过程服务不中断。如果新版本有问题回滚命令helm rollback order-release 1这条命令会非常快地把整个 release 恢复到上一个版本比手动改 YAML 再 apply 要可靠得多。6. 常见问题与排查技巧实录6.1 Pod 一直 ImagePullBackOff本地使用 minikube docker-env 构建镜像时最常遇到这个问题。检查节点上的镜像是否存在minikube ssh docker images | grep order-service如果镜像不在节点上说明 eval 设置失效了。重新执行eval $(minikube docker-env)再 build 一次。如果镜像在远程仓库还需要确认 deployment 里的 imagePullPolicy 和 imagePullSecret 是否配置正确。我会建议所有环境都避免使用latest标签因为 Helm 渲染出来的 deployment 在 tag 不变时即使重新 applyPod 也可能不会重新拉取镜像。6.2 Pod 一直 PendingPending 最常见的原因是节点资源不足。SpringBoot 请求内存设了 256Mi但如果本地集群只分配了 2GB 内存两个副本加系统组件可能就满了。查看事件kubectl describe pod pod-name -n demo事件里通常会有Insufficient memory或0/1 nodes are available的提示。解决方式要么调小 resources.requests要么给 minikube 更多内存。6.3 CrashLoopBackOffSpringBoot 容器反复重启大多是 JVM 内存超限或探测失败。先看日志kubectl logs pod-name -n demo --tail200如果看到Container OOMKilled说明 limits 不够调大 memory limits 或调低MaxRAMPercentage。如果日志里启动正常但随即被重启那很可能是 livenessProbe 的 initialDelaySeconds 太短应用还没就绪就被判死。把 liveness 探针 initialDelaySeconds 调到 30 秒以上再观察。6.4 服务访问不通服务通了但接口 404 或连接拒绝优先检查 readiness 探针。Kubernetes 只有能通过 readiness 探针的 Pod 才会被加入 Service 的 Endpoint 列表kubectl get endpoints -n demo如果 ENDPOINTS 为空说明 Pod 未就绪。用 curl 访问 readiness 路径检查是否 200。另外还要确认 Service 的 selector 和 Pod 的 label 匹配。Helm 渲染时.Release.Name会变很容易出现标签对不上的问题我建议所有 label 都基于.Release.Name拼接保持一致。6.5 HPA 不生效先排除 metrics-server 没装的情况kubectl get --raw /apis/metrics.k8s.io/v1beta1如果返回 404说明 metrics-server 不可用需要安装或启用。再看 hpa 的事件kubectl describe hpa -n demo hpa-name如果事件显示failed to get cpu metric多半是 resources.requests 没配置。HPA 计算利用率是基于 requests 的如果 Pod 没有写 requests.cpuHPA 无法计算指标扩容自然不生效。所以 resources.requests 不仅是调度依据也是扩缩容的基准线。最后再分享一点我的实操体会整套链路跑通之后最值得花心思优化的其实是资源配额和探针参数。初学时我以为只要写好 YAML、部署成功就完事了直到线上流量波动时才明白没有合理的 requests/limits扩容就像没有参考系的缩放Pod 被 OOM 杀死、流量抖动、滚动更新卡住都是连锁反应。建议把资源配额写进 Chart 的必须项而不是放在 optional 位置探针的路径和延迟时间也要按应用的实际启动速度反复调。Helm 的价值在真正需要多环境管理和快速回滚时体现得最明显越早把部署模板化后面运维越省心。这套方案在几十个实例的规模下用起来非常顺手希望对你的项目也能提供同样的帮助。