Skip to content

优雅关闭

概述

优雅关闭是现代 Web 应用程序中一个至关重要的特性,它确保应用程序在关闭时能够妥善处理正在进行的请求。在 Spring Boot 中,优雅关闭功能默认启用,支持所有四种嵌入式 Web 服务器(Jetty、Reactor Netty、Tomcat 和 Undertow),同时兼容响应式和 Servlet 架构的 Web 应用程序。

为什么需要优雅关闭?

在生产环境中,如果直接强制关闭应用程序,可能会导致以下问题:

  • 正在处理的请求被突然中断,造成数据不一致
  • 用户收到错误响应,影响用户体验
  • 数据库事务被意外中断,可能造成数据损坏
  • 资源(如文件句柄、网络连接)无法正确释放

优雅关闭的工作原理

优雅关闭作为应用程序上下文关闭过程的一部分执行,在停止 SmartLifecycle Bean 的最早阶段进行。这个停止过程使用一个超时时间,提供一个宽限期,在此期间:

  1. 现有请求:允许继续完成处理
  2. 新请求:不再接受新的请求
  3. 超时控制:提供合理的等待时间避免无限等待

配置超时时间

可以通过配置 spring.lifecycle.timeout-per-shutdown-phase 属性来设置超时时间:

properties
# 设置优雅关闭超时时间为 20 秒
spring.lifecycle.timeout-per-shutdown-phase=20s
yaml
spring:
  lifecycle:
    # 设置优雅关闭超时时间为 20 秒
    timeout-per-shutdown-phase: "20s"

IMPORTANT

在 IDE 中直接停止应用程序可能会立即关闭而不是优雅关闭,因为 IDE 可能不会发送正确的 SIGTERM 信号。请查看您的 IDE 文档获取更多详细信息。

业务场景示例

电商订单处理场景

假设我们有一个电商系统,正在处理用户的订单支付:

kotlin
@RestController
@RequestMapping("/api/orders")
class OrderController(
    private val orderService: OrderService,
    private val paymentService: PaymentService
) {

    @PostMapping("/{orderId}/pay")
    fun processPayment(@PathVariable orderId: Long, @RequestBody paymentRequest: PaymentRequest): ResponseEntity<PaymentResponse> {
        return try {
            // 这个过程可能需要 5-10 秒时间
            val order = orderService.findById(orderId) // 查询订单 - 1秒
            val paymentResult = paymentService.processPayment(paymentRequest) // 调用第三方支付 - 3-8秒
            orderService.updateOrderStatus(orderId, OrderStatus.PAID) // 更新订单状态 - 1秒

            ResponseEntity.ok(PaymentResponse(paymentResult.transactionId, "支付成功"))
        } catch (ex: Exception) {
            ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(PaymentResponse(null, "支付失败: ${ex.message}"))
        }
    }
}
kotlin
@Service
@Transactional
class PaymentService {

    fun processPayment(paymentRequest: PaymentRequest): PaymentResult {
        // 模拟调用第三方支付接口,可能需要较长时间
        return try {
            // 1. 验证支付信息
            validatePaymentInfo(paymentRequest)

            // 2. 调用第三方支付网关(可能需要 3-8 秒)
            val thirdPartyResponse = callThirdPartyPaymentGateway(paymentRequest)

            // 3. 保存支付记录
            savePaymentRecord(paymentRequest, thirdPartyResponse)

            PaymentResult(thirdPartyResponse.transactionId, PaymentStatus.SUCCESS)
        } catch (ex: PaymentException) {
            PaymentResult(null, PaymentStatus.FAILED)
        }
    }

    private fun callThirdPartyPaymentGateway(request: PaymentRequest): ThirdPartyPaymentResponse {
        // 模拟网络延迟和第三方处理时间
        Thread.sleep(5000) // 实际场景中这是网络请求
        return ThirdPartyPaymentResponse("TXN_${System.currentTimeMillis()}")
    }
}

在这种场景下,如果应用程序需要关闭:

  1. 没有优雅关闭:正在处理的支付请求被强制中断,用户支付状态不明确
  2. 有优雅关闭:系统等待支付处理完成后再关闭,确保数据一致性

在宽限期内拒绝新请求的处理方式

不同的 Web 服务器在宽限期内拒绝新请求的具体方式有所不同:

Web 服务器处理方式
Jetty在网络层停止接受新请求
Reactor Netty在网络层停止接受新请求
Tomcat在网络层停止接受新请求
Undertow接受新连接但立即返回 503 (Service Unavailable) 响应

TIP

要了解您的 Web 服务器使用的具体方法,请查看相应的 shutDownGracefully API 文档:

  • TomcatWebServer.shutDownGracefully(GracefulShutdownCallback)
  • NettyWebServer.shutDownGracefully(GracefulShutdownCallback)
  • JettyWebServer.shutDownGracefully(GracefulShutdownCallback)
  • UndertowWebServer.shutDownGracefully(GracefulShutdownCallback)

监控优雅关闭过程

可以通过实现自定义的监听器来监控优雅关闭过程:

kotlin
@Component
class GracefulShutdownListener : ApplicationListener<ContextClosedEvent> {

    private val logger = LoggerFactory.getLogger(GracefulShutdownListener::class.java)

    override fun onApplicationEvent(event: ContextClosedEvent) {
        logger.info("应用程序开始优雅关闭...")

        // 可以在这里添加额外的清理逻辑
        performCustomCleanup()
    }

    private fun performCustomCleanup() {
        // 清理缓存
        logger.info("清理应用程序缓存...")

        // 关闭自定义线程池
        logger.info("关闭自定义线程池...")

        // 发送关闭通知
        logger.info("发送应用关闭通知...")
    }
}

自定义优雅关闭处理器

对于需要特定关闭逻辑的组件,可以实现 SmartLifecycle 接口:

kotlin
@Component
class CustomGracefulShutdownHandler : SmartLifecycle {

    private val logger = LoggerFactory.getLogger(CustomGracefulShutdownHandler::class.java)
    private var running = false

    override fun start() {
        logger.info("启动自定义优雅关闭处理器")
        running = true
    }

    override fun stop() {
        logger.info("开始执行自定义关闭逻辑...")

        // 执行业务相关的清理工作
        cleanupBusinessResources()

        running = false
        logger.info("自定义关闭逻辑执行完成")
    }

    override fun isRunning(): Boolean = running

    override fun getPhase(): Int {
        // 返回较小的值以确保在其他组件之前关闭
        return SmartLifecycle.DEFAULT_PHASE - 1000
    }

    private fun cleanupBusinessResources() {
        try {
            // 示例:关闭消息队列连接
            logger.info("关闭消息队列连接...")
            Thread.sleep(2000) // 模拟清理时间

            // 示例:保存临时数据
            logger.info("保存临时数据...")
            Thread.sleep(1000)

            logger.info("业务资源清理完成")
        } catch (ex: Exception) {
            logger.error("业务资源清理失败", ex)
        }
    }
}

禁用优雅关闭

在某些场景下(如开发环境或测试环境),您可能希望禁用优雅关闭以加快应用程序关闭速度:

properties
# 禁用优雅关闭,立即关闭应用程序
server.shutdown=immediate
yaml
server:
  shutdown: "immediate"

WARNING

在生产环境中不建议禁用优雅关闭,因为这可能导致数据不一致和用户体验问题。

最佳实践

1. 合理设置超时时间

kotlin
// 根据业务需求设置合适的超时时间
// 对于简单的 CRUD 操作:5-10 秒
// 对于复杂的业务处理:20-30 秒
// 对于批处理操作:60-120 秒

2. 实现健康检查端点

kotlin
@RestController
class HealthController {

    @Autowired
    private lateinit var applicationContext: ConfigurableApplicationContext

    @GetMapping("/health/shutdown")
    fun getShutdownStatus(): ResponseEntity<Map<String, Any>> {
        val status = mapOf(
            "shutdownInitiated" to !applicationContext.isActive,
            "timestamp" to Instant.now().toString()
        )
        return ResponseEntity.ok(status)
    }
}

3. 配置负载均衡器

NOTE

优雅关闭是构建可靠 Web 应用程序的重要特性。通过合理配置和监控,可以确保应用程序在维护或升级时不会影响用户体验和数据完整性。