Dubbo服务之优雅停机
服务提供方停止时,先标记为不接收新请求,新请求过来时直接报错,让客户端重试其它机器。然后,检测线程池中的线程是否正在运行,如果有,等待所有线程执行完成,除非超时,则强制关闭。服务消费方停止时,不再发起新的调用请求,所有新的调用在客户端即报错。
然后,检测有没有请求的响应还没有返回,等待响应返回,除非超时,则强制关闭。
这里先讲一下什么是钩子程序:
在Java程序中可以通过添加关闭钩子,实现在程序退出时关闭资源、平滑退出的功能。
使用Runtime.addShutdownHook(Thread hook)方法,可以注册一个JVM关闭的钩子,这个钩子可以在以下几种场景被调用:
- 程序正常退出
- 使用System.exit()
- 终端使用Ctrl+C触发的中断
- 系统关闭
- 使用Kill pid命令干掉进程
我们通过Runtime.getRuntime().addShutdownHook()
注册一个钩子,发现被ApplicationShutdownHooks.add(hook)
调用,最后被保存到一个叫HOOKS
的IdentityHashMap当中,那是什么时候触发钩子程序的呢?原来ApplicationShutdownHooks
里面有一个静态块:
static {
try {
Shutdown.add(1 /* shutdown hook invocation order */,
false /* not registered if shutdown in progress */,
new Runnable() {
public void run() {
runHooks();
}
}
);
hooks = new IdentityHashMap<>();
} catch (IllegalStateException e) {
hooks = null;
}
}
最终会调用runHooks
方法。我们查看System.exit()
,其实最终还是会通过ShutDown.exit()->sequence()
进来,然后调用runHooks
调用钩子程序。那Java是怎么响应kill
命令的呢?竟是通过SignalHandler
来实现的,在openjdk的windows目录和solaris目录下都有一个Terminator.java
,里面有这样一段代码:
SignalHandler sh = new SignalHandler() {
public void handle(Signal sig) {
Shutdown.exit(sig.getNumber() + 0200);
}
};
Signal.handle(new Signal("HUP"), sh);
Signal.handle(new Signal("INT"), sh);
Signal.handle(new Signal("TERM"), sh);
最后通过void* oldHandler = os::signal(sig, newHandler)
获取到linux系统的signal信号。
回过头了看dubbo,可以设置优雅停机超时时间,缺省超时时间是10秒:(超时则强制关闭)
<dubbo:application ...>
<dubbo:parameter key="shutdown.timeout" value="60000" /> <!-- 单位毫秒 -->
</dubbo:application>
看一下服务端钩子程序:
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
if (logger.isInfoEnabled()) {
logger.info("Run shutdown hook now.");
}
ProtocolConfig.destroyAll();
}
}, "DubboShutdownHook"));
其最终还是调用了ProtocolConfig.destroyAll()
方法:
public static void destroyAll() {
AbstractRegistryFactory.destroyAll();
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
方法,下面看一下DubboProtocol
的destroy
方法:
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(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();
} 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();
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
stubServiceMethodsMap.clear();
super.destroy();
}
- 关闭server
因为服务端是通过DubboProtocol
的openServer
通过Netty开启服务的,serverMap.put(key, createServer(url))
。当关闭的时候肯定需要要服务进行关闭,释放端口和系统资源。 - 关闭reference client
共享链接,ReferenceCountExchangeClient - 关闭ghost client(官方注释叫幽灵client)
这个操作只为了防止程序bug错误关闭client做的防御措施 - 清空stub方法Map
- suer.destroy
关闭Invoker,将服务设置成不可用。然后通过Exporter.unexport()关闭导出的服务
总结
线上服务不要轻易的kill -9
,可进行kill
触发应用的钩子程序,做相关的资源清理,如果一直关闭不掉,最终可以通过kill -9
执行。另外网上也有说Dubbo不支持优雅停机的言论,大家可以借鉴一下:
https://my.oschina.net/u/1398931/blog/790709
http://frankfan915.iteye.com/blog/2254097