Ohhnews

分类导航

$ cd ..
DZone Java原文

Java应用在Kubernetes上的零停机部署

#零停机部署#kubernetes#java#部署策略#ci/cd

本文旨在提供一份全面的指南,帮助 Java 应用在 Kubernetes 上实现零停机部署。我们将涵盖部署策略、Kubernetes 原语、Java 相关注意事项、会话状态处理、数据库迁移、流量切换技术、CI/CD 管道、GitHub Actions、Jenkins 自动回滚、可观测性(Prometheus、Grafana、Jaeger)、Helm/ArgoCD 示例、测试策略(金丝雀分析、混沌测试、冒烟测试)以及故障排查。

部署策略

Kubernetes 提供了几种无停机部署新版本的策略:

滚动更新(Rolling Update)

逐步用新 Pod 替换旧 Pod,保持可用性。Kubernetes Deployment 对象默认使用滚动更新。你可以通过 maxUnavailablemaxSurge 参数来控制滚动过程。

蓝绿部署(Blue-Green Deployment)

运行两个独立的环境:蓝色(当前版本)和绿色(新版本)。每次只有一个环境服务于实时流量。待绿色版本验证通过后,将 Service 或 Ingress 切换指向绿色,然后缩容蓝色。这种模式允许通过将流量切回蓝色实现即时回滚。Argo Rollouts 定义了蓝绿策略,包含 active 和 preview 两个 Service,流量仅流向已晋升(promotion)的版本。

金丝雀部署(Canary Deployment)

逐步将小部分流量切换到新版本。先启动少量 v2 Pod,监控无误后逐步增加流量。可以使用 Istio 或 Argo Rollouts 等工具控制流量权重。例如,通过运行 9 个 v1 Pod 和 1 个 v2 Pod(10% 流量)将 10% 的流量导向 v2。Argo 定义了金丝雀 rollout,包含 setWeight 步骤和用于分析的暂停点。

影子部署/流量镜像(Shadow/Mirroring)

新版本接收实时请求的副本,用于在真实负载下测试,但其响应不会返回给用户。这种方法风险低,但由于用户看不到新行为,对回滚决策帮助不大。

零停机所需的 Kubernetes 原语

Deployment

Deployment 原生支持滚动更新。默认情况下,它会创建一个新的 ReplicaSet 并逐步扩容,同时按 maxUnavailable/maxSurge 控制缩容旧 ReplicaSet,确保始终有 Pod 在提供服务。若使用蓝绿部署,则需要部署两个独立的 Deployment(例如 app-blueapp-green)并切换 Service。

Service 和 Ingress

Service 作为 Pod 前端。对于蓝绿部署,可以将单个 Service 指向蓝色或绿色 Pod。Ingress 也可以在后端服务之间切换。例如,通过调整标签选择器将流量从蓝色版本 Pod 重定向到绿色版本 Pod。

PodDisruptionBudget (PDB)

保证在自愿中断期间(如滚动更新)有最少数量的 Pod 保持运行。例如,设置 minAvailable: 1 确保滚动更新期间至少有一个 Pod 存活,避免完全停机。

水平 Pod 自动缩放器(HPA)

根据 CPU/内存或自定义指标缩放 Pod,自动调整工作负载以满足需求。可以将 HPA 附加到 Deployment,这样在发布过程中如果流量激增,会自动创建新 Pod 来承载负载。示例:

$ config
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: 50

存活探针(Liveness Probe)与就绪探针(Readiness Probe)

这是实现零停机的关键。存活探针检查应用是否存活,失败时 K8s 会重启 Pod。就绪探针告知应用是否已准备好接收流量。在启动或关闭过程中,就绪探针应失败,使 Pod 从 Service 负载均衡器中移除。Spring Boot Actuator 提供 /actuator/health 端点用于此目的。在 K8s YAML 中:

$ config
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 15
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

Spring Boot 默认暴露 health/livenesshealth/readiness 分组。Quarkus 和 Micronaut 也提供类似的健康端点。

Spring Boot 支持优雅关闭:设置 server.shutdown=graceful 并调整 spring.lifecycle.timeout-per-shutdown-phase。这会使嵌入式服务器(Tomcat/Jetty/Undertow)停止接收新流量,并在超时时间内等待正在处理的请求完成。

$ java
@Component
public class ShutdownListener implements SmartLifecycle {
  private boolean running = true;
  @Override
  public void stop() {
    running = false;
  }
  @Override
  public boolean isRunning() {
    return running;
  }
}

Quarkus 提供优雅关闭配置。通过设置 quarkus.shutdown.timeout=10s,Quarkus 会等待最多 10 秒让当前请求完成后再退出。可以使用 @Shutdown 注解在 Bean 方法上执行清理代码。

Micronaut 通过 @EventListener 监听 ShutdownEvent

$ java
@Singleton
public class ShutdownBean {
  @EventListener
  void onShutdown(ShutdownEvent event) {
  }
}

Kubernetes Hooks(PreStop)

可以在 Deployment 的 spec 中使用 preStop hook,在发送 SIGTERM 之前执行脚本:

$ config
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh","-c","sleep 5"]
terminationGracePeriodSeconds: 30

默认的终止宽限期(30 秒)应根据应用完成时间进行调整。K8s 文档描述了容器进入 Terminating、执行 preStop、发送 SIGTERM、等待 terminationGracePeriodSeconds、然后发送 SIGKILL 的序列。

JVM 调优

  • 设置 -XX:+ExitOnOutOfMemoryError 避免 JVM 挂起。
  • 调整线程池使其快速排空。
  • 监控 GC 停顿时间,考虑使用低延迟 GC 以最小化关闭前的停顿。

会话与状态处理

在 Pod 切换时保持零停机:

  • 无状态服务:最佳实践是保持服务无状态。将会话状态或用户数据存储在外部存储中,如 Redis 或数据库。这样任何 Pod 都能处理任何请求,替换 Pod 不会丢失用户会话。
  • 粘性会话:如果应用使用内存会话,可以强制使用粘性会话。
  • Service 亲和性:在 Service 上设置 sessionAffinity: ClientIP,Kubernetes 会将来自同一客户端 IP 的请求路由到同一个 Pod。
  • Ingress 亲和性:使用 Ingress 注解将用户请求绑定到同一个 Pod。但粘性会话存在风险,不适合自动缩放。
  • StatefulSet:对于真正的有状态工作负载,使用 StatefulSet 并赋予稳定标识。StatefulSet 将 Pod 与 PersistentVolume 配对,但本身并不保证零停机。

GitHub Actions CI/CD 管道(零停机)

$ config
name: Deploy
on:
  push:
    branches: [ main ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-java@v3
      with: { java-version: '17' }
    - name: Build
      run: mvn clean package -DskipTests
    - name: Docker Build & Push
      run: |
        docker build -t ghcr.io/myorg/myapp:${{ github.sha }}
        echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
        docker push ghcr.io/myorg/myapp:${{ github.sha }}
    - name: Set image tag
      run: echo "::set-output name=image::ghcr.io/myorg/myapp:${{ github.sha }}"
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
      with: { path: manifests }
    - name: Update K8s deployment
      uses: azure/setup-kubectl@v3
    - name: Deploy to Kubernetes
      run: |
        kubectl set image deployment/myapp-deployment myapp=ghcr.io/myorg/myapp:${{ needs.build.outputs.image }}
        kubectl rollout status deployment myapp-deployment

此工作流构建镜像、推送并更新 Deployment。rollout status 命令会等待所有新 Pod 就绪。如果健康检查失败,该命令将中止,而不会产生停机。

结论

在 Kubernetes 上实现零停机部署需要将细致的架构设计与自动化相结合,包括使用滚动更新、渐进式策略、确保 Java 应用的优雅关闭和健康检查、状态外化、数据库变更管理,以及通过 CI/CD 管道进行编排。Kubernetes 原语如 Deployment、Service、探针和 HPA,以及 Istio 或 Argo Rollouts 等工具,构成了实现的基础。

(DZone 贡献者表达的观点均为其个人观点。)