ARTICLE DETAIL

资讯详情

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

Spring Boot上Kubernetes部署全指南:镜像、HPA与问题排查

Spring Boot上Kubernetes部署全指南:镜像、HPA与问题排查 1. 项目概述与整体设计思路1.1 为什么要把Spring Boot项目搬上Kubernetes先说个背景。我以前带团队的时候遇到过不止一次这样的窘境Spring Boot服务跑得好好的突然某个接口的延迟飙到几秒等我们打开服务器一看CPU已经跑满了但问题是谁也说不清是哪一时刻开始满的、哪个实例先撑不住的。后来我发现这种问题光靠把服务跑起来是解决不了的真正需要的是一个能自动扩容、自愈、能平滑发布的基础设施层。这就是我决定把所有Spring Boot项目统一迁移到Kubernetes上的核心原因。Kubernetes能解决Spring Boot在传统部署方式下最痛的三件事第一滚动升级。以前发布一个新版本要么停机要么用脚本手动逐个摘流量、替换、再挂回去一不小心就出事故在Kubernetes里用Deployment做滚动更新天然就是先起新的、确认健康、再杀掉旧的不需要人工干预。第二自动扩容。Spring Boot应用很容易出现流量突增Kubernetes的HPA能根据CPU、内存甚至自定义指标自动增减Pod副本数半夜流量低的时候甚至能缩到1个副本省资源。第三故障自愈。节点宕机、Pod被驱逐、容器OOMKubernetes会自动把Pod调度到其他可用节点上重新拉起而不是等你半夜被监控告警吵醒。这个方案适合谁来参考我的建议是如果你手里的Spring Boot项目已经超过两三个或者你的服务已经开始出现扩机器靠手动、回滚靠祈祷的情况那你就值得认真看完这篇文章。本文会从镜像构建、资源清单编写、HPA配置到常见问题排查把整条部署链路完整走一遍你可以直接抄作业。1.2 部署架构的整体规划在动手之前先花两分钟想清楚整体架构能少走很多弯路。一个典型的Spring Boot项目上Kubernetes涉及的组件大概有这些组件作用对应Kubernetes资源应用镜像承载Spring Boot应用的运行环境Docker镜像应用编排定义Pod副本数、更新策略、健康检查Deployment服务暴露让集群内外能访问到PodService、Ingress配置管理分离环境配置与应用镜像ConfigMap、Secret弹性伸缩根据负载自动扩缩容HPA存储有状态数据持久化PV、PVC一般Spring Boot无状态项目可不配Spring Boot项目本身基本是无状态应用数据库、缓存、文件存储都外置所以整个部署设计的重点就落在如何把无状态应用跑稳、跑好上。你不需要一开始就追求大而全的微服务网格先把上面这张表里的资源配好就已经超过绝大多数团队的运维水平了。这里特别说一句不要一上来就上Istio、不要一上来就搞Service Mesh。很多团队在还没把Deployment和探针配明白的时候就急着上服务网格结果出了问题都不知道是应用的问题还是Sidecar的问题。我的经验是先把最基础的Controller、Service、Ingress、HPA这套体系跑通如果确实有灰度发布、全链路追踪的需求再考虑引入更重的方案。2. 环境准备与工具选型2.1 集群环境的选择Kubernetes集群本身部署有很多种方式我按使用场景给大家排个序本地开发调试验证首选Minikube或Kind。Minikube自带一个Node适合验证YAML写没写对Kind可以直接在Docker里跑起一个多节点的K8s集群测试调度行为也很方便。生产环境自建集群用kubeadm手工搭建或者用k3s这种轻量级发行版。kubeadm标准、可控适合对K8s有一定了解的团队k3s部署简单、资源占用低在边缘场景或者小规模生产环境非常合适。云平台托管服务如果不想自己维护Master节点的HA直接用托管的K8s服务是最省心的选择。节点坏了、证书要轮换了、etcd要备份了这些都不用你操心。我这里讲一个选型时的关键考虑不要只图安装方便要看你的团队有没有能力运维Kubernetes本身。Kubernetes不是一个装完就完事的软件它是一套需要持续运维的系统——证书更新、版本升级、etcd备份、节点维护哪一样都得有人管。如果团队里没有人能扛住这件事我更推荐先用托管服务把业务跑起来等到对K8s的运维有底了再考虑自建集群。如果你决定本地起一个环境来跟着本文操作我建议直接装Minikube。安装很简单装完执行# 启动一个2核4G的Minikube集群 minikube start --cpus2 --memory4096 # 确认集群状态 kubectl get nodes2.2 命令行工具与依赖准备除了集群本身你还需要准备以下几样东西kubectl操作Kubernetes的命令行工具建议版本和集群版本保持在大版本内一致一般向下兼容问题不大。Docker用于构建Spring Boot项目镜像。Helm可选如果后面要部署Nginx Ingress Controller、Metrics Server这类组件用Helm会省事很多。IDE插件IntelliJ IDEA的Kubernetes插件能直接看YAML的schema错误写资源清单的时候很有帮助。这里我特别想提醒一个坑kubectl的版本最好不要比集群版本新太多差一个大版本在某些资源API的兼容性上会出问题。比如集群是1.24你拿1.30的kubectl去操作某些CRD的转换可能会出现异常。稳妥的做法是让kubectl版本和集群版本保持小版本接近。3. Spring Boot应用镜像构建与优化3.1 多阶段构建告别臃肿镜像Spring Boot应用最常见的镜像构建方式就是把打好的jar包塞进一个带JDK的基础镜像里然后启动。这种方式本身没问题但如果你的基础镜像用的是带完整JDK的centos或者ubuntu镜像体积动辄500MB甚至1GB在镜像下载和存储上都是负担。我用的方案是多阶段构建而且分层利用Spring Boot官方提供的构建缓存。先看一个最直接的Dockerfile# 第一阶段使用Maven镜像构建应用 FROM maven:3.8-openjdk-17 AS builder WORKDIR /app # 先只拷贝pom.xml利用Docker layer缓存依赖下载结果 COPY pom.xml . RUN mvn dependency:go-offline -B # 再拷贝源码并打包 COPY src ./src RUN mvn clean package -DskipTests # 第二阶段使用精简JRE镜像运行 FROM eclipse-temurin:17-jre-jammy WORKDIR /app # 从builder阶段拷贝jar包 COPY --frombuilder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]这个Dockerfile里有一个细节特别值得说COPY pom.xml .之后先执行RUN mvn dependency:go-offline -B再拷贝源码。这样做的原因是Docker构建时每一行指令都会形成一个layer只要这一行之前的输入没变这个layer就可以直接复用。如果你先把源码全部拷贝进去再执行打包那么只要任何一行代码变了整个依赖下载的过程都要重来一遍。把pom.xml单独拷进去先拉依赖能让日常构建速度提升一个量级。第二阶段选择eclipse-temurin的JRE镜像而非JDK镜像也是做过权衡的。运行阶段只需要Java运行时不需要编译器用JRE可以把镜像体积压到200MB以内。如果你对体积有更极致的追求也可以尝试JLink自定义运行时但那是进阶话题这里不展开。3.2 容器里的JVM内存参数一个必须处理的大坑Spring Boot项目在Kubernetes里最常见的一个问题就是明明给Pod限了1Gi内存Java进程却直接OOMKilled。问题根源在于旧版本的JVM默认不感知容器的cgroup内存限制。JVM在启动时会读取宿主机/物理机的内存总量来设置默认的堆大小如果宿主机有32G内存JVM就认为堆可以占到25%也就是8G一旦应用跑起来很快就触发了Pod的内存上限直接被OOMKilled。解决办法有两个方向。方向一是在启动参数里手动指定堆大小java -Xmx512m -Xms512m -jar app.jar方向二是使用JVM的容器感知参数。JDK 8u131以及JDK 10都支持以下参数java -XX:MaxRAMPercentage75.0 -XX:InitialRAMPercentage75.0 -jar app.jar这两者我更推荐第二种。手动指定-Xmx是死值一旦你调整了Pod的内存limit还得同步去改启动参数很容易忘记而MaxRAMPercentage是活的比例JVM会根据容器实际的内存限制自动计算堆大小。比如Pod limit是2Gi那JVM堆最大约1.5Gi剩下500MB留给堆外内存、线程栈、Metaspace这个配比比较合理。实践经验是Pod的内存limit不要只比应用实际占用多一点点建议留出30%左右的余量。Java应用除了堆内存还有Direct Buffer、线程栈、Metaspace这些堆外内存如果limit卡得太死任何一处稍微超了一点Pod就没了。4. Kubernetes资源清单的设计与编写4.1 Deployment清单副本、探针、滚动更新一把抓Deployment是Kubernetes里最核心的编排对象。很多初学者只会在里面写image和replicas跑起来能用就以为万事大吉。但其实Deployment里藏着两个决定线上稳定性的关键设计探针Probe和滚动更新策略。先看一个完整的示例apiVersion: apps/v1 kind: Deployment metadata: name: spring-boot-demo namespace: default labels: app: spring-boot-demo spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: spring-boot-demo template: metadata: labels: app: spring-boot-demo spec: containers: - name: spring-boot-demo image: registry.example.com/demo/spring-boot-demo:v1.0.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: prod resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1 memory: 1Gi startupProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 30 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 3 livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 15 periodSeconds: 10 failureThreshold: 3这里我逐个拆一下关键点。spring-boot-starter-actuator部署时必加。上面探针里请求的/actuator/health端点来自Spring Boot的Actuator模块。Kubernetes的三种探针都需要一个有意义的健康检查入口而Actuator天然提供了这个能力。如果没有Actuator你只能探TCP端口但TCP端口通了不代表应用真的能处理请求了。在application.yml里记得开放management: endpoints: web: exposure: include: health,info endpoint: health: probes: enabled: true show-details: always为什么需要三种探针我用自己的踩坑经历说清楚。很多Spring Boot应用启动过程要加载配置中心、初始化数据库连接池、预热缓存启动时间可能超过30秒。如果只配置livenessProbeK8s会在应用还没启动完成时就发起健康检查连续几次失败就直接把Pod杀掉重启造成启动即崩溃的循环。所以我在Deployment里加了startupProbe——它专门用于判断应用是否完成启动在startupProbe通过之前livenessProbe不会生效。spring boot 2.6项目里为了和探针配合通常还会开启management.endpoint.health.probes.enabledtrue这样/actuator/health会区分/actuator/health/liveness和/actuator/health/readiness两个端点分别对应存活和就绪状态。readinessProbe控制流量进出。当一个Pod还没就绪Service不会把请求转发给它而readinessProbe失败时Pod会从Service的Endpoints里摘除但不重启。这就保证了服务不可用就不收流量。livenessProbe则负责如果应用死锁了就杀掉重启。两者职责完全不同千万不能混用。滚动更新策略的生产级配置strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0这个配置的含义是更新时先新起1个Pod等它ready后再停止1个旧Pod整个过程始终保持至少3个副本可用maxUnavailable: 0意味着任何时刻都不能有Pod不可用。这样就能做到发布过程中对业务零影响。如果你的环境资源比较紧张可以考虑maxSurge: 1, maxUnavailable: 1允许短暂降级到2个副本但线上流量大的服务我不建议这么配。4.2 资源请求与限制给Pod划好预算resources那段配置我列一下我的设计基线resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1 memory: 1Girequests是调度依据limits是运行约束。Kubernetes调度器会根据requests来选节点——如果一个节点剩余可分配内存只有400Mi而Pod request要512Mi那它就不会被调度到这个节点上。而limits决定了Pod在运行时最多能用多少超过就会被限制或杀掉。给Spring Boot设置CPU的request时有个经验值可以参照绝大多数内部管理类Spring Boot服务500m半核CPU的request已经足够如果是对外提供高并发接口的服务可以考虑1到2核。这里特别提醒一句不要一上来就把limits设成和requests一样。如果应用确实有突发流量需要更多CPU而limits卡死在上限就只能等Pod被CPU Throttle延迟直接上升。CPU是可压缩资源适当放大limits的比例没问题但内存是不可压缩资源limits一旦超过就会OOMKilled所以内存的limits一定要比应用实际峰值占用留出足够余量。4.3 Service与Ingress让外部流量进来Deployment只管Pod的生命周期Service负责把一组Pod抽象成一个稳定的访问入口。Spring Boot服务一般写成这样apiVersion: v1 kind: Service metadata: name: spring-boot-demo-service spec: type: ClusterIP selector: app: spring-boot-demo ports: - name: http port: 80 targetPort: 8080这里port: 80是Service暴露的端口targetPort: 8080是Pod里容器的端口。集群内部通过spring-boot-demo-service.default.svc.cluster.local访问也就是服务名.命名空间.svc.cluster.local。如果只是集群内部其他服务调用这个Spring Boot接口那ClusterIP就够用了。如果要对公网提供访问我建议不要在Service层直接用NodePort而是用Ingress来统一收口apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: spring-boot-demo-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: /$2 spec: ingressClassName: nginx rules: - host: demo.example.com http: paths: - path: /api(/|$)(.*) pathType: ImplementationSpecific backend: service: name: spring-boot-demo-service port: number: 80Ingress的好处显而易见域名、TLS证书、路由规则、限流配置全部在一个入口处管理不用每个服务都暴露一个NodePort。生产环境通常用Nginx Ingress Controller或云厂商的Ingress方案只需要在集群里安装一次Ingress Controller就行。关于TLS证书很多团队会自己手工上传证书然后手动改Ingress配置。其实更推荐的方式是部署cert-manager配合Lets Encrypt自动签发和续期证书这个和标题里那个certum证书自动部署的热搜词是同一条思路——证书的自动轮转对线上稳定性非常重要。手工上传证书后到期忘换导致服务告警的案例我见得太多了。4.4 ConfigMap与Secret把配置从镜像里解放出来Spring Boot的配置天然支持外部化优先加载环境变量、SPRING_CONFIG_LOCATION指向的外部文件等。在Kubernetes里最常见的做法就是用ConfigMap承载非敏感的配置文件用Secret承载敏感信息然后用环境变量注入或者挂载文件的方式进入容器。先说ConfigMap挂载配置文件的方式。假设你的Spring Boot需要自定义一份application-prod.ymlapiVersion: v1 kind: ConfigMap metadata: name: spring-boot-demo-config data: application-prod.yml: | server: port: 8080 spring: datasource: url: jdbc:mysql://mysql-service.default.svc.cluster.local:3306/demo username: demo_user password: ${DB_PASSWORD}然后在Deployment里把这份配置挂载进容器volumeMounts: - name: config-volume mountPath: /config volumes: - name: config-volume configMap: name: spring-boot-demo-config启动参数里指定env: - name: SPRING_CONFIG_LOCATION value: /config/这个方案的好处是镜像完全和环境解耦。测试环境的镜像和生产环境的镜像可以是同一个区别只在于挂载的ConfigMap不同。你发布一个新版本镜像tag变了但配置还是同一份或者只是改个配置项不用重新构建镜像直接更新ConfigMap再滚动重启Deployment就能生效。不过要注意ConfigMap更新后已运行的Pod不会自动感知需要手动执行kubectl rollout restart deployment/spring-boot-demo来让新配置生效。敏感信息用Secret比如数据库密码apiVersion: v1 kind: Secret metadata: name: spring-boot-demo-secret type: Opaque data: db-password: bXlwYXNzd29yZASecret的value要求是base64编码所以bXlwYXNzd29yZA实际是mypassword。这里必须提醒一句Secret默认只是base64编码不是加密。任何有权限读取集群etcd或者Secret对象的人都能拿到明文。如果你的安全要求高一定要配KMS加密或者用External Secrets方案从云厂商的密钥管理服务同步。在Deployment里引用Secret时推荐用环境变量的方式env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: spring-boot-demo-secret key: db-password这样Spring Boot的application.yml里只要写password: ${DB_PASSWORD}就能读到。把敏感信息以环境变量注入比对容器直接挂载Secret文件更常见也更方便Spring Boot用占位符读取。4.5 HPA让副本数跟着流量走HPAHorizontalPodAutoscaler是我强烈建议每个线上Spring Boot服务都配置的对象。它能让Pod副本数根据CPU、内存或者自定义指标自动调整。以CPU为基准的最简配置如下apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: spring-boot-demo-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: spring-boot-demo minReplicas: 2 maxReplicas: 8 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70这个HPA的含义是以Deployment的CPU使用率为指标控制在70%的utilization目标副本数在2到8之间动态调整。HPA扩容的算法官方文档的公式是期望副本数 ceil(当前副本数 × 当前指标值 / 期望指标值)。举个例子当前2个副本CPU使用率各100%目标70%那么期望副本数就是ceil(2 × 100/70) ceil(2.86) 3。要使用CPU指标的HPA集群里必须装Metrics Server。很多自建集群默认没有装结果配置了HPA却永远不生效。用Helm安装很简单helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/ helm upgrade --install metrics-server metrics-server/metrics-serverHPA还有一个容易被忽略的细节Pod的resources.requests必须设置因为HPA计算CPU利用率时是用当前CPU使用量 / requests里的CPU值来得到utilization的。如果你没写requestsMetrics Server可能无法计算出一个合理的利用率百分比HPA就失去了扩容依据。5. 实操部署流程全记录5.1 构建镜像并推送私有仓库先给镜像打好tag并推送到私有仓库。生产环境基本都会用私有的镜像仓库比如Harbor、Nexus或者云厂商的容器镜像服务。命令如下# 构建镜像指定平台避免在arm节点的集群上拉取失败 docker build -t registry.example.com/demo/spring-boot-demo:v1.0.0 . # 登录私有仓库生产建议使用CI中的机器人账号 docker login registry.example.com # 推送镜像 docker push registry.example.com/demo/spring-boot-demo:v1.0.0这里有一个我踩过的坑如果你的K8s集群是混合架构比如有些节点是arm64、有些是amd64直接docker build构建出来的镜像可能只适合当前机器架构。比较稳妥的做法是用docker buildx构建multi-arch镜像docker buildx build --platform linux/amd64,linux/arm64 \ -t registry.example.com/demo/spring-boot-demo:v1.0.0 \ --push .否则你的Pod可能被调度到架构不匹配的节点上启动时直接报exec format error。5.2 应用资源清单假设你已经在Git仓库里维护好了所有YAML文件标准的发布流程是这样# 创建/更新命名空间 kubectl create namespace demo-namespace # 应用配置和Deployment kubectl apply -f configmap.yaml kubectl apply -f secret.yaml kubectl apply -f deployment.yaml kubectl apply -f service.yaml kubectl apply -f ingress.yaml kubectl apply -f hpa.yaml重要提醒配置文件应该走GitOps流程管理。我强烈建议所有K8s资源清单存入Git仓库发布时用kubectl apply或CI/CD流水线自动执行。很多人习惯在服务器上直接改YAML文件改完也不知道改了什么等出问题的时候根本没法回滚。GitOps不仅仅是规范问题更是生产和测试环境一致性、可审计性的基础。应用完成后查看整个部署状态# 查看Pod状态 kubectl get pods -n demo-namespace -o wide # 查看Deployment滚动更新状态 kubectl rollout status deployment/spring-boot-demo -n demo-namespace # 查看Service和Endpoints kubectl get svc,ep -n demo-namespace正常情况下你会看到类似这样的输出NAME READY STATUS RESTARTS AGE spring-boot-demo-7d8f9b6c9f-abcde 1/1 Running 0 2m spring-boot-demo-7d8f9b6c9f-fghij 1/1 Running 0 2m spring-boot-demo-7d8f9b6c9f-klmno 1/1 Running 0 2m三个副本全部处于Running且READY为1/1的状态说明探针已经通过服务正常对外提供流量。5.3 日志查看与排障操作查看Pod日志是我日常用得最多的排障手段# 实时查看某个Pod的日志 kubectl logs -f spring-boot-demo-7d8f9b6c9f-abcde -n demo-namespace # 查看某个Deployment下所有Pod的日志前一版本 kubectl logs -f deployment/spring-boot-demo -n demo-namespace --previous # 进入Pod内部排查 kubectl exec -it spring-boot-demo-7d8f9b6c9f-abcde -n demo-namespace -- /bin/sh这里的--previous是一个容易被忽略但非常有用的参数。如果容器崩溃重启了旧的日志被新容器覆盖掉用--previous能看到崩溃前的容器日志这对排查CrashLoopBackOff至关重要。部署后的访问验证一般先验证集群内部连通性再走Ingress验证外部访问# 在集群内临时起一个curl Pod做连通性测试 kubectl run curl-test --imagecurlimages/curl -it --rm -- sh # 进入交互shell后 curl http://spring-boot-demo-service.default.svc.cluster.local/actuator/health这一步的目的是绕开Ingress直接验证Service链路是否正常。如果Service链路通了但Ingress不通问题就出在Ingress配置或者Ingress Controller本身。5.4 滚动更新与快速回滚发布新版本就是换一个镜像tag# 更新镜像 kubectl set image deployment/spring-boot-demo \ spring-boot-demoregistry.example.com/demo/spring-boot-demo:v1.1.0 \ -n demo-namespace # 或者更推荐的方式直接apply更新后的YAML kubectl apply -f deployment.yaml # 观察滚动更新进度 kubectl rollout status deployment/spring-boot-demo -n demo-namespace如果滚动更新过程中发现了问题Kubernetes的滚动更新会自动暂停——当新Pod的readinessProbe连续失败时K8s不会继续杀掉旧Pod而是保持旧版本继续服务。这也是我坚持maxUnavailable: 0的原因整个更新过程对业务完全无感。需要手动回滚时# 回滚到上一个版本 kubectl rollout undo deployment/spring-boot-demo -n demo-namespace # 回滚到指定版本 kubectl rollout undo deployment/spring-boot-demo \ --to-revision3 -n demo-namespace # 查看历史版本 kubectl rollout history deployment/spring-boot-demo -n demo-namespace回滚操作在秒级之内就能完成这也是Kubernetes部署相比传统脚本部署的一个巨大优势——出了问题不再是跑服务器上把旧包重新拷一遍而是一条命令的事。6. 常见问题与排查技巧实录6.1 CrashLoopBackOff容器反复重启Pod状态显示CrashLoopBackOff说明容器不断崩溃Kubernetes在按退避策略反复重启它。遇到这个问题第一件事就是看日志kubectl logs spring-boot-demo-7d8f9b6c9f-abcde -n demo-namespace --previous --tail200Spring Boot项目最常见的CrashLoopBackOff原因就几类端口被占用、配置读取失败比如数据库连不上、启动时用了错的profile、jar包损坏。排除时按顺序排查先看日志里有没有明显的Exception或Error特别是Spring Boot启动失败时的APPLICATION FAILED TO START提示。用kubectl describe pod看事件。这个命令会显示容器的重启原因和最近的事件有时候会告诉你Back-off restarting failed container之外的隐藏信息。用kubectl exec进入容器确认我们之前配的JVM参数和启动命令是否生效ps aux看进程还在不在。一个我实际遇到过的案例项目本地用java -jar跑得好好的放到K8s里就CrashLoopBackOff。日志显示Web server failed to start. Port 8080 was already in use。排查发现是Docker镜像里用了eclipse-temurin:17-jre-jammy这个镜像默认暴露了8080端口吗其实不是是项目里某个内嵌组件抢先占用了8080端口。最后处理方法是启动命令中显式指定随机端口--server.port0或者调整内嵌组件的端口绑定。这类问题光看应用日志很难发现一定要配合describe看Pod里完整的事件链。6.2 ImagePullBackOff镜像拉不下来ImagePullBackOff意味着镜像拉取失败常见原因包括镜像tag不存在、私有仓库未登录、镜像仓库地址写错、节点上docker的registry配置不对。排查命令kubectl describe pod spring-boot-demo-7d8f9b6c9f-abcde -n demo-namespace输出里Events部分通常会给出具体的错误信息比如unauthorized: authentication required或者manifest unknown。对于私有仓库你需要提前创建imagePullSecret并且在Deployment的spec.template.spec里声明imagePullSecrets: - name: registry-secret这个坑很多人会踩明明创建了Secretkubectl get secret也能看到但Pod还是拉不下来。原因就是忘了在Deployment里引用imagePullSecretsSecret创建了不代表K8s会自动使用它。6.3 OOMKilled内存超限被内核击杀OOMKilled是Spring Boot容器最常遇到的死法。Pod状态显示OOMKilled时内核已经杀掉了容器进程。我的排查步骤是先看Pod的resources是否合理用kubectl describe pod看容器当时的events。看应用日志里有没有内存相关的错误特别是有没有java.lang.OutOfMemoryError: Java heap space。如果日志有堆溢出说明堆配置偏小如果日志没有连JVM的OOM日志都没打出来说明是cgroup层面直接杀的很可能是堆外内存超出limit。对照Pod的limit和你给JVM配的MaxRAMPercentage。如果Pod内存limit是1Gi你配了MaxRAMPercentage80那么堆最多约800MB再加上Metaspace、线程栈、Direct Buffer等堆外内存很容易撞到limit。我的经验是MaxRAMPercentage建议控制在60-75之间给堆外留够空间。如果确认是内存不够用优先考虑调大Pod的memory limit而不是只调JVM参数。因为Spring Boot应用的内存峰值受流量影响很大你很难用一个固定值精确预估留足余量才是稳妥的做法。6.4 探针配置不当引发的诡异流量问题这一节的坑我至少见过三四个团队踩过。最典型的现象是服务一切正常但偶尔有请求超时或者HPA缩容后出现间歇性不可用。问题通常出在readinessProbe的periodSeconds和failureThreshold设置不合理。比如readinessProbe每5秒检查一次failureThreshold: 3意味着15秒内连续失败才把Pod从Endpoints摘除。如果检查频率太低或者失败阈值太大一个正在缓慢死亡的实例会在15秒内持续接流量上游请求就会超时。更隐蔽的问题是Spring Boot的 Actuator health返回了UP但应用实际已经无法处理业务了。比如数据库连接池耗尽Actuator的health检查如果不带数据库探活它会返回UPreadinessProbe自然通过流量还是会打进来然后接口全部报错。这种情况下正确的做法是在Actuator里配置详细的健康检查指标management: health: db: enabled: true redis: enabled: true让/actuator/health真实反映数据库、Redis这些关键依赖的状态。如果你的服务对某一个外部依赖特别敏感把它纳入健康检查readinessProbe才能发挥它应有的价值。6.5 排查工具与方法小结整理一个速查表供平时遇到问题时对照使用现象可能原因排查命令/手段Pod Pending节点资源不足、调度约束不满足kubectl describe pod看EventsContainerCreating镜像拉取中、存储卷挂载失败kubectl describe pod、检查PVCCrashLoopBackOff应用启动失败、配置错误kubectl logs --previousImagePullBackOff镜像不存在、私有仓库鉴权失败kubectl describe pod看EventsOOMKilledJVM堆参数或Pod内存limit不合理查看日志、调整MaxRAMPercentageReadiness探针失败应用慢启动或依赖外部组件异常curl /actuator/health看详细状态服务间歇性超时探针周期过长、失败阈值过大调整Probe参数、详细健康检查另外推荐一个排查工具kubectl get events --sort-by.lastTimestamp它会按时间顺序列出所有事件有时候能看出问题的完整脉络比只盯着Pod本身强很多。7. 经验心得与后续演进建议做Kubernetes部署这件事技术上其实没什么玄学大坑小坑上面都列了。我真正想说的反而是两件事第一把基础设施层的部署当成研发流程的一部分而不是运维的杂活。很多团队把K8s的YAML丢给运维去维护开发和运维之间信息断层最后谁都不清楚线上跑的镜像是什么版本的、配置是什么时候改的。如果能让开发团队参与维护资源清单、把部署纳入CI/CD流水线整个效率会提升一个台阶。第二注意观察和监控的配套建设。Kubernetes的部署只是第一步Pod一旦多起来没有监控就会变成黑盒。Spring Boot自己的Actuator暴露的metrics配合Prometheus和Grafana可以形成一个不错的可观测性方案。我见到的所有线上事故几乎都是看得到症状、找得到原因两件事做反了——部署上线后没有第一时间建立监控等到出问题才临时去看日志往往已经晚了。最后再分享一个实际工作中的小习惯每次部署后立刻执行一次kubectl describe确认探针和资源限制配置真的生效了。很多YAML文件部署成功不代表配置正确——比如resources拼错了字段limits写成了limitK8s不一定报错只是配置没生效。部署后花10秒钟检查一下实际运行的Pod配置能避免一整天排查为什么我的配置没生效的苦功夫。
返回列表