什么是 Dubbo 的优雅停机,怎么实现的?


面试考察点

  1. 生产运维意识:面试官不仅仅是想知道优雅停机的原理,更是想知道你是否理解服务发布上线时的风险——服务正在处理的请求会不会丢失?消费者能不能及时感知提供者下线?
  2. Dubbo SPI 机制理解:优雅停机涉及 ShutdownHookProtocolRegistry 等多个扩展点,看你是否能把这些点串起来。
  3. 版本差异认知:Dubbo 2.6 和 2.7+ 在优雅停机实现上有较大差异(尤其是引入 Spring Boot 后),能否说清楚不同版本的做法。

核心答案

先说结论:Dubbo 的优雅停机,核心目标就是确保服务下线时,已经在处理中的请求能正常完成,同时不再接收新的请求。

整个过程分 3 步

步骤 做了什么 效果
① 从注册中心注销 把自己的服务元数据从注册中心删掉 消费者感知不到我了,不会再发新请求
② 关闭 Server,拒绝新请求 停止接收网络连接和新的 RPC 请求 即使有消费者还没感知到下线,也打不进来了
③ 等待正在处理的请求完成 等一段时间(默认 10 秒),让处理中的请求跑完 保证已有请求不会丢

深度解析

一、为什么需要优雅停机?

先看看 "不优雅" 的停机是什么样的:

上图对比了两种停机方式的差异。关键在于:

  • 不优雅停机kill -9):JVM 直接被杀,正在执行的线程全部中断。消费者的请求要么超时报错,要么连接被重置。更要命的是,注册中心感知到节点下线有延迟(ZooKeeper 的 Session 过期通常要 30 秒),这段时间消费者还在往一个已经死了的节点发请求。
  • 优雅停机kill 发送 SIGTERM):JVM 收到信号后,先执行 ShutdownHook,按照 "注销 → 拒新 → 等完" 的顺序一步步来,确保已有请求处理完才退出。

二、Dubbo 优雅停机的实现原理

Dubbo 的优雅停机入口是 JVM 的 ShutdownHook 机制。

// Dubbo 源码中注册 ShutdownHook 的位置
// org.apache.dubbo.config.DubboShutdownHook

public class DubboShutdownHook extends Thread {

    @Override
    public void run() {
        // 执行优雅停机的核心逻辑
        if (registered.getAndSet(false)) {
            // ① 先销毁注册中心,从注册中心注销自己
            AbstractRegistryFactory.destroyAll();
            // ② 再销毁协议层,关闭 Server,等待已有请求完成
            destroyProtocols();
            // ③ 最后关闭其他资源
            DubboBootstrap.reset();
        }
    }
}

这段代码在 DubboBootstrap 初始化时通过 Runtime.getRuntime().addShutdownHook() 注册到 JVM 中。当收到 SIGTERM 信号(kill 命令默认发送的就是这个信号),JVM 会在退出前执行这个 Hook。

下面展开讲每一步。

第一步:从注册中心注销

上图展示了注销过程。提供者主动向注册中心(Nacos、ZooKeeper 等)发送注销请求,注册中心删除该提供者的服务元数据,并推送变更事件给所有消费者。消费者收到通知后更新本地缓存,后续请求就不会再打到这个提供者了。

但这里有个时间差:从注册中心推送变更到消费者真正更新本地缓存,中间可能有几十到几百毫秒的延迟。在这段时间里,消费者可能还在用旧的缓存列表发请求。所以光注销还不够,还需要第二步。

第二步:关闭 Server,拒绝新请求

// DubboProtocol 的 destroy 方法(简化版)
public void destroy() {
    // 遍历所有的 Server
    for (String key : new ArrayList<>(serverMap.keySet())) {
        ExchangeServer server = serverMap.remove(key);
        if (server != null) {
            // 关闭 Server,底层调用 Netty 的 channel.close()
            // 关闭后,新的 TCP 连接和请求都进不来了
            server.close(getShutdownTimeout());
        }
    }
}

关闭 Server 后,即使有消费者还没更新缓存,请求也会在 TCP 连接层被拒绝。这就堵住了 "注销通知还没到达消费者" 这个时间窗口里的新请求。

Dubbo 还有个细节:关闭 Server 时不是直接 close(),而是先发送一个 readonly 事件(2.7+ 版本),告诉消费者 "我只读了,别再发写请求了"。消费者收到后会立即把流量切走,不等注册中心推送。

第三步:等待已有请求完成

上图展示了等待过程。Dubbo 给了一个超时时间(默认 10 秒,可通过 dubbo.service.shutdown.timeout 配置),在这个时间内如果所有请求都处理完了就提前退出,没处理完到时间也强制退出——防止某个慢请求导致服务一直下不来。

消费者端也要优雅停机

上面说的都是提供者端。消费者端停机时也有对应逻辑:

  • 销毁所有 ReferenceConfig,释放连接资源
  • 关闭与提供者的长连接
  • 断开与注册中心的连接

三、Spring Boot 环境下的注意事项

如果你用的是 Spring Boot + Dubbo(大部分人的选择),有个坑需要注意:Spring Boot 自己也有 ShutdownHook,JVM 的 ShutdownHook 是并发执行的,不保证顺序。

这就可能导致 Spring 容器已经销毁了(Bean 都被回收了),但 Dubbo 的优雅停机还没跑完,结果请求进来发现 Service Bean 都是 null,直接 NPE。

Dubbo 2.7+ 对此做了优化:

  • 默认不注册自己的 ShutdownHook,而是依赖 Spring 的 DisposableBean / @PreDestroy 机制
  • 在 Spring 容器关闭的回调中执行优雅停机,保证 Spring Bean 还活着的时候处理完所有请求
# application.yml 配置
dubbo:
  service:
    shutdown:
      timeout: 30000  # 优雅停机等待时间,默认 10s,生产建议 30s

生产环境中,如果服务里有耗时较长的操作(比如批量导出、大文件上传),建议把超时时间调大一些,避免请求被强制中断。

四、在 Kubernetes 中的优雅停机

现在很多公司用 K8s 部署服务,这里也有个经典坑:K8s 的 Pod 终止流程和 Dubbo 的优雅停机需要配合好。

上图展示了 K8s 和 Dubbo 的配合关系。核心就是一句话:K8s 的 terminationGracePeriodSeconds 必须大于 Dubbo 的优雅停机等待时间,否则 Dubbo 还没等完请求,K8s 就 kill -9 强杀了,前面的优雅停机全白费。

# K8s Deployment 配置示例
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60  # K8s 等待时间
      containers:
        - name: my-service
          # ...
# Dubbo 配置
dubbo:
  service:
    shutdown:
      timeout: 30000  # Dubbo 等待 30s,给 K8s 留 30s 缓冲

面试高频追问

  1. 追问一:kill -9kill 的区别是什么?

    kill(不加 -9)默认发送 SIGTERM 信号,JVM 收到后会执行 ShutdownHook,然后再退出。kill -9 发送 SIGKILL 信号,操作系统直接杀进程,JVM 来不及做任何收尾工作。所以生产发版一定要用 kill,千万别用 kill -9

  2. 追问二:Dubbo 的优雅停机和 Spring Boot 的优雅停机会冲突吗?

    Dubbo 2.7+ 已经处理了这个问题。它默认不再注册自己的 ShutdownHook,而是通过 Spring 的生命周期回调(DisposableBeanSmartLifecycle)来触发优雅停机,保证在 Spring 容器关闭时执行,避免 Bean 已经销毁但请求还在进的尴尬。如果用的老版本,需要手动关闭 Dubbo 的 ShutdownHook-Ddubbo.shutdown.hook=false

  3. 追问三:消费者端下线时也需要优雅停机吗?

    需要。消费者停机时可能有正在进行的 RPC 调用(等待提供者返回结果),如果不优雅停机直接杀进程,这些请求的回调就无法执行了。消费者端的优雅停机会先销毁所有 ReferenceConfig,关闭与提供者的连接,断开注册中心的订阅。

常见面试变体

  • 变体一:"Dubbo 服务发布上线时,怎么保证不影响正在处理的请求?"
  • 变体二:"Dubbo 的 ShutdownHook 做了哪些事情?"
  • 变体三:"在 K8s 里部署 Dubbo 服务,优雅停机要注意什么?"
  • 变体四:"kill -9kill 对 Dubbo 服务有什么不同影响?"

记忆口诀

优雅停机三步走:"注、拒、等"

  • :注册中心注销,消费者不再来新请求
  • :关闭 Server,拒绝所有新连接
  • :等待处理中的请求完成(默认 10 秒,超时强杀)

K8s 一句话terminationGracePeriodSeconds > dubbo.shutdown.timeout,否则白搭。

总结

一句话:Dubbo 的优雅停机通过 ShutdownHook 实现 "注册中心注销 → 关闭 Server 拒绝新请求 → 等待已有请求完成" 三步走,确保服务下线不丢请求。生产环境中要注意 killkill -9 的区别,以及在 K8s 里配好等待时间。