ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot项目Kubernetes化:从镜像构建到生产级部署实践

Spring Boot项目Kubernetes化:从镜像构建到生产级部署实践 我接手过不少Spring Boot项目早期基本都是打jar包扔到一台服务器上用nohup java -jar xxx.jar 这种原始方式跑起来。单机部署确实省事但服务一多、流量一大就开始难受日志分散在各台机器上扩容要手动加机器发布一个新版本搞得全组人如临大敌。后来我把项目迁到KubernetesK8s上用声明式的方式统一管理部署整个过程才真正变得有序可控。这篇文章就把我从Docker镜像构建、Deployment编写到探针配置、自动扩缩容、监控排障的完整经验整理出来。适合三类人看第一类是Spring Boot开发写了不少但对容器编排一知半解想搞明白部署到底怎么落地第二类是团队刚引入K8s正准备把老项目容器化的运维或架构同学第三类是想学习云原生部署路径把Deployment、Service、ConfigMap这些概念串起来的入门者。如果是老手可以直接跳到第3章的实操部分里面有我在生产环境里踩过的坑和调参经验。1. 为什么要把Spring Boot项目搬上Kubernetes1.1 传统部署和K8s部署的差别传统方式部署Spring Boot项目流程一般是CI拉代码Maven打包把jar包传到服务器杀掉旧进程启动新进程再用Nginx或SLB把流量切过去。这套流程在单机场景没问题但放到多实例、微服务化的场景里账就不太好算了。服务增多后每台服务器的资源利用率很难均衡有的机器CPU飘红有的机器内存长期闲置。水平扩容需要手动加机器、改负载均衡配置、再部署服务整个过程全靠人出错概率高。机器宕机后恢复流程依赖运维个人经验没有统一的自愈机制。服务之间的调用地址写IPIP一变全都得跟着改。K8s把这些问题抽象成了几个核心对象Pod是最小的调度单元Deployment负责声明期望的实例数和滚动更新策略Service提供稳定的访问入口ConfigMap和Secret做配置与敏感信息分离。Spring Boot应用在这些对象的配合下从一个需要被细心照看的进程变成一个可以被调度、被观测、被自动恢复的云原生服务。1.2 哪些Spring Boot项目适合上K8s我的建议是别盲目跟风不是所有项目都非要上K8s。我判断一个项目该不该容器化主要看几个条件。第一服务需要多副本或者需要弹性伸缩。如果永远是单实例一台虚拟机跑jar包反而更省事引入K8s是给自己找麻烦。第二团队有容器化基础至少Docker用得熟练因为K8s虽然是编排层但底层所有应用都得跑在容器里镜像构建不熟练后面全卡在镜像环节。第三有配套的日志、监控、CI/CD诉求K8s的滚动发布和回滚能力很强但没有自动化流水线手工敲命令发布效率照样上不去。Spring Boot在这个场景里有天然优势它打包出来的可执行jar可以直接用java命令启动Actuator开箱即用地提供健康检查端点Spring Boot 2.3之后还针对容器环境做了内存感知优化。所以Spring Boot加K8s的组合几乎是我能想到的最顺滑的云原生落地路径。2. 镜像构建准备阶段的核心动作在K8s上部署Spring Boot第一步是把应用打包成镜像。这一步看起来简单但实际优化空间相当大里面有不少细节决定你后面部署是顺风顺水还是处处踩坑。2.1 用多阶段构建减小镜像体积我见过不少团队直接把jar包扔进openjdk:8-jdk-alpine里跑一个镜像动辄四五百MB推到私有仓库慢拉取也慢。其实Spring Boot项目完全可以做到100MB左右。推荐的方式是多阶段构建。第一阶段用Maven镜像编译第二阶段只复制jar包到运行时镜像。我常用的Dockerfile大概长这样FROM maven:3.8-openjdk-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-alpine WORKDIR /app COPY --frombuilder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app/app.jar]这里有几个细节值得说明。第一dependency:go-offline先把依赖缓存到镜像层只要pom文件不变后续构建会快很多。第二运行时镜像用JRE而不是JDK体积会小很多。第三别在Dockerfile里直接写死JVM参数尽量通过环境变量传入这是为后面在K8s的Deployment里统一管理资源做准备。另外项目里一定要加.dockerignore文件把.git、target、.idea这些目录排除掉否则COPY的时候会把一堆没用的文件带进构建上下文拖慢构建速度。如果你用Spring Boot 3以上版本JDK版本至少要17基础镜像可以考虑eclipse-temurin或bellsoft这两个在安全更新和维护上相对靠谱。2.2 镜像仓库与版本管理镜像构建出来必须推到仓库K8s节点才能拉取。私有仓库我推荐用Harbor或阿里云ACR功能上都够用区别不大。但有几个习惯建议养成。镜像tag坚决不要用latest生产环境一定要带上版本号比如1.2.3或git-abc1234。latest有个很坑的地方你拉下来的时候根本不知道是哪次构建的产物线上出了问题想回滚发现tag对不上之前版本直接抓瞎。镜像仓库建议开启漏洞扫描Spring Boot依赖多第三方库容易出CVE扫描能在上线前发现明显问题。还有一个真实踩坑记录有段时间我们镜像push到Harbor后K8s节点一直拉取失败后来排查了半天发现是节点上的容器运行时没配置Harbor的insecure-registryHTTP请求被拒。这类问题不在代码层面但会让你部署进度直接卡死。提前配好仓库的访问能力和认证信息比什么都重要。如果节点需要从私有仓库拉取镜像记得在K8s里创建imagePullSecret并配置到Deployment中否则ImagePullBackOff会一直缠着你。2.3 配置外部化让环境变量接管Spring Boot本身支持多套配置源的优先级覆盖命令行参数大于Java系统属性Java系统属性大于环境变量环境变量大于application.yml。在K8s里环境变量的注入非常方便所以我强烈建议把数据库、Redis、MQ等连接信息全部从配置文件中抽离通过ConfigMap或Secret注入。举个例子原来的application.yml里可能直接写着spring: datasource: url: jdbc:mysql://localhost:3306/demo username: root password: 123456迁到K8s后应该改成spring: datasource: url: ${DB_URL} username: ${DB_USERNAME} password: ${DB_PASSWORD}然后在Deployment里引用ConfigMap管理普通配置、Secret管理敏感信息。连接信息这种敏感数据建议放在Secret而不是ConfigMap虽然K8s自带Secret的加密能力相对有限但至少做到了配置与代码分离权限控制也好做。3. 核心部署实战从YAML到上线前两章是把地基打牢这一步是重头戏。我会用一个典型的订单服务为例从命名空间、Deployment、Service、配置注入到探针和自动伸缩全部串起来讲一遍。3.1 命名空间与资源隔离很多团队一上来就把所有资源全塞进default命名空间短期用着方便但项目一多就乱了。我建议至少按环境分dev、staging、prod。多业务线的还可以再按业务分。命名空间做的是逻辑隔离不是安全隔离。它的主要价值有三个资源配额管理、权限控制、对象组织。可以给每个命名空间设置ResourceQuota防止某个环境把整个集群资源吃光apiVersion: v1 kind: ResourceQuota metadata: name: dev-quota namespace: dev spec: hard: requests.cpu: 8 requests.memory: 16Gi limits.cpu: 16 limits.memory: 32Gi多团队共用集群的时候这个配置能少吵很多架。没有配额限制的环境一旦有人把副本数调到几十个整个集群都可能被拖垮。每个Deployment我都会在metadata里加namespace字段防止不小心部署错环境。这一步看似不起眼但能避免不少生产事故。3.2 Deployment用声明式描述期望状态Deployment是K8s里和Spring Boot关系最紧密的对象它声明了期望的副本数、Pod模板和更新策略。下面是一个相对完整的参考apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: prod labels: app: order-service spec: replicas: 3 revisionHistoryLimit: 10 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: containers: - name: order-service image: harbor.example.com/ecommerce/order-service:1.2.3 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http envFrom: - configMapRef: name: order-service-config - secretRef: name: order-service-secret resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1 memory: 1Gi livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 20 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3几个关键参数说说怎么定。replicas副本数不是拍脑袋定的取决于QPS、单实例处理能力和期望的冗余。假设单实例能扛200QPS业务峰值在1000QPS左右那5个副本是起步再留20%左右的buffer所以5到6个比较合理。resources里的requests是调度依据limits是运行约束。我习惯把requests设成业务实际常常占用的量把limits设成requests的1.5到2倍。比如一个Spring Boot服务刚启动占300MB内存正常运行后500MBrequests给512Milimits给1Gi。CPU单位m是毫核500m就是0.5核。limits不要设太高否则某个Pod异常吃CPU时可能把整个节点拖垮。探针配置是整个Deployment里最需要调的地方。readiness负责流量接入liveness负责容器存活。initialDelaySeconds要根据Spring Boot实际启动时间调整我这个例子假设启动10到20秒。如果你的服务启动要40秒探针就得等一等否则刚起来还没监听端口就被探针打死形成CrashLoopBackOff。3.3 Service与Ingress让流量稳定地进来Pod的IP在K8s里是随时变化的不能直接依赖。Service提供了一组Pod的稳定访问入口通过标签选择器关联Pod并把流量负载均衡到每个Pod。Service类型有四种按使用场景区分Service类型适用场景说明ClusterIP内部服务调用只在集群内可达服务间调用首选NodePort测试环境把端口暴露在每个节点上外部可用节点IP访问LoadBalancer公有云云平台自动创建负载均衡器把公网流量接到ServiceExternalName服务映射给外部域名做别名偶尔有用Spring Boot微服务之间的调用一般直接用ClusterIP。比如订单服务要调用户服务只需要通过http://user-service:8080这个地址访问K8s会自动做负载均衡应用层面不需要感知对端Pod的具体IP。下面是订单服务的Service定义apiVersion: v1 kind: Service metadata: name: order-service namespace: prod spec: type: ClusterIP selector: app: order-service ports: - name: http port: 8080 targetPort: httpport是Service暴露的端口targetPort指向Pod里的容器端口。把端口命名成http方便后面Ingress配置时引用可读性也好。如果要把服务暴露到公网我会在前面加一层Ingress域名路由。下面的配置把api.example.com/order路径转发到order-service/user路径转发到user-serviceapiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: api-ingress namespace: prod spec: ingressClassName: nginx rules: - host: api.example.com http: paths: - path: /order pathType: Prefix backend: service: name: order-service port: number: 8080 - path: /user pathType: Prefix backend: service: name: user-service port: number: 8080Ingress Controller我用得最多的是Nginx Ingress和TraefikNginx稳定性更强Traefik配置更简洁。具体选型看团队熟悉度功能上差别不大。3.4 ConfigMap与Secret配置解耦的完整实践我在第2章提到要把配置外部化这里展示具体怎么做。ConfigMap管理非敏感配置apiVersion: v1 kind: ConfigMap metadata: name: order-service-config namespace: prod data: SPRING_PROFILES_ACTIVE: prod SERVER_PORT: 8080 LOG_LEVEL: INFO REDIS_HOST: redis-master.default.svc.cluster.local REDIS_PORT: 6379Secret管理敏感配置apiVersion: v1 kind: Secret metadata: name: order-service-secret namespace: prod type: Opaque stringData: DB_URL: jdbc:mysql://mysql-db:3306/order?useSSLfalseserverTimezoneUTC DB_USERNAME: order_app DB_PASSWORD: ChangeMe123在Deployment的envFrom里引用Pod启动后环境变量就会被注入成这些配置值。注意Secret用stringData写的是明文在YAML文件里就能直接看到。如果对安全性要求比较高建议用外部Secret存储方案或者把Secret做加密这个按团队安全制度来。还有一个很实际的坑ConfigMap和Secret如果改了线上Pod不会自动更新环境变量。想让配置生效要么重启Deployment让Pod重建重新注入要么用Reloader这类工具监听变化自动滚动重启。我个人习惯是配置变更也算发布动作直接改完ConfigMap后重启Deployment保持发布流程的一致性。3.5 水平自动扩缩容HPASpring Boot应用上K8s后的一个重要红利就是弹性。HPA可以根据CPU、内存等指标自动调整副本数。我的订单服务早年配置过这样的HPAapiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: order-service-hpa namespace: prod spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: order-service minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60解释一下平均利用率的意思。averageUtilization: 60表示所有Pod的CPU平均使用量达到requests值的60%时HPA开始扩容。例如requests是500m当所有副本平均实际使用到300m也就是60%的时候就会增加副本。反过来持续一段时间低于设定值会缩容。HPA每5秒采集一次指标但缩容有默认冷却时间5分钟内不会立刻缩下去这个机制是为了防止指标抖动导致频繁扩缩。如果你的业务流量有明显的潮汐特征还可以用KEDA做定时伸缩或基于消息队列长度的伸缩这个看具体业务形态再决定。4. 可观测性健康检查、监控与日志部署只是开始真正的运维难点在观测。应用写得再稳线上出了问题也必须快速定位没有可观测性的K8s集群光靠kubectl logs是很低效的。4.1 把Actuator探针和K8s探针联动起来Spring Boot 2.3之后Actuator增加了liveness和readiness两个独立端点分别对应K8s的存活探针和就绪探针。这是官方面向云原生场景独立设计的比直接用原始的/actuator/health更合理。为什么这样说因为原始的health会把数据库、Redis等组件状态全部汇总。如果数据库短暂抖动health返回DOWNK8s会误以为Pod挂了把它重启但重启并不能恢复数据库反而可能造成雪崩。liveness和readiness的拆分思路是readiness探针检查应用是否具备接收流量的能力可以包含下游依赖状态。下游未就绪就先别把流量打进来。liveness探针只检查JVM进程本身是否存活。进程还活着就不轻易重启避免无意义的反复拉起。实践配置里application.yml开启探针端点的写法是management: endpoints: web: exposure: include: health,info,prometheus,metrics health: probes: enabled: true endpoint: health: show-details: always这样配置后/actuator/health/liveness和/actuator/health/readiness就能正常访问了。再配合Deployment里的readinessProbe和livenessProbe配置流量接入和进程存活就被区分开了。4.2 接Prometheus监控Spring Boot指标Spring Boot的指标监控我一般配合Micrometer使用加上micrometer-registry-prometheus依赖后Spring Boot会自动暴露Prometheus格式的/actuator/prometheus端点。Prometheus通过服务发现拉取指标Grafana做可视化展示。这个链路能拿到的指标非常全JVM堆内存、GC次数、线程池状态、Tomcat活跃线程数、HTTP请求耗时全部都是标准化格式。指标有了之后告警规则也要配上比如堆内存超过85%持续5分钟就告警groups: - name: springboot-alerts rules: - alert: HighHeapMemoryUsage expr: jvm_memory_used_bytes{areaheap} / jvm_memory_max_bytes{areaheap} 0.85 for: 5m labels: severity: warning如果你不想自己搭一套Prometheus先用K8s自带的metrics-server看CPU和内存也可以但JVM层面的指标看不到排查Java服务问题很容易抓瞎。做Java服务的生产环境Prometheus加Grafana基本是标配。4.3 日志收集JSON化与采集方案K8s里的Pod是随时变化的登录Pod用kubectl logs只能做临时排查。要持久化日志我推荐两条路线轻量方案是Pod日志以stdout输出通过Fluent Bit收集到Loki标准方案是如果公司已有ELK直接把日志打到Kafka或Logstash再入ES。对Spring Boot应用日志格式建议统一为JSON这一点很容易被忽略。非结构化日志只能全文搜索而JSON格式可以根据level、traceId、serviceName直接过滤排障效率完全是两个级别。数一下你每次排障时在日志里翻了多少页就知道结构化日志有多重要。logback配置里加一个JSON编码器用logstash-logback-encoder这个库appender nameJSON classch.qos.logback.core.ConsoleAppender encoder classnet.logstash.logback.encoder.LogstashEncoder/ /appender日志以JSON格式打到stdout后K8s采集时不需要额外解析后续接入任何日志平台都能直接处理。5. 常见问题与排查实战记录这一章我整理了迁移过程中真实遇到过的几个问题每个都附排查思路而不是只给一个现象查询表。5.1 Pod一直CrashLoopBackOff现象kubectl get pods看到Pod反复重启。排查分三步。第一步kubectl logs pod-name看应用日志。Spring Boot应用启动失败日志里通常会抛出异常堆栈最常见的是端口被占用、数据库连接不上、配置缺失。第二步kubectl describe pod pod-name看Events。有时日志是正常的但容器被K8s主动杀掉Events里会写原因比如Liveness probe failed或者OOMKilled。第三步如果是探针失败调整探针参数。启动慢就加大initialDelaySeconds探针太频繁就调大periodSeconds。Spring Boot启动阶段有个容易忽略的细节它启动时会在控制台打印Banner如果日志刷屏老版本应用在10秒内还没监听端口readiness探针就开始打这个端口一打失败就重启。所以启动慢的应用一定要给足够的启动等待时间。5.2 ImagePullBackOff镜像拉不下来现象Pod状态显示ImagePullBackOffEvents里是Failed to pull image。原因基本就这么几类镜像tag对不上、私有仓库未配置认证、节点到仓库网络不通、insecure-registry配置缺失。我的定位方法是先在节点上手动执行一次docker pull。如果节点上能拉下来说明K8s侧的imagePullSecret或权限配置有问题如果节点上也拉不下来那就是仓库或网络的问题。这个二分法定位非常高效。另外提一句如果你push镜像用了latest标签偶尔会出现拉下来的镜像还是旧缓存。加个imagePullPolicy: Always可以强制每次拉取。当然最好还是回到我在第2.2节说过的建议从一开始就使用带明确版本号的tag。5.3 滚动更新卡住或流量中断现象更新Deployment镜像后新版本一直起不来旧版本也退不掉或者更新期间请求出现502。核心原因通常是readiness探针配置不当。滚动更新的逻辑是新Pod完全就绪才开始终止旧Pod。如果新Pod永远不就绪比如探针路径配错、应用启动失败滚动更新就会一直卡着。另一个坑是maxUnavailable为0、maxSurge为1的组合它保证没有Pod同时下线但要求集群先能腾出资源启动新Pod。如果集群资源不足新Pod会一直Pending更新同样被卡住。资源紧张的集群要么提前预留资源要么调整maxSurge和maxUnavailable的数值。流量中断还有一个隐蔽原因Spring Boot的优雅停机没配置。K8s终止Pod时先发SIGTERM如果你的应用直接硬退出存量请求就会断掉。正确做法是在Spring Boot 2.3以上配置server.shutdown: graceful同时给Pod设置一个合理terminationGracePeriodSeconds比如30秒让应用处理完当前请求再退出。对应的回滚命令是kubectl rollout undo deployment/order-service操作要快先把服务恢复再慢慢查原因。5.4 数据库连接池和副本数不匹配这是一个非常容易踩到生产环境大坑的问题。Spring Boot默认用HikariCP默认最大连接池大小是10。如果服务有10个副本全都连同一个数据库数据库要承受100个连接。副本扩到20连接池瞬间变成200数据库连接数被打满其他服务也会跟着受牵连。到K8s环境后建议把数据库连接池上限和副本数一起评估。比如HPA最大10个副本单副本连接池限制10那数据库连接上限至少要大于100还要再留一些给其他用途。反过来想也可以连接池最大连接数等于数据库可用连接数除以预期副本数再预留一部分冗余。这种问题在单机部署时根本不会暴露一旦容器化多副本化就立刻显现属于上K8s后最容易忽视的连带问题。5.5 时区和内存参数导致的奇怪问题说两个看起来小、实际影响很大的问题。第一个是时区。很多基础镜像默认是UTC业务日志常用的是北京时间。不统一时区的话日志时间和数据库时间可能差8小时排障时时间线对不上非常难受。解决方式是在Deployment里加环境变量TZAsia/Shanghai或在Dockerfile里安装tzdata并设置时区二选一都行。第二个是JVM内存感知问题。老版本JVM对容器内存限制的感知有限可能出现JVM在limits之外申请内存然后被K8s莫名杀掉。如果用的是Java 8至少用8u191之后的版本配合-XX:UseContainerSupport。Java 17默认已经支持容器感知但为了保险我还是会在启动参数里显式设置-Xmx取值不要超过limits的70%给元空间和堆外内存留余地。6. 一些提升部署效率的经验技巧最后分享几个我日常使用很顺手的习惯不一定每个团队都适用但实践下来确实能省不少事。6.1 用Helm统一管理部署模板Deployment、Service、ConfigMap、Secret这些YAML如果每个服务都写一份维护成本会很高。Helm把公共模板抽象出来用values文件区分环境。我现在每个Spring Boot服务一个Chart不同环境传不同values部署就是一条命令helm upgrade --install order-service ./charts/order-service -f values-prod.yaml这套做法的好处是新人上手成本低环境差异完全靠values隔离回滚用helm rollback一条命令完成。如果嫌Helm太重Kustomize也是不错的轻量替代但Helm在模板能力和社区生态上更成熟。6.2 把部署嵌入CI/CD流水线K8s部署只解决了最终怎么跑起来代码提交、构建镜像、推仓库、更新Deployment还是要靠流水线串起来。我现在的流程是代码push主分支GitLab CI触发构建Maven编译打包构建镜像推到Harbor最后更新K8s Deployment整个流程10分钟左右。流水线里一定要加一个生产环境的人工确认环节。见过有人把自动发布直接怼到生产结果一个配置没改对一堆Pod循环重启。生产发布前让人review一遍配置能拦截大部分低级错误。6.3 本地开发贴近K8s环境的方案有团队问过我本地开发怎么模拟K8s环境。两个方向可以试。第一个是minikube或k3s适合在单机跑个完整集群做体验。minikube对资源要求高内存至少8G起步k3s轻量很多适合笔记本。这类环境适合验证性能和真实集群差距还是明显的。第二个是Telepresence或kt-connect这类工具让本地进程直接接入集群网络。你可以在本地跑一个Spring Boot进程让它直接调测试集群里的其他微服务。这个方案对微服务联调体验提升很大特别是你只想改自己负责的那个服务不需要把整套环境拉到本地的时候。我个人的开发环境就是这样本地起了几个Spring Boot微服务通过Telepresence连到测试集群随时可以热调试不用反复打包部署体验比纯本地跑整套环境舒服太多。到这里整套部署链路基本讲完了。最后说一个我自己的体会K8s的文档、视频、资料特别多但真正有价值的东西往往是在生产环境里踩过坑之后总结出来的判断。上面写的探针参数、资源配比、优雅停机、连接池评估这些都不是抄来的模板而是从一个个线上问题里排出来的做法。如果你照着这套流程去部署再结合自己业务的实际情况调整参数大概率能比那些只跑通demo就开始生产的人少走很多弯路。K8s不是银弹但对于Spring Boot这类Java应用的部署和运维它确实是一套值得长期投入的方向。
返回列表