一次線上問題引發的對dubbo優雅下線的思考


一.背景
       我們經常聊到dubbo的啟動,是如何暴露接口的,如何注冊到注冊中心的,但是就一個完整的生命周期而言,有上線就必然有下線,而下線這一部分往往被人忽略,這次就一次線上發布問題為入口,來分析dubbo下線的過程和其中遇到的問題,從另一個方面加深dubbo整個生命周期的理解。

二.案例
       某次生產發布,雖是對外停機發布,一般內部來講,仍采用的是藍綠發布,即先新起服務,等新的服務健康檢查通過后,在殺掉原來的服務進程。開始正常,新服務健康起來,舊的容器(公司使用的是docker封裝的服務)被殺掉,測試同學開始驗證,然后問題來了,新的服務接口時不時出現超時,平均每三次出現倆次,這個不像是網絡或者環境抖動,一開始檢查業務是不是有慢查詢,但是發現出問題的接口沒有更新,屬於原來的回歸,開始還比較慌,有點摸不着頭腦,后來慢慢把視線轉到超時日志上,超時日志打印出來超時的provider的地址,這個地址不是新發布的服務端地址!!

   不是新服務的地址,是哪的呢? 趕緊打開ZK的控制台,發現這個服務的provider的有兩個,新發布的服務只是其中一個,如下圖所示:

 

 

這個服務因為不是核心業務,目前是單例的,為什么會有兩個provider,那另一個是從哪來的?

找了一通,然后我們在被殺死的容器里面找到了這個地址

 

這不是被殺死的服務嗎,為什么還在ZK的列表里面? 是服務沒下線,還是下線了ZK那里沒有注銷掉? 問題已經出來,然后我們來分析

三.分析
       首先要確定到底是ZK沒下線,還是服務本身就沒下線(公司的容器調度平台是自研,也有出bug的可能),然后現場就聯系的平台部的同事,反饋是容器確實本殺掉了,也就是進程已經不存在了,但是ZK上這個provider還存在。為什么?

    1> dubbo的注冊機制
       要回答這個問題,我們先來看看dubbo的注冊機制,在dubbo啟動的時候,會進行provider的初始化,這里面包括暴露服務端口,注冊服務,啟動底層通信模塊等一系列的動作,網上有很多資料可查,這里不在贅述,就注冊服務這個環節進行分析

       注冊服務,即 將應用需要暴露的接口注冊到注冊中心(zookeeper或nacos),供消費者訂閱以及控制台進行配置和管理,以ZK(zookeeper)為例,provider 注冊到ZK上的節點形式一般如下:

ls /dubbo/com.test.demo.shard.dubbo.DubboDeno/providers/dubbo%3A%2F%2F30.43.89.110%3A26880%2Fcom.test.demo.shard.dubbo.DubboDeno%3Fanyhost%3Dtrue%26application%3Doms-starter%26dubbo%3D2.6.2%26generic%3Dfalse%26interface%3Dcom.test.demo.shard.dubbo.DubboDeno%26methods%3DgetValue%26pid%3D44050%26retries%3D0%26revision%3D1.0.0%26side%3Dprovider%26timestamp%3D1579164865375%26version%3D1.0.0

 


組成結構是 dubbo + 接口類名 + providers/consumers/configration/router/ + 具體屬性值(地址,版本,方法和參數等等),providers下面是一個list,每注冊一個provider實例,就會添加進去。

ZK上面的節點一般有兩種類型,持久節點和臨時節點,

持久節點(persisit):指節點不隨session變化而變化,一直存在的節點;

臨時節點(ephemeral):一旦注冊這個節點的客戶端連接斷開或者session超時,zk會自動將其注冊的臨時節點和監聽器給清除掉;

另外注意,臨時節點不能有葉子節點。

dubbo的provider是什么類型呢,我們看下對應的源碼,dubbo的與注冊相關的接口都封裝在ZookeeperRegistry里面(以ZK為例,其他類型注冊中心會在XXRegistry),看下注冊的過程:

@Override
protected void doRegister(URL url) {
try {
zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}

 


這個注意到那個url.getParameter表達式,如果沒有取到值的話,默認為true,,接着往下看

@Override
public void create(String path, boolean ephemeral) {
int i = path.lastIndexOf('/');
if (i > 0) {
String parentPath = path.substring(0, i);
if (!checkExists(parentPath)) {
create(parentPath, false);
}
}
if (ephemeral) {
createEphemeral(path);
} else {
createPersistent(path);
}
}

 


這個先檢查路徑是否創建,路徑是什么,就是之前結構里面的前半部分dubbo + 接口類名 +providers/consumers/configration/router/,這里面可以看到,路徑都是持久化節點,為什么,因為路徑臨時節點不能有子節點,真正的provider是掛在路徑下面的;

下面繼續看,創建具體節點的時候,if ..else, 這個里面的判斷條件就是之前那個url.getParameter,這個的默認值,我們看

//AbstractServiceConfig
// whether to register as a dynamic service or not on register center
protected Boolean dynamic;

 


在AbstractServiceConfig里面,一般是沒有默認值的,所以根據上面的語句,給出表達式給出默認值值true;注意,這個不是所有的版本都沒有默認值,比如在dubbo 2.7.1的版本里面,dynamic就直接默認是false,后面會專門說。

這樣的話,再看創建節點,實際上ephemeral =true的話,那么創建的就是臨時節點,dubbo服務接口在ZK上的注冊的節點都是臨時節點,也就是說,一旦provider斷開連接,或者宕機,ZK會把所有的這個實例注冊的節點全部清除掉,然后把刪除后的節點內容推送到所有的消費端,這樣消費者在進行負載均衡的時候,就不會在選擇到已下線的provider,這恰恰是我們服務治理所需要的! 從這點來講,ZK 非常契合dubbo對注冊中心能力的需求。

       看完了注冊上線的流程,我們再看服務下線的流程,上面已經說了一種情況,非正常下線,連接斷開或者服務宕機,就是ZK自動清理節點;還是一種是主動去ZK上把之前注冊的節點都注銷,這是更為可靠和優雅的做法,這就是dubbo的優雅停機。列舉下,

1.自動下線,就是如上面所述,通過斷開連接,由zk自動清理

2.服務下線時,執行destroy方法,注銷所有暴露的接口,以及在注冊中心的節點和監聽器,回收相關的資源

第二種就是dubbo的優雅停機,怎么觸發,我們來專門看

   2> dubbo的優雅停機
       dubbo的優雅停機實際上一個重要的問題,也是一個很容易的被忽略的問題,了解它,對保證線上的平穩運行,理解dubbo的完整生命周期都有很強的啟發意義,說句題外話,之前也是不怎么關注這塊 ,直到線上出了這次的問題。

       拋開現有成熟方案,單就思考,如果這樣的問題丟出來給我們,應該怎么做;熟悉spring的朋友應該不陌生,spring里面的bean都可以配置一個destroy方法,這個方法會在bean被銷毀的時候執行,然后可以實現這個方法來實現自己的銷毀邏輯,比如釋放一些資源,或者打印日志,統計參數之類,那么這個方法什么時候執行,當然是bean被銷毀的時候,那么什么時候被銷毀,一般來講,bean的生命都是由spring容器管理的,額外的講,這個方法,不會說讓你怎么銷毀bean,而是spring銷毀bean的時候,你還可以額外做些什么,屬於你定制的東西,也就是鈎子,會在容器銷毀bean的時候調用,而就一般的業務邏輯,不會單獨去銷毀一個bean,所以實際是整個容器生命周期結束的時候,或者容器本身被銷毀的時候,也就是服務被停掉的時候。講這些想說明什么?是想說,一般好的框架設計,會提供若干鈎子函數或者事件,實現或者復寫這些方法可以實現我們定制的邏輯,在框架啟停或者對應特定時刻,來執行這些邏輯。

       事實上,dubbo也確實是這么設計的,一般來講,除非熱加載的情形,我們不太會單獨去銷毀或替換某個bean, 更常見的是在應用即將關閉的時候銷毀所有的bean,dubbo提供了一個專門用於優雅停機的鈎子DubboShutdownHook,這個鈎子會在spring上下文關閉的時候被調用,用於釋放與dubbo相關的資源,包括dubbo底層網絡通信的服務,在zk上注冊的節點,dubbo本身的配置對象等等。我們來看下對應的源碼和觸發時機。

@Override
public void run() {
if (logger.isInfoEnabled()) {
logger.info("Run shutdown hook now.");
}
doDestroy();
}


/**
* Destroy all the resources, including registries and protocols.
*/
public void doDestroy() {
if (!destroyed.compareAndSet(false, true)) {
return;
}
// destroy all the registries
AbstractRegistryFactory.destroyAll();
// destroy all the protocols
destroyProtocols();
}

 


   里面主要是做兩個事情,銷毀所有與注冊的中心相關的節點和監聽器;消息所有的dubbo protocol。

   那么什么時候觸發呢,看注冊機制,

private static class ShutdownHookListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextClosedEvent) {
DubboShutdownHook shutdownHook = DubboShutdownHook.getDubboShutdownHook();
shutdownHook.doDestroy();
}
}
}

 


很明顯,是在監聽到spring 的容器關閉事件后觸發的,一般來講,也就是服務被停掉的時候。

       這個里面還有一個問題,就是並不是所有的時候都會觸發這個事件,我們知道spring本身也是運行在java虛擬機里面的,java虛擬機是運行在操作系統里面,從廣義角度講,一個容器里面的元素提供鈎子,供承載它的平台在結束時調用;這里面容器和元素是相對的,對於bean而言,spring就是它的容器;對於spring而言,虛擬機就是他們容器;對於虛擬機而言,操作系統就是它的容器,這是個嵌套。實際上,JVM本身就提供對應的銷毀實例的鈎子,dubbo對spring的鈎子和JVM的鈎子都做了兼容處理,因為早期就存在non-spring類型的應用,而對於spring這兩個只需要調一個就夠了,關於優雅停機更深層次的內容,見Kirito大佬的文章:

https://www.cnkirito.moe/dubbo-gracefully-shutdown/

這里面在討論另外一個問題,什么時候spring的鈎子會被執行

程序正常退出
程序中使用System.exit()退出JVM
系統發生OutofMemory異常
使用kill pid干掉JVM進程的時候
注意,第四條,實際上殺進程常用的命令有兩個,kill -9 和kill -15,正常我們業務服務下線應該使用的是 kill -15,也就是kill sig,通知對應的進程結束,進程執行內置的清理函數,然后平穩結束;而kill -9直接從操作系統層面殺掉,直接清除占用的資源,很多的后置函數也沒有執行的,dubbo的DubboShutdownHook也同樣不會被執行。所以除非必要,一般不要使用kill -9來殺進程。

四.結論
       很慚愧,到現在為止,找了很多線索仍沒有找到問題的真正原因,在加上問題沒有復現,當時留着的證據並不多,很多線索查到一半就斷了,但其中遇到的很多思路讓我受益匪淺,這個分享出來,大家如果遇到類似的問題也可以參考

1.dubbo服務注銷之后,ZK的監聽器沒有注銷,具體的PR https://github.com/apache/dubbo/pull/1792/commits,在dubbo 2.6.2版本中存在,2.6.3之后修復,實際上筆者的版本也是2.6.2,但實際排查后發現不是這個問題。

2.dubbo的配置 dynamic默認是false, 導致創建的節點是永久節點,如果是kill - 9殺掉的,就不會主動在ZK上注銷,在dubbo 2.7.1版本中出現,之后修復,見issue  https://github.com/apache/dubbo/issues/3785

3.ZK的心跳的檢測,在上面可能的原因都被一一排除掉之后,最后的關注點落到了ZK的心跳檢測上,懷疑(或者很有可能)是ZK的服務端的心跳檢測出了問題,客戶端下線后,ZK仍不斷為其延續session 的存續時間,導致原有的臨時節點仍然存在,但這個涉及ZK更深層次的東西,其實和dubbo的關系就不是那么密切了,由於個人精力問題,就沒有在深入下去,有興趣的同學或者大佬有這方面經驗的歡迎隨時交流指正。

       最后的做法是,手動移除已經下線的節點,然后恢復正常,后續仍需觀察,看是否還有類似的情況發生,以及驗證升級ZK版本是否會有類型情況。雖然最后終極的原因沒有找到,不過在此過程中還是學到很多,如果有類似場景處理經驗的大佬歡迎隨時指教。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM