先说结论
现在相对完美的 dubbo 优雅停机方案是 qos + dubbo 自身优雅停机
qos 接入
前置条件:dubbo >= 2.5.8, netty 4
简而言之,通过 qos 提供的接口,在停机前先调用 qos 提供的 offline 接口下线所有服务 官方文档
和运维同学沟通了下,现在新的 dubbo 应用默认都接入了 qos, 旧的应用如果不确定的话,可以找运维同学确认下 运维接入Qos功能
如果你想监听 qos offline 事件,可以配合 qos-plus 使用 qos offline功能增强
dubbo 优雅停机
官方解释
Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的,所以如果用户使用 kill -9 PID 等强制关闭指令,是不会执行优雅停机的,只有通过 kill PID 时,才会执行。 官方文档
最佳实践
1、低版本的 dubbo 优雅停机有很多问题,建议升级 dubbo 版本到相对稳定的版本(2.6.3进一步优化了dubbo优雅停机和 2.6.5 修复了leastactive loadbalance的预热问题) dubbo 2.5.x 升级到 2.7.15
2、Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的(高版本优化了,放到了 spring ContextClosedEvent 事件里面)
真实项目中依赖关系比较复杂,开发要保证先停掉所有 dubbo 消费的地方(比如 消费 kafka 消息然后调用 dubbo 服务, 定时任务调用 dubbo 服务等),然后再触发 dubbo 的优雅停机
4、JDK 的 ShutdownHook 的执行顺序是并发加无序执行的,建议统一放到 spring ContextClosedEvent 里面,由自己编排需要停机的任务顺序
以下是部分原理分享,有兴趣的可以看下
优雅停机流程
dubbo.service.shutdown.wait设置优雅停机等待时间,默认10s 下文中的10s为该值
- 从注册中心中取消注册自己,从而使消费者不要再拉取到它(client会移除该服务提供者信息)
- sleep (默认10s) 等到服务消费接收到注册中心通知到该服务提供者已经下线 (2.6.x移除了该功能)
- 优先关闭自身对外提供的服务,然后关闭外部的引用,最后关闭幽灵链接(ghostClient)
关闭自身对外提供的服务流程
- sendChannelReadOnlyEvent对所有的consumer广播READONLY事件告诉consumer我要下线了不要调用我了
- 判断连接是否还存在(2.6.2+以后逻辑,之前版本判断 CHANNELS map中是否存在chanel) && 超过timeout (10s)
- 停止心跳
- 关闭server
关闭server流程
- shutdown io 线程池
- 等待线程池中任务执行(最多10s)shutdownNow 强制终止io线程
- 关闭tcp连接
2.6.x优化点
- AnnotationBean销毁时不销毁service 让dubbo 的shutdown hook销毁该服务保证业务处理完成在销毁服务
- 判断连接是否存在的逻辑,就版本中判断CHANNELS map中是否存在chanel 该map会在有请求时放入map,请求完成后移除,会存在请求来了还未放入map中的情况,修改为channel.isConnected() 判断这个连接还连着,就不处理等待超时
- 为什么移除从注册中心下线后 等待时间; 3.1. 关闭自身服务时会广播所有消费者不要给我发消息了 3.2. 关闭服务本身会有等待时间
整合spring
dubbo
的优雅停机和 spring
的关闭都是通过 shutdownhook
实现,但因为 shutdownhook
是 并发 + 无序 执行的,理想状态下希望 dubbo
在 spring
之前关闭
dubbo 从 2.6x 开始逐渐对优雅停机进行优化,加入对spring的支持,以下是 2.7x org.apache.dubbo.config.spring.extension.SpringExtensionFactory
的部分源码
public static void addApplicationContext(ApplicationContext context) {
contexts.add(context);
if (context instanceof ConfigurableApplicationContext) {
// 注册 spring ShutdownHook
((ConfigurableApplicationContext) context).registerShutdownHook();
// 取消dubbo自己默认的shutdownHook
DubboShutdownHook.getDubboShutdownHook().unregister();
}
BeanFactoryUtils.addApplicationListener(context, shutdownHookListener);
}
private static class ShutdownHookListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextClosedEvent) {
DubboShutdownHook shutdownHook = DubboShutdownHook.getDubboShutdownHook();
shutdownHook.doDestroy();
}
}
}
源码分析
- 注册 shutdownhook
com.alibaba.dubbo.config.AbstractConfig中通过静态初始化块注册shutdownhook
// 2.6.x
static {
legacyProperties.put("dubbo.protocol.name", "dubbo.service.protocol");
legacyProperties.put("dubbo.protocol.host", "dubbo.service.server.host");
legacyProperties.put("dubbo.protocol.port", "dubbo.service.server.port");
legacyProperties.put("dubbo.protocol.threads", "dubbo.service.max.thread.pool.size");
legacyProperties.put("dubbo.consumer.timeout", "dubbo.service.invoke.timeout");
legacyProperties.put("dubbo.consumer.retries", "dubbo.service.max.retry.providers");
legacyProperties.put("dubbo.consumer.check", "dubbo.service.allow.no.provider");
legacyProperties.put("dubbo.service.url", "dubbo.service.address");
Runtime.getRuntime().addShutdownHook(DubboShutdownHook.getDubboShutdownHook());
}
// com.alibaba.dubbo.config.DubboShutdownHook
@Override
public void run() {
if (logger.isInfoEnabled()) {
logger.info("Run shutdown hook now.");
}
destroyAll();
}
// 2.5.x
static {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
if (logger.isInfoEnabled()) {
logger.info("Run shutdown hook now.");
}
ProtocolConfig.destroyAll();
}
}, "DubboShutdownHook"));
}
- destroyAll
// 2.6.x
public void destroyAll() {
if (!destroyed.compareAndSet(false, true)) {
return;
}
//通过AbstractRegistryFactory.destroyAll()来“注销”在所有注册中心注册的服务,
// 通过调用ZkClient客户端的zkClient.close()关闭ZK长连接。
// 这样服务消费者就看不到已经被注销的服务了。
// 当然这是理想情况。毕竟从服务提供者注销自己,到消费者发现提供者不可用中间存在一定的时间差。
// @see com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry#destroy
AbstractRegistryFactory.destroyAll();
// destroy all the protocols
ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
for (String protocolName : loader.getLoadedExtensions()) {
try {
Protocol protocol = loader.getLoadedExtension(protocolName);
if (protocol != null) {
protocol.destroy();
}
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
// 2.5.x
public static void destroyAll() {
if (!destroyed.compareAndSet(false, true)) {
return;
}
AbstractRegistryFactory.destroyAll();
// Wait for registry notification
// 2.6.x去掉了下面代码
try {
Thread.sleep(ConfigUtils.getServerShutdownTimeout());
} catch (InterruptedException e) {
logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");
}
ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
for (String protocolName : loader.getLoadedExtensions()) {
try {
Protocol protocol = loader.getLoadedExtension(protocolName);
if (protocol != null) {
protocol.destroy();
}
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
- protocol.destroy() 以dubbo协议为列
@Override
public void destroy() {
for (String key : new ArrayList<String>(serverMap.keySet())) {
ExchangeServer server = serverMap.remove(key);
if (server != null) {
try {
if (logger.isInfoEnabled()) {
logger.info("Close dubbo server: " + server.getLocalAddress());
}
//关闭自己暴露的服务
server.close(ConfigUtils.getServerShutdownTimeout());
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
for (String key : new ArrayList<String>(referenceClientMap.keySet())) {
ExchangeClient client = referenceClientMap.remove(key);
if (client != null) {
try {
if (logger.isInfoEnabled()) {
logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
}
// 优雅的关闭对下游服务的调用,停止心跳并关闭连接
client.close(ConfigUtils.getServerShutdownTimeout());
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
// 关闭幽灵连接
for (String key : new ArrayList<String>(ghostClientMap.keySet())) {
ExchangeClient client = ghostClientMap.remove(key);
if (client != null) {
try {
if (logger.isInfoEnabled()) {
logger.info("Close dubbo connect: " + client.getLocalAddress() + "-->" + client.getRemoteAddress());
}
client.close(ConfigUtils.getServerShutdownTimeout());
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
stubServiceMethodsMap.clear();
super.destroy();
}