核心概念
优雅停机(Graceful Shutdown) 是指在服务关闭时,不是立即终止所有请求,而是:
- 停止接收新请求(注销服务、关闭端口)
- 等待正在处理的请求完成(设置超时时间)
- 清理资源(关闭连接、释放线程池)
核心目标:
- 避免请求丢失(Consumer 调用已关闭的 Provider)
- 避免请求失败(Provider 强制关闭,导致正在处理的请求异常)
- 保证服务的高可用性
对比暴力停机:
// 暴力停机(kill -9)
kill -9 <pid>
// 问题:
// 1. 正在处理的请求被强制中断
// 2. Consumer 不知道 Provider 已下线,继续发送请求
// 3. 连接未正常关闭,可能导致资源泄漏
// 优雅停机(kill 或 Ctrl+C)
kill <pid> // 或 Ctrl+C
// 优点:
// 1. 等待正在处理的请求完成
// 2. 主动通知 Consumer,Provider 即将下线
// 3. 正常关闭连接,释放资源
优雅停机的流程
1. 完整流程
┌─────────────────────────────────────────────────────────┐
│ 1. 收到停机信号(kill、Ctrl+C、程序退出) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. 标记服务为"正在关闭"状态 │
│ - 不再接收新的请求 │
│ - 向注册中心注销服务 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. 等待正在处理的请求完成 │
│ - 设置超时时间(默认 10 秒) │
│ - 定期检查是否还有请求在处理 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 4. 关闭服务器和连接 │
│ - 关闭 Netty Server(Provider 端) │
│ - 关闭 Netty Client(Consumer 端) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 5. 清理资源 │
│ - 关闭线程池 │
│ - 释放注册中心连接 │
│ - 清理缓存 │
└─────────────────────────────────────────────────────────┘
2. Provider 端优雅停机流程
// ==================== Provider 优雅停机 ====================
// 1. 注册 JVM ShutdownHook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("开始优雅停机...");
// 2. 标记为"正在关闭"状态
shutdownFlag.set(true);
// 3. 向注册中心注销服务
registryProtocol.unexport();
// Zookeeper 删除服务节点
// /dubbo/com.example.UserService/providers/dubbo%3A%2F%2F192.168.1.100%3A20880
// 4. 等待一小段时间,让 Consumer 接收到下线通知
Thread.sleep(1000); // 等待 1 秒
// 5. 停止接收新请求(关闭 Netty Server)
for (ProtocolServer server : servers) {
server.close(); // 不再接收新连接
}
// 6. 等待正在处理的请求完成
long timeout = 10000; // 超时时间 10 秒
long start = System.currentTimeMillis();
while (hasActiveRequests()) {
if (System.currentTimeMillis() - start > timeout) {
System.out.println("超时,强制关闭");
break;
}
Thread.sleep(100); // 每 100ms 检查一次
}
// 7. 关闭所有连接
for (Channel channel : channels) {
channel.close();
}
// 8. 关闭线程池
executorService.shutdown();
executorService.awaitTermination(timeout, TimeUnit.MILLISECONDS);
// 9. 清理资源
registryProtocol.destroy();
System.out.println("优雅停机完成");
}));
// 检查是否还有正在处理的请求
private boolean hasActiveRequests() {
// 统计活跃请求数
int activeCount = 0;
for (ProtocolServer server : servers) {
activeCount += server.getActiveCount();
}
return activeCount > 0;
}
3. Consumer 端优雅停机流程
// ==================== Consumer 优雅停机 ====================
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Consumer 开始优雅停机...");
// 1. 标记为"正在关闭"状态
shutdownFlag.set(true);
// 2. 停止发起新的请求
// 新的调用会抛出异常
// 3. 等待正在进行的 RPC 调用完成
long timeout = 10000;
long start = System.currentTimeMillis();
while (hasPendingRequests()) {
if (System.currentTimeMillis() - start > timeout) {
System.out.println("超时,强制关闭");
break;
}
Thread.sleep(100);
}
// 4. 取消订阅(从注册中心注销)
registryProtocol.destroy();
// 5. 关闭所有连接
for (ReferenceConfig<?> config : referenceConfigs) {
config.destroy();
}
// 6. 清理资源
executorService.shutdown();
System.out.println("Consumer 优雅停机完成");
}));
核心实现原理
1. ShutdownHook 机制
JVM 提供的钩子:
// 注册 ShutdownHook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// 优雅停机逻辑
}));
// 触发时机:
// 1. 正常退出:main 方法执行完毕
// 2. System.exit(0)
// 3. 收到 SIGTERM 信号(kill <pid>)
// 4. Ctrl+C(发送 SIGINT 信号)
// 5. IDE 停止按钮
// 不触发时机:
// 1. kill -9 <pid>(SIGKILL 信号,无法捕获)
// 2. JVM 崩溃
// 3. 操作系统强制关闭
Dubbo 的 ShutdownHook 注册:
public class DubboShutdownHook extends Thread {
private static final DubboShutdownHook INSTANCE = new DubboShutdownHook();
private DubboShutdownHook() {
super("DubboShutdownHook");
}
public static DubboShutdownHook getInstance() {
return INSTANCE;
}
public void register() {
Runtime.getRuntime().addShutdownHook(this);
}
@Override
public void run() {
// 执行优雅停机逻辑
doShutdown();
}
private void doShutdown() {
// 1. 注销服务
ProtocolConfig.destroyAll();
// 2. 关闭注册中心连接
RegistryConfig.destroyAll();
// 3. 清理缓存
clearCaches();
System.out.println("Dubbo 优雅停机完成");
}
}
// 在 Dubbo 启动时注册
DubboShutdownHook.getInstance().register();
2. 注销服务(Registry)
// Provider 优雅停机时,向注册中心注销服务
public class RegistryProtocol {
public void unexport() {
// 遍历所有已暴露的服务
for (Exporter<?> exporter : exporters.values()) {
try {
// 1. 从注册中心注销
URL url = exporter.getInvoker().getUrl();
registry.unregister(url);
// 2. 本地下线
exporter.unexport();
} catch (Throwable t) {
logger.warn("注销服务失败", t);
}
}
}
}
// Zookeeper 实现
public class ZookeeperRegistry {
@Override
public void unregister(URL url) {
// 删除临时节点
String path = toUrlPath(url);
// /dubbo/com.example.UserService/providers/dubbo%3A%2F%2F192.168.1.100%3A20880
try {
zkClient.delete(path);
// Consumer 会收到节点删除通知(Watch 机制)
// 自动从可用 Provider 列表中移除
} catch (Exception e) {
logger.error("删除节点失败", e);
}
}
}
3. 等待活跃请求完成
// 统计活跃请求数
public class ActiveCountManager {
// 每个方法的活跃请求数
private final ConcurrentMap<String, AtomicInteger> activeCountMap =
new ConcurrentHashMap<>();
// 请求开始
public void beginRequest(String methodName) {
activeCountMap.computeIfAbsent(methodName, k -> new AtomicInteger())
.incrementAndGet();
}
// 请求结束
public void endRequest(String methodName) {
AtomicInteger count = activeCountMap.get(methodName);
if (count != null) {
count.decrementAndGet();
}
}
// 获取总活跃请求数
public int getTotalActiveCount() {
return activeCountMap.values().stream()
.mapToInt(AtomicInteger::get)
.sum();
}
}
// Provider 端处理请求
public class DubboProtocol {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) {
// 1. 检查是否正在关闭
if (isShuttingDown()) {
throw new RpcException("服务正在关闭,拒绝新请求");
}
String methodName = invocation.getMethodName();
try {
// 2. 增加活跃请求数
activeCountManager.beginRequest(methodName);
// 3. 执行业务逻辑
return invoker.invoke(invocation);
} finally {
// 4. 减少活跃请求数
activeCountManager.endRequest(methodName);
}
}
}
// 优雅停机时等待
public void awaitTermination(long timeout) {
long start = System.currentTimeMillis();
while (activeCountManager.getTotalActiveCount() > 0) {
if (System.currentTimeMillis() - start > timeout) {
// 超时,强制关闭
logger.warn("等待超时,还有 {} 个活跃请求未完成",
activeCountManager.getTotalActiveCount());
break;
}
try {
Thread.sleep(100); // 每 100ms 检查一次
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
4. 关闭连接和资源
// 关闭 Netty Server
public class NettyServer {
private final EventLoopGroup bossGroup;
private final EventLoopGroup workerGroup;
private final Channel channel;
public void close() {
try {
// 1. 关闭 Channel(不再接收新连接)
if (channel != null) {
channel.close().syncUninterruptibly();
}
// 2. 优雅关闭 EventLoopGroup(等待任务完成)
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
} catch (Throwable t) {
logger.warn("关闭 Netty Server 失败", t);
}
}
}
// 关闭线程池
public class ExecutorUtil {
public static void gracefulShutdown(ExecutorService executor, long timeout) {
if (executor == null) return;
try {
// 1. 停止接收新任务
executor.shutdown();
// 2. 等待已提交的任务完成
if (!executor.awaitTermination(timeout, TimeUnit.MILLISECONDS)) {
// 3. 超时,强制关闭
executor.shutdownNow();
// 4. 再次等待
if (!executor.awaitTermination(timeout, TimeUnit.MILLISECONDS)) {
logger.warn("线程池无法正常关闭");
}
}
} catch (InterruptedException e) {
// 中断,强制关闭
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
配置与使用
1. 默认配置
dubbo:
provider:
# 优雅停机超时时间(毫秒)
shutdown:
timeout: 10000 # 默认 10 秒
# 是否等待所有请求完成
shutdown:
wait: true # 默认 true
2. 编程式配置
@Configuration
public class DubboConfig {
@Bean
public ProviderConfig providerConfig() {
ProviderConfig config = new ProviderConfig();
// 设置优雅停机超时时间
config.setShutdownWait(10000); // 10 秒
return config;
}
}
3. 优雅停机最佳实践
// Provider 端配置
dubbo:
provider:
shutdown:
timeout: 30000 # 增加超时时间(处理慢请求)
wait: true
# 拒绝新请求时的响应
status: "closing"
// Consumer 端配置
dubbo:
consumer:
shutdown:
timeout: 10000
# 容错策略:失败自动切换到其他 Provider
cluster: failover
retries: 2
常见问题与解决方案
问题 1:Provider 下线后,Consumer 仍然调用
原因:
- Consumer 缓存了 Provider 列表
- 注册中心通知有延迟
解决方案:
// 1. Provider 延迟关闭(等待通知传播)
dubbo:
provider:
shutdown:
# 先注销服务,等待 1 秒后再关闭
delay: 1000
// 2. Consumer 配置容错策略
@Reference(
cluster = "failover", // 失败自动切换
retries = 2, // 重试 2 次
timeout = 3000 // 超时 3 秒
)
private UserService userService;
问题 2:长时间运行的任务被强制中断
原因:
- 优雅停机超时时间不够
解决方案:
// 1. 增加超时时间
dubbo:
provider:
shutdown:
timeout: 60000 # 60 秒
// 2. 任务支持中断
public class UserServiceImpl implements UserService {
@Override
public void processLongTask(Long taskId) {
// 检查是否正在关闭
while (!isShuttingDown() && hasMoreWork()) {
// 处理任务
processChunk();
// 定期检查关闭标志
if (isShuttingDown()) {
// 保存进度,下次继续
saveProgress();
throw new RpcException("服务关闭,任务中断");
}
}
}
}
问题 3:kill -9 导致请求丢失
原因:
- kill -9 无法触发 ShutdownHook
解决方案:
// 1. 使用 kill(不带 -9)
kill <pid> // 发送 SIGTERM 信号,触发优雅停机
// 2. 在容器环境(K8s)配置 preStop 钩子
apiVersion: v1
kind: Pod
spec:
containers:
- name: dubbo-provider
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"] # 延迟 15 秒
答题总结
Dubbo 优雅停机的核心要点:
1. 概念:
- 优雅停机:停止接收新请求 → 等待请求完成 → 清理资源
- 暴力停机(kill -9):立即终止,导致请求丢失
2. 实现流程(Provider 端):
收到停机信号
↓
标记"正在关闭"状态
↓
向注册中心注销服务(Consumer 感知下线)
↓
停止接收新请求(关闭 Server 端口)
↓
等待正在处理的请求完成(超时 10 秒)
↓
关闭连接和线程池
↓
清理资源
3. 核心实现机制:
- ShutdownHook:注册 JVM 关闭钩子,捕获停机信号
- 注销服务:从注册中心删除节点,通知 Consumer
- 活跃请求统计:跟踪正在处理的请求数,等待完成
- 优雅关闭连接:Netty、线程池的优雅关闭
4. 配置:
dubbo:
provider:
shutdown:
timeout: 10000 # 超时时间 10 秒
wait: true # 等待请求完成
5. 最佳实践:
- 使用
kill(不带 -9)触发优雅停机 - 配置合理的超时时间(根据业务)
- Consumer 配置容错策略(failover + retries)
- 长时间任务支持中断
6. 优势:
- ✅ 避免请求丢失
- ✅ 避免请求失败
- ✅ 保证服务高可用
- ✅ 正常释放资源
面试技巧:强调优雅停机的目标(避免请求丢失)、流程(注销 → 等待 → 清理)和实现机制(ShutdownHook + 活跃请求统计)。可以对比暴力停机的问题,说明优雅停机在高可用系统中的重要性。