前置知识: Java

Java 与 Kubernetes

11 min中级

Java 应用在 Kubernetes 上的部署完整指南:资源限制、健康检查、优雅停机、自动伸缩与云原生实践。

前置知识

学习目标

  • 掌握「0. 本节阅读指引(先读这一节)」的核心机制、典型用法与常见陷阱
  • 掌握「1. 历史动机与发展脉络」的核心机制、典型用法与常见陷阱
  • 掌握「2. 形式化定义」的核心机制、典型用法与常见陷阱
  • 掌握「3. 理论推导与原理解析」的核心机制、典型用法与常见陷阱
  • 掌握「4. 代码示例(带详尽注释)」的核心机制、典型用法与常见陷阱

0. 本节阅读指引(先读这一节)

本篇是「Java 与 Kubernetes」进阶文档。

第一遍只读:4. 代码示例(多阶段 Dockerfile、Deployment 清单)与 9. 知识要点总结;理解探针、优雅停机、滚动更新即可。

可跳过:1-3 节(历史、形式化、理论推导)与 5-8 节第二遍细读。

前置:092 Java 与 Docker。

1. 历史动机与发展脉络

Java 的“一次编写、到处运行”理念依赖 JVM,但传统部署中 Java 应用以 WAR 包部署到应用服务器(Tomcat、Jetty),环境差异与依赖冲突普遍存在。2013 年 Docker 兴起后,Java 应用开始容器化,但早期遇到两个典型问题:JVM 不感知容器 CPU/内存限制(导致 OOM 或被 cgroup 杀死),以及镜像体积过大(数百 MB 的 JDK 镜像)。

Java 10(2018)引入容器感知(-XX:+UseContainerSupport 默认开启),JVM 自动读取 cgroup 限制;Java 8 的 8u191+ 也通过 backport 支持。2019 年 Eclipse Adoptium 项目接管 OpenJDK 构建,发布 Eclipse Temurin 镜像。随后 jlink(Java 9+)支持定制最小运行时,Distroless 镜像与 GraalVM Native Image 进一步压缩体积。Kubernetes 2015 年开源后,Java 微服务成为其最典型的工作负载,Spring Boot 与 Spring Cloud Kubernetes 生态随之成熟。

timeline
    title Java 云原生演进
    2013 : Docker 发布,Java 容器化起步
    2018 : Java 10 容器感知 JVM
    2018 : Kubernetes 成为容器编排事实标准
    2019 : Eclipse Temurin 镜像发布
    2021 : Spring Boot 3 / GraalVM 原生镜像
    2024+ : Java LTS 21/25 云原生优化持续演进

2. 形式化定义

2.1 容器化定义

Java 应用的容器化是把 JVM 与应用程序封装进镜像,使其在任何符合 OCI 规范的运行时中一致运行。形式化表述:镜像 = 基础运行时(JRE)+ 应用制品(JAR)+ 启动配置(ENTRYPOINT)+ 元数据(ENV/EXPOSE/LABEL)。

2.2 Kubernetes 核心资源

Pod:调度与运行的最小单位,一个 Pod 可包含一个主容器与若干 sidecar 容器;

Deployment:声明式管理 Pod 副本、滚动更新与回滚;

Service:为 Pod 组提供稳定的虚拟 IP 与 DNS 负载均衡;

ConfigMap 与 Secret:把配置与敏感数据从镜像中剥离,挂载为环境变量或文件;

Ingress:七层 HTTP 路由,把外部流量分发到 Service;

HorizontalPodAutoscaler(HPA):按 CPU/内存/自定义指标调整副本数。

2.3 探针定义

readinessProbe:就绪探针,失败时从 Service 端点摘除,但不重启容器;

livenessProbe:存活探针,失败时按 restartPolicy 重启容器;

startupProbe:启动探针,用于 JVM 冷启动较慢的应用,成功前不执行其他探针。

flowchart LR
    A["Java 源码"] --> B["Maven/Gradle 构建"]
    B --> C["JAR 制品"]
    C --> D["多阶段 Dockerfile 构建镜像"]
    D --> E["镜像仓库"]
    E --> F["Kubernetes Deployment"]
    F --> G["Pod 运行 JVM"]
    G --> H["Service 负载均衡"]
    H --> I["Ingress 对外暴露"]

3. 理论推导与原理解析

3.1 JVM 与 cgroup 内存模型

JVM 的堆大小默认按物理内存的 1/4 计算。容器中物理内存是宿主机内存,若不感知 cgroup 限制,堆可能远超容器限额,触发 OOMKilled。容器感知开启后,JVM 读取 cgroup 内存上限,-XX:MaxRAMPercentage=75 表示堆上限为容器内存的 75%,为元空间、线程栈、直接内存等留出余量。

推导公式:容器内存限制 M,堆上限 H = M × MaxRAMPercentage。若 M=512Mi,H≈384Mi。应用实际使用中还应考虑:JVM 非堆(元空间、代码缓存)与 GC 开销通常占 25%-30%,因此 75% 是平衡值。

3.2 优雅停机推导

Kubernetes 滚动更新时向旧 Pod 发送 SIGTERM,等待 terminationGracePeriodSeconds(默认 30s)后发送 SIGKILL。Spring Boot 注册 shutdown hook,收到 SIGTERM 后停止接收新请求、完成进行中的请求(server.shutdown=graceful + spring.lifecycle.timeout-per-shutdown-phase)。若应用在期限内未退出,会被强杀,导致请求中断或数据不一致。

3.3 滚动更新推导

Deployment 的 RollingUpdate 策略:maxSurge(更新期间超出期望副本数的最大增量)与 maxUnavailable(允许不可用的最大副本数)共同控制更新速率。例如期望 3 副本、maxSurge=1、maxUnavailable=1:先起 1 个新 Pod,再停 1 个旧 Pod,交替直到全部更新。readinessProbe 决定新 Pod 是否加入 Service,从而避免流量打到未就绪实例。

4. 代码示例(带详尽注释)

4.1 多阶段 Dockerfile

# 第一阶段:构建环境(包含完整 JDK 与构建工具)
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app

# 先复制构建描述文件,利用 Docker 层缓存
COPY pom.xml .
RUN mvn dependency:go-offline

# 复制源码并打包
COPY src ./src
RUN mvn package -DskipTests

# 第二阶段:运行环境(只包含 JRE,镜像更小)
FROM eclipse-temurin:21-jre
WORKDIR /app

# 从构建阶段复制 JAR 制品
COPY --from=builder /app/target/*.jar app.jar

# 容器感知 JVM 参数:堆上限为容器内存 75%
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75 -XX:+UseContainerSupport"

# 非 root 用户运行,降低安全风险
RUN useradd -m appuser
USER appuser

EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

讲解:多阶段构建把构建工具链与运行环境分离,最终镜像只含 JRE 与 JAR。dependency:go-offline 预下载依赖以利用缓存层;USER appuser 避免以 root 运行;MaxRAMPercentage 是容器 Java 的核心参数。

4.2 Deployment 清单

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  labels:
    app: order-service
spec:
  # 期望副本数
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: registry.example.com/order-service:1.4.2
          ports:
            - containerPort: 8080
          # 资源请求与限制:调度依据与上限保护
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 768Mi
          env:
            # 环境变量注入配置中心地址
            - name: SPRING_PROFILES_ACTIVE
              value: prod
            - name: CONFIG_SERVER_URL
              valueFrom:
                configMapKeyRef:
                  name: app-config
                  key: config-server-url
          # 就绪探针:/actuator/health 返回 200 才接收流量
          readinessProbe:
            httpGet:
              path: /actuator/health
              port: 8080
            initialDelaySeconds: 20
            periodSeconds: 10
          # 存活探针:健康检查持续失败则重启
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 15
          # 启动探针:JVM 冷启动慢,先放宽等待
          startupProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            failureThreshold: 30
            periodSeconds: 5

讲解:该清单是 Java 服务的标准模板。resources 的 limits 与 JVM 的 MaxRAMPercentage 必须联动(内存限制 768Mi 时 JVM 堆约 576Mi);三个探针分工明确,避免滚动更新期间流量中断与故障实例无法恢复。

4.3 Service 与 Ingress

apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  # 选择器与 Deployment 的 Pod 标签一致
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-service
spec:
  rules:
    - host: orders.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80

讲解:Service 提供集群内稳定的访问入口;Ingress 负责外部 HTTP 路由、TLS 终止与路径转发。targetPort 指向容器端口 8080,Service 端口 80 是集群内虚拟端口。

4.4 ConfigMap 与 Secret

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  config-server-url: http://config-server:8888
  log-level: INFO
---
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
stringData:
  db-password: change-me

讲解:配置与敏感信息从镜像剥离,镜像因此可在多环境复用。Secret 的 value 以 base64 存储(并非加密),生产环境应配合外部密钥管理(如 External Secrets Operator、Vault)。

4.5 HPA 自动扩缩容

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

讲解:HPA 按平均 CPU 利用率 60% 调整副本数。Java 应用扩缩容时注意:JVM 冷启动慢,建议配合 startupProbe 与就绪探针,避免扩容期间流量损失。

4.6 Spring Boot 优雅停机配置

# application.yml
server:
  shutdown: graceful

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

讲解:server.shutdown=graceful 让 Web 容器停止接收新请求并等待进行中请求完成;timeout-per-shutdown-phase 限制最长等待,避免无限挂起。该配置必须与 Pod 的 terminationGracePeriodSeconds(默认 30s)协调:停机等待应小于终止宽限期。

4.7 Java 代码中的优雅停机

// 应用关闭时执行清理:释放连接、保存状态
@Component
public class GracefulShutdownListener {

    // Spring Boot 关闭前回调
    @PreDestroy
    public void onShutdown() {
        System.out.println("收到停机信号,清理资源...");
        // 关闭数据库连接池、取消定时任务等
    }
}

讲解:@PreDestroy 回调在 Spring 容器关闭时执行。配合 SIGTERM,Pod 删除时应用先清理资源再退出,避免强制杀死导致的数据损坏。

5. 对比分析

5.1 部署形态对比

维度传统虚拟机部署Docker 容器Kubernetes
环境一致性依赖配置管理镜像级一致镜像 + 声明式清单
扩缩容手工/脚本单机编排自动调度与 HPA
故障恢复手工重启单机守护自愈与滚动更新
运维成本高中需要集群管理

5.2 镜像方案对比

方案体积启动速度适用
完整 JDK 镜像大(300MB+)一般开发调试
JRE + jlink 定制中(80-150MB)一般生产常规
Distroless小(50-100MB)一般安全敏感生产
GraalVM Native极小(30-60MB)极快(毫秒级)无反射/低内存场景

5.3 探针组合对比

只配 liveness 不配 readiness:滚动更新时新 Pod 未就绪就接收流量,导致 502;只配 readiness 不配 liveness:死锁应用永远不被重启。三探针齐备是生产基线。

6. 常见陷阱与最佳实践

陷阱一:JVM 堆按宿主机内存计算,容器内 OOM。最佳实践:-XX:MaxRAMPercentage=75 并保持 resources.limits.memory 与之一致。

陷阱二:镜像以 root 运行。最佳实践:创建专用用户,最小权限运行。

陷阱三:探针路径使用应用默认端点而非专门的健康端点。Spring Boot 应引入 actuator 并区分 liveness/readiness 端点。

陷阱四:terminationGracePeriodSeconds 小于 Spring 优雅停机时间,导致请求被强杀。最佳实践:停机宽限 > 请求最大处理时间。

陷阱五:把数据库密码写进镜像或 ConfigMap 明文。最佳实践:Secret + 外部密钥管理。

陷阱六:滚动更新时 maxUnavailable=0 且 maxSurge=0,更新被卡死。最佳实践:至少允许一个额外 Pod。

陷阱七:HPA 与 JVM 堆固定值冲突(堆按启动时容器限制计算,扩缩容后实例规格不变则无问题;但限制变化需重启)。理解 HPA 只调副本数,不调整单实例规格。

7. 工程实践

7.1 CI/CD 流水线

# .github/workflows/deploy.yml 片段
jobs:
  build:
    steps:
      - uses: actions/checkout@v4
      # 构建镜像并推送
      - run: docker build -t registry.example.com/order-service:${{ github.sha }} .
      - run: docker push registry.example.com/order-service:${{ github.sha }}
      # 更新清单中的镜像版本并部署
      - run: kubectl set image deployment/order-service order-service=registry.example.com/order-service:${{ github.sha }}

讲解:镜像 tag 使用 commit SHA 保证可追溯;kubectl set image 触发滚动更新。生产环境应增加镜像签名、漏洞扫描与金丝雀验证阶段。

7.2 可观测性接入

<!-- pom.xml:Micrometer 指标与 Prometheus 暴露 -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# Prometheus 抓取配置
annotations:
  prometheus.io/scrape: "true"
  prometheus.io/path: "/actuator/prometheus"
  prometheus.io/port: "8080"

讲解:Java 应用通过 Micrometer 输出 JVM 指标(堆内存、GC、线程),Prometheus 按注解抓取。配合 Grafana 仪表盘与告警规则,形成完整可观测性。

8. 案例研究:订单服务的云原生改造

需求:把单体订单服务改造为 Kubernetes 部署,要求:滚动更新零中断、自动扩缩容、配置外置、优雅停机。

改造步骤:

第一步,Spring Boot 引入 actuator 并配置健康端点与优雅停机;

第二步,编写多阶段 Dockerfile(Temurin 21 JRE,非 root);

第三步,编写 Deployment/Service/Ingress 清单,配置三探针与资源限制;

第四步,配置 ConfigMap/Secret,把配置与密码移出镜像;

第五步,创建 HPA(CPU 60%,2-10 副本);

第六步,接入 Prometheus 指标与告警;

第七步,CI 构建镜像、冒烟测试、滚动发布。

验证要点:滚动发布期间压测观察错误率(应为 0);模拟 Pod 崩溃观察自动重启;模拟流量高峰观察 HPA 扩容;删除 Pod 观察优雅停机日志与连接清理。

9. 知识要点总结与深入讲解

Java 上 Kubernetes 的三个关键数字:MaxRAMPercentage=75(堆占容器内存比例)、terminationGracePeriodSeconds > 优雅停机超时、readiness 先行、liveness 兜底。理解这三个数字,就掌握了 Java 容器化的主线。

镜像分层与多阶段构建解决体积与安全问题;ConfigMap/Secret 解决配置外置;探针解决流量与自愈;HPA 解决弹性。每一层都对应一个明确的运维问题。

概述

Kubernetes 是容器编排的事实标准,Java 应用的云原生部署需要关注资源限制、健康检查、优雅停机和自动伸缩等方面。本文介绍 Java 应用在 Kubernetes 上的部署最佳实践,包括 Deployment 配置、服务发现、配置管理和监控集成。

基础概念

Kubernetes 核心资源

资源说明
Deployment管理无状态应用,控制 Pod 副本数
Service为 Pod 提供稳定的访问入口
ConfigMap存储非敏感配置信息
Secret存储敏感信息(密码、密钥)
HPA水平 Pod 自动伸缩器
IngressHTTP 路由和 TLS 终止

Java 容器化关键点

  • JVM 需要正确识别容器的 CPU 和内存限制
  • 合理设置堆内存,避免 OOM Killed
  • 配置健康检查端点
  • 实现优雅停机,确保请求处理完成

快速上手

Deployment 配置

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: registry.example.com/myapp:1.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              memory: '512Mi'
              cpu: '500m'
            limits:
              memory: '1Gi'
              cpu: '1000m'
          env:
            - name: JAVA_OPTS
              value: '-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC'
            - name: SPRING_PROFILES_ACTIVE
              value: 'prod'

Service 与 Ingress

# Service:集群内部访问
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP

---
# Ingress:外部访问入口
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: 'true'
spec:
  tls:
    - hosts:
        - myapp.example.com
      secretName: myapp-tls
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myapp-service
                port:
                  number: 80

详细用法

健康检查

# 配置存活和就绪探针
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
        - name: myapp
          image: myapp:latest
          # 存活探针:检测应用是否卡死
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 30
            failureThreshold: 3
          # 就绪探针:检测应用是否就绪
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
// Spring Boot 健康检查端点
@RestController
@RequestMapping("/actuator/health")
public class HealthController {
    @GetMapping("/liveness")
    public Map<String, String> liveness() {
        return Map.of("status", "UP");
    }

    @GetMapping("/readiness")
    public Map<String, String> readiness() {
        // 检查数据库连接等依赖是否就绪
        boolean dbReady = dataSource.getConnection().isValid(1);
        return Map.of("status", dbReady ? "UP" : "DOWN");
    }
}

配置管理

# ConfigMap:存储配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  application.yml: |
    server:
      port: 8080
    spring:
      datasource:
        url: jdbc:postgresql://db-service:5432/mydb
    logging:
      level:
        com.example: DEBUG

---
# Secret:存储敏感信息
apiVersion: v1
kind: Secret
metadata:
  name: myapp-secret
type: Opaque
data:
  db-password: c2VjcmV0 # base64 编码
  api-key: YXBpLWtleQ==
# 在 Deployment 中引用配置
spec:
  containers:
    - name: myapp
      envFrom:
        - configMapRef:
            name: myapp-config
        - secretRef:
            name: myapp-secret
      volumeMounts:
        - name: config-volume
          mountPath: /config
  volumes:
    - name: config-volume
      configMap:
        name: myapp-config

优雅停机

# 配置优雅停机
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60 # 最多等待 60 秒
      containers:
        - name: myapp
          lifecycle:
            preStop:
              exec:
                command: ['sh', '-c', 'sleep 10'] # 等待 Service 摘除
# Spring Boot 优雅停机配置
# application.yml
server:
  shutdown: graceful # 启用优雅停机
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s # 最多等待 30 秒

常见场景

HPA 自动伸缩

# 基于 CPU 使用率自动伸缩
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300 # 缩容冷却期 5 分钟
    scaleUp:
      stabilizationWindowSeconds: 60

Spring Cloud Kubernetes 服务发现

# 使用 Kubernetes 原生服务发现替代 Eureka
# pom.xml 添加依赖
# spring-cloud-starter-kubernetes-client

# application.yml
spring:
  cloud:
    kubernetes:
      discovery:
        enabled: true
      config:
        enabled: true
        sources:
          - name: myapp-config

注意事项

  • JVM 内存设置必须小于容器内存限制,留出堆外内存空间
  • 使用 MaxRAMPercentage 代替硬编码 Xmx,适配不同规格的 Pod
  • 初始延迟(initialDelaySeconds)应大于应用启动时间
  • 优雅停机的超时时间应小于 terminationGracePeriodSeconds
  • ConfigMap 更新后需要重启 Pod 才能生效,或使用 Spring Cloud Kubernetes 动态刷新
  • 生产环境建议使用 PodDisruptionBudget 保证最小可用副本数

进阶用法

Init Container 初始化

# 使用 Init Container 等待依赖服务就绪
spec:
  initContainers:
    - name: wait-for-db
      image: busybox
      command: ['sh', '-c', 'until nc -z db-service 5432; do echo waiting for db; sleep 2; done']
    - name: wait-for-redis
      image: busybox
      command:
        ['sh', '-c', 'until nc -z redis-service 6379; do echo waiting for redis; sleep 2; done']
  containers:
    - name: myapp
      image: myapp:latest

PodPreset 与 PodTemplate

# 使用 PodDisruptionBudget 保证服务可用性
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: myapp-pdb
spec:
  minAvailable: 2 # 至少保持 2 个 Pod 可用
  selector:
    matchLabels:
      app: myapp

GraalVM Native Image 部署

# Native Image 镜像部署,资源需求更低
spec:
  containers:
    - name: myapp-native
      image: myapp-native:latest
      resources:
        requests:
          memory: '64Mi' # Native Image 内存需求极低
          cpu: '100m'
        limits:
          memory: '128Mi'
          cpu: '500m'
      # Native Image 启动极快,缩短初始延迟
      livenessProbe:
        httpGet:
          path: /actuator/health/liveness
          port: 8080
        initialDelaySeconds: 5