什么是 Dubbo 的优雅停机,怎么实现的?
面试考察点
- 生产运维意识:面试官不仅仅是想知道优雅停机的原理,更是想知道你是否理解服务发布上线时的风险——服务正在处理的请求会不会丢失?消费者能不能及时感知提供者下线?
- Dubbo SPI 机制理解:优雅停机涉及
ShutdownHook、Protocol、Registry等多个扩展点,看你是否能把这些点串起来。 - 版本差异认知: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 缓冲
面试高频追问
-
追问一:
kill -9和kill的区别是什么?kill(不加-9)默认发送SIGTERM信号,JVM 收到后会执行ShutdownHook,然后再退出。kill -9发送SIGKILL信号,操作系统直接杀进程,JVM 来不及做任何收尾工作。所以生产发版一定要用kill,千万别用kill -9。 -
追问二:Dubbo 的优雅停机和 Spring Boot 的优雅停机会冲突吗?
Dubbo 2.7+ 已经处理了这个问题。它默认不再注册自己的
ShutdownHook,而是通过 Spring 的生命周期回调(DisposableBean、SmartLifecycle)来触发优雅停机,保证在 Spring 容器关闭时执行,避免 Bean 已经销毁但请求还在进的尴尬。如果用的老版本,需要手动关闭 Dubbo 的ShutdownHook:-Ddubbo.shutdown.hook=false。 -
追问三:消费者端下线时也需要优雅停机吗?
需要。消费者停机时可能有正在进行的 RPC 调用(等待提供者返回结果),如果不优雅停机直接杀进程,这些请求的回调就无法执行了。消费者端的优雅停机会先销毁所有
ReferenceConfig,关闭与提供者的连接,断开注册中心的订阅。
常见面试变体
- 变体一:"Dubbo 服务发布上线时,怎么保证不影响正在处理的请求?"
- 变体二:"Dubbo 的
ShutdownHook做了哪些事情?" - 变体三:"在 K8s 里部署 Dubbo 服务,优雅停机要注意什么?"
- 变体四:"
kill -9和kill对 Dubbo 服务有什么不同影响?"
记忆口诀
优雅停机三步走:"注、拒、等"
- 注:注册中心注销,消费者不再来新请求
- 拒:关闭 Server,拒绝所有新连接
- 等:等待处理中的请求完成(默认 10 秒,超时强杀)
K8s 一句话:terminationGracePeriodSeconds > dubbo.shutdown.timeout,否则白搭。
总结
一句话:Dubbo 的优雅停机通过 ShutdownHook 实现 "注册中心注销 → 关闭 Server 拒绝新请求 → 等待已有请求完成" 三步走,确保服务下线不丢请求。生产环境中要注意 kill 和 kill -9 的区别,以及在 K8s 里配好等待时间。
