什么是优雅停机?在 Spring Boot 中如何平稳关闭服务

1,775 阅读13分钟

优雅停机(Graceful Shutdown)在现代微服务架构中是非常重要的,它帮助我们确保在应用程序停止时,不会中断正在进行的请求或导致数据丢失。让我们以通俗易懂的方式来讲解这个概念以及如何在 Spring Boot 中实现它。

优雅停机在微服务架构中的角色

想象一下,你在一家餐厅用餐,突然服务员走过来说:“很抱歉,我得下班了,请您尽快结账离开。”你可能刚刚点了主菜,或许正在等着甜点,结果这个突如其来的通知让你感到非常不满。你的就餐体验被打断了,甚至你的账单也可能出现混乱。与此同时,餐厅的运营也可能因此变得不顺畅,服务员离开后,未完成的订单和客户的需求都可能无法及时得到处理,产生了不必要的麻烦。

在微服务架构中,这种情况的对应场景是应用程序的突发关闭。假设一个用户正在发起一个请求,这个请求正在进行中时,某个微服务却突然被强制关闭,或者在维护过程中突然停止。这就会导致用户的操作被中断,未处理的请求可能丢失,甚至产生数据不一致或错误的状态。

这种场景不仅会让用户体验变差,还可能给后端系统带来极大的问题。例如,用户信息可能未能成功保存到数据库中,或者订单状态没有被正确更新,最终造成数据的“脏”或不一致。为了避免这种问题,优雅停机机制在现代微服务系统中变得至关重要。它确保在服务关闭时,所有进行中的请求能够得到适当的处理,相关资源得以顺利释放,避免因应用程序的突然关闭而导致的问题。

1、什么是优雅停机

简单来说,优雅停机就是指当我们需要关闭一个服务时,服务能够有序地完成当前的工作并停止。 具体来说,优雅停机包括以下几个步骤:

  • 停止接收新请求:当系统开始关闭时,需要通知负载均衡器或网关,告知它不要再将新的请求发送到即将下线的实例。
  • 处理正在进行的请求:对于已经到达并正在处理的请求,系统要给它们完成的机会,不会突然中断。
  • 释放资源:像数据库连接池、线程池等资源需要被安全释放,避免资源泄露。
  • 持久化临时数据:如果有必要,系统会保存当前的状态到数据库或文件,以便下次启动时可以恢复。

2、为什么需要优雅停机

  • 部署新版本时平稳过渡:当我们需要更新应用时,优雅停机可以让旧版本服务平稳关闭,避免突然的停机对用户造成影响。
  • 避免资源泄露:不管是内存、数据库连接,还是线程池资源,都需要在关闭时释放,否则就可能导致内存泄漏等问题。
  • 确保数据一致性:如果有正在处理的事务,优雅停机可以让这些事务有机会完成,避免数据丢失或者不一致。

3、优雅停机的实际应用场景

  • 服务更新: 在系统版本升级时,通过优雅停机完成请求处理和资源释放,避免对用户造成干扰。

  • 流量调控: 在高并发场景下,如果需要暂时下线部分服务节点,优雅停机可以帮助实现“无感”迁移。

  • 订单处理: 如出租车平台,在订单完成后再下线服务,避免出现“中途被抛弃”的情况。

4、优雅停机可能失效的情况

  1. 强制关闭:使用 kill -9 强制终止进程将导致优雅停机机制无法触发。
  2. 资源耗尽:系统资源不足可能导致清理操作无法完成。
  3. 未配置超时:如果未配置超时时间,处理长时间任务可能导致停机时间过长。

5、如何在 Spring Boot 中实现优雅停机

Spring Boot 优雅停机的基础实现

  • Spring Boot 2.3 开始,优雅停机的支持更加简单和强大。通过设置 server.shutdown 配置,可以决定应用停机时的行为。

立即停机模式

  • 在立即停机模式下,应用会立刻中断所有请求和任务。
server:
  shutdown: immediate

虽然简单高效,但这种方式通常只适用于测试或无状态服务。

优雅停机模式

  • 在优雅停机模式下,Spring Boot 会等待当前的处理任务完成,再进行停机操作。
server:
  shutdown: graceful

注意: 该模式下的默认等待时间为 30 秒,可通过 spring.lifecycle.timeout-per-shutdown-phase 进行配置。


添加 spring-boot-starter-actuator 依赖

首先,需要确保你的项目中包含了 spring-boot-starter-actuator 依赖,这是启用 Spring Boot 内置监控和管理端点的工具包。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

启用 shutdown 端点

默认情况下,Spring Bootshutdown 端点是禁用的。我们需要在 application.propertiesapplication.yml 中显式启用它。

对于 application.properties

management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=shutdown

或者,如果你使用 application.yml

management:
  endpoint:
    shutdown:
      enabled: true
  endpoints:
    web:
      exposure:
        include: "shutdown"

触发优雅停机

配置好后,你可以通过发送 HTTP 请求来触发优雅停机。例如,使用 curl 命令:

curl -X POST http://localhost:8080/actuator/shutdown

当你调用这个端点时,Spring Boot 应用会停止接收新的请求,继续处理已经收到的请求,直到所有请求处理完毕后,应用才会退出。

6、通过 ApplicationListener 接口实现清理逻辑

为了在应用关闭时执行特定的清理操作(例如关闭数据库连接、释放资源等),你可以实现 ApplicationListener<ContextClosedEvent> 接口。

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;

@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {
    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        // 执行必要的清理操作
        System.out.println("Starting graceful shutdown...");
        try {
            Thread.sleep(5000); // 模拟清理任务
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Graceful shutdown completed.");
    }
}

在这个例子中,我们模拟了一个清理操作的过程(通过 Thread.sleep() 来等待)。实际上,你可以替换这部分逻辑,比如关闭数据库连接、释放文件句柄等。

7、使用 JVM 的钩子函数

Java 提供了 Runtime.addShutdownHook() 方法,可以注册一个线程,在 JVM 终止时执行清理任务。这个方法适用于一些更底层的清理操作,尤其是在某些情况下,ApplicationListener 可能没有机会被触发时。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MainApp {

    public static void main(String[] args) {
        // 注册关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("JVM is shutting down, executing cleanup...");
            try {
                Thread.sleep(5000); // 模拟清理任务
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Cleanup completed.");
        }));

        SpringApplication.run(MainApp.class, args);
    }
}

需要注意的是,JVM 关闭钩子并不总是能执行,尤其是在遇到强制停止(如 kill -9)时。因此,它应该作为一种补充机制,而不是唯一的保证。

8、触发优雅停机的方式

除了通过 /actuator/shutdown 端点来触发优雅停机外,还有一些常见的方法:

  • SIGTERM 信号:通过发送 SIGTERM 信号(例如使用 kill 命令)可以触发 JVM 的正常退出流程。

    kill <pid>
    

    这里的 <pid> 是应用的进程 ID。

    kill -9 pid可以模拟了一次系统宕机,系统断电等极端情况,而kill -15 pid则是等待应用关闭,执行阻塞操作,有时候也会出现无法关闭应用的情况

    #查看jvm进程pid
    jps
    #列出所有信号名称
    kill -l
    
    # Windows下信号常量值
    # 简称  全称    数值 
    # INT   SIGINT     2       Ctrl+C中断
    # ILL   SIGILL     4       非法指令
    # FPE   SIGFPE     8       floating point exception(浮点异常)
    # SEGV  SIGSEGV    11      segment violation(段错误)
    # TERM  SIGTERM    5       Software termination signal from kill(Kill发出的软件终止)
    # BREAK SIGBREAK   21      Ctrl-Break sequence(Ctrl+Break中断)
    # ABRT  SIGABRT    22      abnormal termination triggered by abort call(Abort)
    
    #linux信号常量值
    # 简称  全称  数值  
    # HUP   SIGHUP      1    终端断线  
    # INT   SIGINT      2    中断(同 Ctrl + C)        
    # QUIT  SIGQUIT     3    退出(同 Ctrl + \)         
    # KILL  SIGKILL     9    强制终止         
    # TERM  SIGTERM     15    终止         
    # CONT  SIGCONT     18    继续(与STOP相反, fg/bg命令)         
    # STOP  SIGSTOP     19    暂停(同 Ctrl + Z)        
    #....
    
    #可以理解为操作系统从内核级别强行杀死某个进程
    kill -9 pid 
    #理解为发送一个通知,等待应用主动关闭
    kill -15 pid
    #也支持信号常量值全称或简写(就是去掉SIG后)
    kill -l KILL
    
  • JVM 工具:Java 提供了一些工具(如 jcmdjconsole)可以用来控制 JVM 的生命周期。例如,使用 jcmd 来发送退出命令:

    jcmd <pid> VM.exit
    
  • 容器平台:如果你的应用运行在 KubernetesDocker 等容器平台上,平台通常会在删除容器时发送 SIGTERM 信号,并等待一段时间让应用完成工作。


其他方法

在 Spring Boot 中实现优雅停机(Graceful Shutdown)除了通过 spring-boot-actuatorApplicationListener<ContextClosedEvent>JVM 钩子等方式外,实际上还有一些其他方法可以帮助我们实现优雅停机。以下是几种不同的实现方式,并配合实际应用场景和代码示例。

1. 使用 @PreDestroy 注解

@PreDestroy 注解是 Java EE 中的一种注解,用来在 Bean 销毁之前执行清理任务。Spring 也支持这个注解,当 Spring 容器关闭时,所有带有 @PreDestroy 注解的方法都会被调用。它通常用于执行资源的释放操作,如关闭数据库连接池、清理缓存等。

代码示例:
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class GracefulShutdownService {

    @PreDestroy
    public void onShutdown() {
        System.out.println("Performing cleanup before shutdown...");
        // 在此处执行资源清理,如关闭数据库连接、释放线程池等
    }
}
适用场景:
  • 适用于需要在应用关闭时清理资源的场景,尤其是在资源管理方面(例如关闭连接池、清理缓存等)。
  • Web 应用中,通常用来释放与外部系统(如数据库、消息队列等)的连接。

2. 使用 DisposableBean 接口

Spring 提供了 DisposableBean 接口,用于定义 Bean 在销毁时需要执行的清理操作。它的 destroy() 方法会在 Spring 容器关闭时被自动调用。

代码示例:

启用 Shutdown Hook

Spring Boot 默认会通过 JVM 的 Shutdown Hook 触发优雅停机。确保以下配置启用:

spring:
  main:
    register-shutdown-hook: true
自定义资源释放逻辑

如果需要在停机时执行特定的清理操作,比如关闭数据库连接或停止线程池,可以通过添加 Shutdown Hook 或实现 DisposableBean 接口。

import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;

@Component
public class GracefulShutdownService implements DisposableBean {

    @Override
    public void destroy() throws Exception {
        System.out.println("Graceful shutdown - Cleaning up resources...");
        // 执行清理操作
    }
}

或者直接通过 JVM 钩子实现:

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    System.out.println("执行自定义的资源清理逻辑");
}));
超时机制

避免因某些请求耗时过长导致系统停机过程被阻塞,可以通过以下配置设置超时时间:

spring:
  lifecycle:
    timeout-per-shutdown-phase: 20s # 默认30秒

适用场景:

  • @PreDestroy 类似,但 DisposableBean 提供了一种更明确的方式来处理 Spring 容器中的 Bean 销毁。
  • 适用于需要进行自定义清理操作(如关闭连接池、停止后台线程等)的场景。

3. 配合自定义线程池实现优雅停机

如果你的应用使用了自定义线程池,想确保所有线程在应用关闭时能够有序停止,可以通过设置线程池的 shutdownshutdownNow 方法来实现。

Spring Boot 提供了多种方式来创建和配置线程池,如 TaskExecutor@Async 等。如果应用在停止时需要等待线程池中的任务完成,可以通过以下方式进行配置。

代码示例:

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {

    private final ThreadPoolTaskExecutor taskExecutor;

    public GracefulShutdownListener(ThreadPoolTaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        System.out.println("Shutting down thread pool...");
        
        // 给当前线程池中的任务一些时间来完成
        taskExecutor.shutdown();
        
        try {
            // 等待任务完成
            if (!taskExecutor.getThreadPoolExecutor().awaitTermination(60, TimeUnit.SECONDS)) {
                System.out.println("Timeout reached, forcing shutdown...");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

适用场景:

  • 适用于你的应用需要执行异步任务时,并希望确保这些任务能够有序地完成,防止强制中断。
  • 适用于后台线程池或异步任务系统,确保应用关闭时,任务能够平稳终止。

4. 在 Kubernetes 中实现优雅停机

如果你将 Spring Boot 应用部署在容器编排平台(如 Kubernetes)上,Kubernetes 会自动帮助你实现优雅停机。Kubernetes 发送 SIGTERM 信号,并等待容器停止一定时间(通常是 30 秒),在这段时间内,应用应当完成正在进行的请求并清理资源。

你可以通过设置 terminationGracePeriodSeconds 来配置应用在收到 SIGTERM 信号后的最大优雅停机时间。

配置示例(Kubernetes):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  template:
    spec:
      containers:
        - name: my-app
          image: my-app-image
          ports:
            - containerPort: 8080
      terminationGracePeriodSeconds: 60  # 等待 60 秒

适用场景:

  • 当应用部署在 KubernetesDocker Swarm 等容器平台时,这种方式能够自动触发优雅停机,配合应用的优雅停机策略(如通过 Actuator 触发 shutdown)。
  • 适用于容器化部署和云原生应用,能够与平台的生命周期管理机制配合。

5. 使用自定义 shutdown 信号处理器

如果你不想完全依赖 Spring 提供的机制,可以实现一个自定义的信号处理器来捕获和响应关闭信号。通过这种方式,你可以更精细地控制停机流程。

代码示例:

import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class ShutdownSignalHandler {

    public ShutdownSignalHandler() throws IOException {
        // 注册 SIGTERM 信号处理器
        Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
    }

    private void shutdown() {
        System.out.println("Received shutdown signal. Performing graceful shutdown...");
        // 执行清理操作,如关闭连接池、停止后台服务等
    }
}

适用场景:

  • 如果你需要更灵活的优雅停机控制,可以使用自定义的信号处理器。
  • 适用于需要处理各种不同信号(如 SIGTERMSIGINT 等)的复杂场景,特别是对于非 Spring 的基础设施组件。

9、小结

优雅停机是 Spring Boot 应用的重要特性,它帮助我们确保在关闭应用时能够平稳地释放资源,处理完正在进行的请求,从而提高系统的稳定性和可靠性。通过 Spring Boot Actuator、ApplicationListener 接口和 JVM 钩子函数等多种方式,我们可以确保应用程序能够安全、顺利地关闭,而不会影响用户体验或导致数据丢失。

除了 Spring Boot Actuator 和 ApplicationListener 外,还有多种方式可以实现优雅停机。每种方式有不同的适用场景:

  1. @PreDestroyDisposableBean:适用于简单的资源释放和清理操作。
  2. 自定义线程池清理:适用于需要确保线程池任务完成的场景。
  3. 容器平台的优雅停机:适用于容器化应用,Kubernetes 会自动管理服务的优雅停机。
  4. 自定义信号处理:适用于需要更灵活、底层控制的停机过程。

根据应用的需求,你可以选择合适的方式实现优雅停机,从而提高系统的可靠性和用户体验。