dubbo優雅停機的實現,首先主要依賴於jvm的ShutdownHook鉤子函數,例如dubbo 2.5.x版本,在AbstractConfig中定義了:
static {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
if (logger.isInfoEnabled()) {
logger.info("Run shutdown hook now.");
}
ProtocolConfig.destroyAll();
}
}, "DubboShutdownHook"));
}
在靜態塊裏面註冊了一個關閉鉤子,當jvm準備關閉時(tomcat shutdown命令、kill pid等),會自動觸發註冊好的關閉鉤子,執行ProtocolConfig.destroyAll()方法,此方法主要做了從註冊中心(比如zookeeper)解註冊、關閉provider、關閉consumer三件事情。
1、第一步解註冊,consumer和provider都會在zookeeper註冊臨時節點,在停機時首先調用了AbstractRegistryFactory的destroyAll():
public static void destroyAll() {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Close all registries " + getRegistries());
}
// Lock up the registry shutdown process
LOCK.lock();
try {
for (Registry registry : getRegistries()) {
try {
registry.destroy();
} catch (Throwable e) {
LOGGER.error(e.getMessage(), e);
}
}
REGISTRIES.clear();
} finally {
// Release the lock
LOCK.unlock();
}
}
在該方法中首先拿到本機所有的registry(dubbo官方的角色說明:registry是服務註冊與發現的註冊中心。爲了便於理解,registry可以不嚴謹地看做本機整體服務註冊到註冊中心的信息),然後調用registry的destroy()方法執行解註冊。值得一提的是,dubbo裏面使用了很多設計模式,整個註冊中心的邏輯部分使用了模板模式:
比如我們使用的zookeeper,那麼會來到ZookeeperRegistry.destroy(),
@Override
public void destroy() {
super.destroy();
try {
zkClient.close();
} catch (Exception e) {
logger.warn("Failed to close zookeeper client " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
裏面依次調用其父類FailbackRegistry及其祖父類AbstractRegistry.destroy(),在其中會執行
for (URL url : new HashSet<>(getRegistered())) {
if (url.getParameter(DYNAMIC_KEY, true)) {
try {
unregister(url);
if (logger.isInfoEnabled()) {
logger.info("Destroy unregister url " + url);
}
} catch (Throwable t) {
logger.warn("Failed to unregister url " + url + " to registry " + getUrl() + " on destroy, cause: " + t.getMessage(), t);
}
}
}
這麼做是爲了在虛基類中執行一些公共處理,不依賴於具體註冊中心,比如將 private final Set <URL> registered
,註冊URL Set中對應的URL(provider和consumer)刪掉。
以及在ZookeeperRegistry中:
@Override
public void doUnregister(URL url) {
try {
zkClient.delete(toUrlPath(url));
} catch (Throwable e) {
throw new RpcException("Failed to unregister " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
刪掉在zookeeper中添加的節點。以及前面提到的zkClient.close();關閉zk客戶端。
解註冊的過程基本如此。
2、從註冊中心解註冊之後,本機跟註冊中心之間的連接就斷開了,接下來銷燬所有的protocol。protocol屬於遠程調用層,封裝 RPC 調用。
private void destroyProtocols() {
//獲取Protocol的擴展點實現類
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擴展點實現,包括實際外部通信協議(這裏是dubbo)、injvm(本機通信)、registry protocol,這裏只重點關注dubbo protocol,來到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(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(getServerShutdownTimeout());
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
stubServiceMethodsMap.clear();
super.destroy();
}
這裏首先做的服務端的關閉,調用ExchangeServer接口的實現類HeaderExchangeServer
public void close(final int timeout) {
startClose();
if (timeout > 0) {
final long max = (long) timeout;
final long start = System.currentTimeMillis();
if (getUrl().getParameter(Constants.CHANNEL_SEND_READONLYEVENT_KEY, true)) {
logger.info("============getUrl():" + getUrl());
sendChannelReadOnlyEvent();
}
while (HeaderExchangeServer.this.isRunning()
&& System.currentTimeMillis() - start < max) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
logger.warn(e.getMessage(), e);
}
}
}
doClose();
server.close(timeout);
}
如果設置了發送readonly事件,則先執行sendChannelReadOnlyEvent();這是一個單向請求,由服務端發給消費端,告訴consumer我要關閉channel了,只能從channle中讀未讀取完的內容。
HeaderExchangeServer.this.isRunning()判斷是否還有客戶端持有連接,如果有的話sleep 10ms然後再一直嘗試直到能夠關閉(此處2.5.x版本存在bug,導致不能正確判斷客戶端持有連接,因此2.5版本里面需要在解註冊後sleep 10s後才執行protocol的關閉)。
在 doClose()裏面:
private void doClose() {
if (!closed.compareAndSet(false, true)) {
return;
}
stopHeartbeatTimer();
try {
scheduled.shutdown();
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
執行關閉服務端客戶端之間TCP長連接的心跳檢測。
在server.close(timeout);方法:
仍然使用模板模式,首先AbstractServer關閉服務線程池:
public void close(int timeout) {
ExecutorUtil.gracefulShutdown(executor, timeout);
close();
}
public void close() {
if (logger.isInfoEnabled()) {
logger.info("Close " + getClass().getSimpleName() + " bind " + getBindAddress() + ", export " + getLocalAddress());
}
ExecutorUtil.shutdownNow(executor, 100);
try {
super.close();
} catch (Throwable e) {
logger.warn(e.getMessage(), e);
}
try {
doClose();
} catch (Throwable e) {
logger.warn(e.getMessage(), e);
}
}
然後doClose();到實際的實現類(比如NettyServer)關閉了實際建立的連接。
到這裏服務端的關閉完成,接下來關閉客戶端,由於客戶端與服務端關閉十分類似,不再贅述。
總結
整個dubbo服務關閉的過程,可大致歸納爲:
- jvm關閉,關閉鉤子調用dubbo關閉服務
- 註冊中心解註冊,刪除consumer和provider節點
- 服務端關閉
- protocol的註銷
- 發送readonly
- 停止與客戶端長連接心跳檢測
- 服務線程池關閉
- NettyServer關閉
待續:dubbo註冊jvm關閉鉤子時存在的問題。