Spring Boot 服务优雅上下线实践指南
为什么要优雅上下线?
在微服务架构中,应用实例的频繁部署、扩缩容、重启都是常态。
如果不进行优雅上下线(Graceful Startup & Shutdown),就可能导致:
- 正在处理的请求被强制中断,造成接口异常或数据异常;
- 服务注册中心(如 Nacos)仍然认为节点可用,导致请求被路由到已关闭的实例;
- 新实例还没充分启动完成,请求就进来了,导致接口异常。
为了解决这些问题,我们需要让服务在启动和关闭阶段都更有序、更“温柔”、更可控。
Spring Boot 内置的优雅关闭机制
从 Spring Boot 2.3+ 开始,官方就支持优雅关闭(Graceful Shutdown):
server:
shutdown: graceful或通过启动参数启用:
-Dserver.shutdown=graceful这表示当 Spring Boot 接收到关闭信号(如 SIGTERM)时,不会立刻中断请求,而是:
- 停止接收新请求;
- 等待当前正在处理的请求完成;
- 超过等待时间后强制关闭。
⚠️ 注意:
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=serviceregistry2️⃣ 使用 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("服务即将关闭。");
}
}