把 Spring 应用交付到现代生产环境:Java 21、容器探针与可观测性实战

2026-09-21 15 预计阅读时间: 1 分钟
来源: spring.io AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

现代 Spring 应用面对的挑战,已经不只是“能否启动”。团队还要回答一组更难的问题:构建结果能否复现,实例能否安全退出,平台如何判断服务是否就绪,运行状态是否可观测,以及新 Java 能力是否真的改善了工作负载。

由于来源摘要没有列出具体版本和发布特性,本文不把标题解读成某个版本的完整发布说明,而是给出一条可以直接实践的现代 Spring 交付基线。

发布物不应依赖服务器现场配置

传统部署经常把 JAR、JDK、启动脚本和环境配置分别复制到服务器。这样做短期直接,长期却容易出现环境漂移:测试环境使用 Java 21,某台生产机器可能仍在运行旧 JDK;不同节点上的 JVM 参数也可能不一致。

更稳妥的发布单元应当具备三个特征:

  • 不可变:同一个镜像从测试晋级到生产,而不是在生产环境重新构建。
  • 可追踪:镜像标签最好关联 Git 提交或发布版本。
  • 配置外置:数据库地址、凭据和环境开关通过环境变量或 Secret 注入,不写进镜像。

Spring Boot 的构建插件可以利用 Cloud Native Buildpacks 生成 OCI 镜像,团队不一定需要自己维护 Dockerfile。下面可以创建一个最小的 Java 21 项目。运行前需要安装 Java 21、curltar

mkdir modern-spring && cd modern-spring

curl -fsS -G https://start.spring.io/starter.tgz \
  --data-urlencode type=maven-project \
  --data-urlencode language=java \
  --data-urlencode groupId=com.example \
  --data-urlencode artifactId=modern-spring \
  --data-urlencode name=modern-spring \
  --data-urlencode packageName=com.example.modernspring \
  --data-urlencode javaVersion=21 \
  --data-urlencode dependencies=web,actuator \
  -o app.tgz

tar -xzf app.tgz
rm app.tgz

加入一个可以观察处理线程的接口:

cat > src/main/java/com/example/modernspring/WorkController.java <<'EOF'
package com.example.modernspring;

import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class WorkController {

    @GetMapping("/api/work")
    Map<String, Object> work() throws InterruptedException {
        Thread.sleep(100);
        return Map.of(
            "status", "done",
            "thread", Thread.currentThread().toString()
        );
    }
}
EOF

这里的 Thread.sleep 只是模拟阻塞式 I/O,不能当作生产业务代码。它让我们能够直观看到请求由什么线程处理。

把健康检查与优雅停机纳入应用设计

容器平台不应该通过“进程还在不在”判断应用是否可以接收流量。一个 Spring 进程可能已经启动,但连接池、缓存预热或外部依赖仍未准备好;它也可能仍然存活,却已无法正常服务。

可以加入以下配置:

cat > src/main/resources/application.yaml <<'EOF'
server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 20s
  threads:
    virtual:
      enabled: true

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      probes:
        enabled: true
      show-details: never
EOF

启动并验证:

./mvnw spring-boot:run

另开终端执行:

curl -s http://localhost:8080/api/work
curl -s http://localhost:8080/actuator/health
curl -s http://localhost:8080/actuator/health/liveness
curl -s http://localhost:8080/actuator/health/readiness

这里启用了虚拟线程,适合用于评估包含大量阻塞等待的请求模型,但它不是一个无条件的性能开关。数据库连接池容量、下游限流、synchronized 临界区以及固定大小的第三方线程池,仍可能成为瓶颈。上线前应使用真实流量模型做压测,并比较吞吐量、尾延迟、内存和下游压力。

优雅停机则解决滚动发布中的请求中断问题:平台发出终止信号后,应用停止接收新请求,并在限定时间内处理已有请求。平台的终止宽限期必须大于应用的停机等待时间,否则容器仍可能被强制杀死。

从本地 JAR 到容器和 Kubernetes

先执行测试并构建镜像。build-image 需要本机存在可用的 Docker 守护进程:

./mvnw clean verify
./mvnw spring-boot:build-image \
  -Dspring-boot.build-image.imageName=modern-spring:local

docker run --rm -p 8080:8080 modern-spring:local

如果部署到 Kubernetes,可以这样配置探针。请先把 registry.example.com/team/modern-spring:1.0.0 替换为实际已经推送的镜像:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: modern-spring
spec:
  replicas: 2
  selector:
    matchLabels:
      app: modern-spring
  template:
    metadata:
      labels:
        app: modern-spring
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: app
          image: registry.example.com/team/modern-spring:1.0.0
          ports:
            - name: http
              containerPort: 8080
          startupProbe:
            httpGet:
              path: /actuator/health/liveness
              port: http
            periodSeconds: 5
            failureThreshold: 30
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: http
            periodSeconds: 5
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: http
            periodSeconds: 10
            failureThreshold: 3
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
  name: modern-spring
spec:
  selector:
    app: modern-spring
  ports:
    - port: 80
      targetPort: http

三类探针承担不同职责:

  • startupProbe 给启动较慢的应用留出时间,避免初始化期间被存活探针反复重启。
  • readinessProbe 决定实例是否进入 Service 流量池。
  • livenessProbe 只应判断进程是否需要重启,不宜强依赖临时不可用的外部系统,否则数据库短暂抖动可能引发整个应用集群重启。

可观测性不是简单暴露所有 Actuator 端点

Actuator 提供了健康状态和指标基础,但生产环境不能直接暴露全部管理端点。上面的配置只开放 healthinfometrics,并隐藏健康检查细节,避免泄露组件名称、内部地址或异常信息。

真实系统还可以这样实践:

  • 使用 Micrometer 将指标交给 Prometheus 或其他监控后端。
  • 在入口生成或传递关联 ID,让日志、指标和链路能够对应同一次请求。
  • 分别记录请求速率、错误率和延迟分位数,不只观察平均响应时间。
  • 为发布版本添加标签,便于判断错误峰值是否与某次部署相关。
  • 将管理端口放在内部网络,并配合认证和网络策略,而不是依赖“路径不公开”。

需要注意,指标标签不能无限增长。把用户 ID、订单号或完整 URL 放进标签,会制造高基数数据并迅速推高监控成本。

采用这条基线时的检查表

面向现代环境发布 Spring,不等于一次性打开所有新功能。更可靠的方式是逐步建立可验证的工程约束:

  • [ ] CI 使用固定的 Java 版本,并执行单元测试与集成测试。
  • [ ] 镜像只构建一次,使用不可变版本或摘要晋级到不同环境。
  • [ ] 凭据通过 Secret 注入,不进入 Git、JAR 或镜像层。
  • [ ] 区分启动、就绪和存活状态,避免一个 /health 承担全部语义。
  • [ ] 应用优雅停机时间小于平台终止宽限期。
  • [ ] Actuator 端点遵循最小暴露原则。
  • [ ] 虚拟线程、原生镜像等能力先经过基准测试,再按工作负载采用。
  • [ ] 每次发布都能通过版本、提交号和镜像摘要追踪。

现代化的核心并不是追逐某一个特性,而是让构建、运行、观测和回滚形成闭环。Spring 提供了不少可用的基础设施,但真正决定发布质量的,仍是团队是否把这些能力变成自动化、可测试且可重复的交付流程。


相关推荐