为什么要优雅上下线?

在微服务架构中,应用实例的频繁部署、扩缩容、重启都是常态。
如果不进行优雅上下线(Graceful Startup & Shutdown),就可能导致:

  • 正在处理的请求被强制中断,造成接口异常或数据异常;
  • 服务注册中心(如 Nacos)仍然认为节点可用,导致请求被路由到已关闭的实例;
  • 新实例还没充分启动完成,请求就进来了,导致接口异常。

为了解决这些问题,我们需要让服务在启动和关闭阶段都更有序、更“温柔”、更可控。

Spring Boot 内置的优雅关闭机制

从 Spring Boot 2.3+ 开始,官方就支持优雅关闭(Graceful Shutdown):

server:
  shutdown: graceful

或通过启动参数启用:

-Dserver.shutdown=graceful

这表示当 Spring Boot 接收到关闭信号(如 SIGTERM)时,不会立刻中断请求,而是:

  1. 停止接收新请求;
  2. 等待当前正在处理的请求完成;
  3. 超过等待时间后强制关闭。
⚠️ 注意:
Kubernetes 的 terminationGracePeriodSeconds 也默认为 30 秒。
通常建议比 spring.lifecycle.timeout-per-shutdown-phase 多几秒,例如设置成 35 秒,以防请求未处理完就被容器强制终止。

配合 Nacos 实现优雅下线

当 Spring Boot 应用关闭时,如果没有及时从 Nacos 注销,注册中心仍会把流量转发到这个实例。
因此,我们要在关闭前主动告诉 Nacos:“我下线啦!”

1️⃣ 开启 actuator 的 serviceregistry 端点

Spring Cloud Alibaba 的 Nacos 注册组件提供了 Actuator 的 serviceregistry 端点,允许通过 HTTP 修改注册状态。

在配置文件中启用:

management:
  endpoints:
    web:
      exposure:
        include: serviceregistry

或通过启动参数:

-Dmanagement.endpoints.web.exposure.include=serviceregistry

2️⃣ 使用 preStop 主动下线

Kubernetes 在删除 Pod 前,会触发 preStop 钩子。
我们可以在这里调用 actuator 接口,将实例状态改为 DOWN,让 Nacos 立即下线。

lifecycle:
  preStop:
    exec:
      command:
        - sh
        - -c
        - |-
          curl -XPOST -H "Content-Type: application/json" \
               -d '{"status": "DOWN"}' \
               http://localhost/actuator/serviceregistry

这样做可以确保:

  • 下线信号发出后,Nacos 不再分配流量;
  • 然后进入 Spring Boot 的 graceful shutdown 阶段,等待请求完成;
  • 最后容器再被销毁,整个过程丝滑自然。

代码实现:延迟注册 & 延迟下线

除了配置层面的控制,我们还可以在代码中实现更灵活的控制逻辑。

✅ 优雅上线:延迟注册到 Nacos

有时服务刚启动完成,但还没完全准备好(缓存没加载完、依赖服务没准备好等),
此时立刻注册到 Nacos 可能导致流量提前进入。
因此可以延迟几秒再注册。

@Component
public class DelayedNacosRegister implements ApplicationListener<WebServerInitializedEvent> {

    private final NacosServiceRegistry registry;
    private final Registration registration;

    private int localPort;

    public DelayedNacosRegister(NacosServiceRegistry registry, Registration registration) {
        this.registry = registry;
        this.registration = registration;
    }

    @Override
    public void onApplicationEvent(WebServerInitializedEvent event) {
        this.localPort = event.getWebServer().getPort();
        // 延迟一段时间再注册
        new Thread(() -> {
            try {
                Thread.sleep(10000); // 延迟 10 秒
                // 更新 registration 的端口
                if (registration instanceof NacosRegistration nacosRegistration) {
                    nacosRegistration.setPort(localPort);
                }
                registry.register(registration);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    }
}

✅ 优雅下线:先注销再等待

@Component
@Slf4j
public class GracefulShutdownHandler implements ApplicationListener<ContextClosedEvent> {

    private final NacosServiceRegistry registry;
    private final Registration registration;

    public GracefulShutdownHandler(NacosServiceRegistry registry, Registration registration) {
        this.registry = registry;
        this.registration = registration;
    }

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        try {
            // 1. 先从 Nacos 注销实例
            registry.deregister(registration);
            log.info("已从 Nacos 注销实例,等待 30 秒再关闭服务...");

            // 2. 等待 30 秒,让请求处理完毕
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        log.info("服务即将关闭。");
    }
}

标签: none

添加新评论