Rocketmq優(yōu)雅停機(jī)往事
本文轉(zhuǎn)載自微信公眾號(hào)「捉蟲(chóng)大師」,作者捉蟲(chóng)大師。轉(zhuǎn)載本文請(qǐng)聯(lián)系捉蟲(chóng)大師公眾號(hào)。
1
時(shí)間追溯到2018年12月的某一天夜晚,那天我正準(zhǔn)備上線(xiàn)一個(gè)需求完就回家,剛點(diǎn)下發(fā)布按鈕,告警就響起,我擦,難道回不了家了?看著報(bào)錯(cuò)量只有一兩個(gè),斷定只是偶發(fā),穩(wěn)住不要慌。
把剩下的機(jī)器發(fā)完,又出現(xiàn)了幾個(gè)同樣的錯(cuò)誤,作為一名優(yōu)(咸)秀(魚(yú))程序員,這種問(wèn)題必須追查到底。
2
嫻熟地查詢(xún)到報(bào)錯(cuò)日志
- org.apache.ibatis.exceptions.PersistenceException: ### Error querying database. Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is com.alibaba.druid.pool.DataSourceClosedException: dataSource already closed
看著異常信息,陷入了沉思
表面上看報(bào)錯(cuò)是因?yàn)槭褂昧艘呀?jīng)關(guān)閉的數(shù)據(jù)源
數(shù)據(jù)源什么時(shí)候會(huì)關(guān)閉呢?只有進(jìn)程被殺死的時(shí)候
莫非是應(yīng)用關(guān)閉時(shí)不夠平滑?發(fā)布時(shí)會(huì)先摘除流量的呀,應(yīng)該不至于呀
天色已經(jīng)很晚,漫無(wú)目的地拖動(dòng)日志,疲憊地尋找新線(xiàn)索,突然報(bào)錯(cuò)日志中一個(gè)單詞引入眼簾:「rocketmq」
精神抖擻,大概知道原因了,這應(yīng)用中還有個(gè)兢兢業(yè)業(yè)的rocketmq consumer一直在消費(fèi)消息,在應(yīng)用關(guān)閉時(shí),外部流量被摘除了,但沒(méi)人通知rocketmq consumer,于是它拋異常了。
3
出于我對(duì)rocketmq不深刻甚至有點(diǎn)膚淺的理解,它的消費(fèi)采用ack的方式,如果報(bào)錯(cuò),消息稍后還會(huì)重試,不會(huì)丟消息,而且如果消費(fèi)代碼是冪等的,也不會(huì)有業(yè)務(wù)上的異常,總之這不重要,因?yàn)樗膊皇俏覍?xiě)的代碼。
瞅了一眼consumer的代碼(這里就不貼代碼了,反正貼了你也不會(huì)看),consumer注冊(cè)了一個(gè)ShutdownHook,ShutdownHook里consumer執(zhí)行了shutdown來(lái)優(yōu)雅地退出,并且給這個(gè)shutdownThread設(shè)置了最高優(yōu)先級(jí),然而從實(shí)踐看來(lái),這個(gè)線(xiàn)程最高優(yōu)先級(jí)并沒(méi)有什么卵用。
而且從《ShutdownHook原理》這篇文章中也知道ShutdownHook是并發(fā)執(zhí)行的,spring容器關(guān)閉也是一個(gè)ShutdownHook,他們之前沒(méi)有先后順序。
了解原因后,第一時(shí)間想到了類(lèi)似dubbo摘流的方案,吭哧吭哧寫(xiě)了個(gè)優(yōu)雅關(guān)閉rocketmq cosnumer的接口,在應(yīng)用關(guān)閉腳本的kill之前調(diào)用該接口,完美解決問(wèn)題,趕緊下班回家,不然要猝死了。
4
夜里入睡,夢(mèng)到老板讓我把所有的系統(tǒng)都改造掉,嚇得我一機(jī)靈。
于是第二天又重新思考這個(gè)問(wèn)題,總覺(jué)得在應(yīng)用里實(shí)現(xiàn)一個(gè)接口并在stop腳本中去調(diào)用是一件非常不優(yōu)雅的事,更重要的是這也沒(méi)法復(fù)制到其他項(xiàng)目,我又陷入了沉思。
既然是spring容器關(guān)閉時(shí)bean的銷(xiāo)毀順序?qū)е碌膯?wèn)題,那么能不能利用spring的depend-on把順序理順了?說(shuō)干就干。
起初我遇到是這樣的依賴(lài)關(guān)系:
手把手在xml的每個(gè)bean中把depend-on關(guān)系都配上,似乎也起到了作用。
但當(dāng)我打開(kāi)第二個(gè)項(xiàng)目時(shí),它的bean之間的依賴(lài)關(guān)系大致如下:
好家伙,26個(gè)字母差點(diǎn)不夠用,當(dāng)時(shí)我的心情是這樣的
所以我覺(jué)得以當(dāng)前的速度,改造完所有項(xiàng)目可能都到9102年了。
5
又過(guò)了一段時(shí)間,在github交友網(wǎng)站上突然看到了rocketmq官方實(shí)現(xiàn)的spring-boot-starter,于是點(diǎn)進(jìn)去看了它的實(shí)現(xiàn)。好家伙,看完直呼666。
官方starter實(shí)現(xiàn)了spring的SmartLifecycle接口,它的start方法能在所有bean初始化完成后被調(diào)用,stop方法會(huì)在bean被銷(xiāo)毀前調(diào)用,對(duì)rocketmq consumer來(lái)說(shuō)簡(jiǎn)直完美。
順便還復(fù)習(xí)了一下spring容器的關(guān)閉,代碼在AbstractApplicationContext的doClose方法,這里我總結(jié)成一幅圖:
通過(guò)上圖能看到,銷(xiāo)毀bean之前,有關(guān)閉lifecycle bean和發(fā)送ContextClosedEvent兩個(gè)動(dòng)作,官方starter選擇了實(shí)現(xiàn)LifeCycle接口的方式。
6
到這里我該給老板匯報(bào)去了,之所以rocketmq consumer發(fā)布時(shí)不平滑是我們的使用姿勢(shì)問(wèn)題,雖然對(duì)業(yè)務(wù)沒(méi)影響,但不優(yōu)雅,解決方案有兩個(gè),老板你選吧:
- 全都換成官方starter,依賴(lài)spring-boot,官方維護(hù),改造成本很高,
- 監(jiān)聽(tīng)ContextClosedEvent來(lái)實(shí)現(xiàn)優(yōu)雅關(guān)閉,這塊可以封裝一下,讓業(yè)務(wù)方引入依賴(lài)即可


































