核心概念

优雅停机(Graceful Shutdown) 是指在服务关闭时,不是立即终止所有请求,而是:

  1. 停止接收新请求(注销服务、关闭端口)
  2. 等待正在处理的请求完成(设置超时时间)
  3. 清理资源(关闭连接、释放线程池)

核心目标

  • 避免请求丢失(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 + 活跃请求统计)。可以对比暴力停机的问题,说明优雅停机在高可用系统中的重要性。