解決mongo的tickets被耗盡導(dǎo)致卡頓問(wèn)題
近一年來(lái),項(xiàng)目線上環(huán)境的mongo數(shù)據(jù)庫(kù)出現(xiàn)多次tickets被耗盡,導(dǎo)致數(shù)據(jù)庫(kù)卡頓,并且都是突然出現(xiàn),等待一段時(shí)間后又能自動(dòng)恢復(fù)。
為了解決這個(gè)問(wèn)題,我們進(jìn)行了長(zhǎng)期的探索和研究,先后從多個(gè)角度進(jìn)行優(yōu)化,于此記錄和分享一下這一路的歷程。
tickets是什么
為了解決這個(gè)問(wèn)題,我們首先要明白ticktes是什么,其實(shí)網(wǎng)上基本都說(shuō)的一知半解,沒(méi)有一個(gè)能說(shuō)明白的,但是有一個(gè)查詢tieckts消耗情況的mongo命令:
db.serverStatus().wiredTiger.concurrentTransactions
查詢結(jié)果:
{ "write" : { "out" : 0, "available" : 128, "totalTickets" : 128 }, "read" : { "out" : 1, "available" : 127, "totalTickets" : 128 } }
可以看到tickets分為讀寫(xiě)兩種,那ticktets到底是什么呢,我們根據(jù)這個(gè)查詢命令,其實(shí)大致可以猜測(cè)認(rèn)為是當(dāng)前同時(shí)存在的事務(wù)數(shù)量。
也就是mongo限制了同時(shí)進(jìn)行的事務(wù)數(shù)。
早期因?yàn)椴恢纓ickets到底是什么意思,嘗試過(guò)很多思路錯(cuò)誤的優(yōu)化,所以解決問(wèn)題,最好還是能弄明白問(wèn)題本身,才能對(duì)癥下藥。
思考?xì)v程
在眾多數(shù)據(jù)庫(kù)卡頓的經(jīng)歷中,曾有一次因?yàn)閞abbitmq導(dǎo)致的數(shù)據(jù)庫(kù)卡頓,原因是一小伙伴在請(qǐng)求的過(guò)濾層加了一個(gè)發(fā)送mq的邏輯,但是沒(méi)有進(jìn)行限制,導(dǎo)致每次只有有接口被調(diào),都會(huì)去發(fā)布一個(gè)mq消息,由于過(guò)高的并發(fā)導(dǎo)致rabbitmq不堪重負(fù),倒是讓人想不明到的是mq卡的同時(shí),數(shù)據(jù)庫(kù)也卡住了。
一開(kāi)始以為是因?yàn)橄⑦^(guò)多,導(dǎo)致消費(fèi)者瘋狂消費(fèi),壓垮了數(shù)據(jù)庫(kù),其實(shí)不存在這個(gè)問(wèn)題,因?yàn)槲覀兊膍q配置單個(gè)消費(fèi)者機(jī)器是串行的,也就是同一臺(tái)機(jī)器同一時(shí)間只會(huì)消費(fèi)同一個(gè)消息隊(duì)列的一條消息,所以并不會(huì)因?yàn)橄⒌亩嘟o數(shù)據(jù)庫(kù)帶來(lái)壓力,只會(huì)堆積在mq集群里。所以這次其實(shí)沒(méi)有找到mq卡頓導(dǎo)致mongo卡頓的原因。
我們接入的幾家第三方服務(wù),比如給我們提供IM消息服務(wù)的融云,每次他們出現(xiàn)問(wèn)題的時(shí)候,我們也會(huì)出現(xiàn)數(shù)據(jù)庫(kù)卡頓,并且每次時(shí)間出奇的一直,但也始終找不到原因。
起初經(jīng)過(guò)對(duì)他們調(diào)用我們接口情況進(jìn)行分析,發(fā)現(xiàn)每次他們出問(wèn)題時(shí),我們收到的請(qǐng)求會(huì)倍增,以為是這個(gè)原因?qū)е碌臄?shù)據(jù)庫(kù)壓力過(guò)大,并且我們基于redis和他們回調(diào)的流水號(hào)進(jìn)行了攔截,攔截方式如下:
- 當(dāng)請(qǐng)求過(guò)來(lái)時(shí)從redis中查詢?cè)摴P流水號(hào)狀態(tài),如果狀態(tài)為已完結(jié),則直接成功返回
- 如果查詢到狀態(tài)是進(jìn)行中,則拋異常給第三方,從而讓他繼續(xù)重試
- 如果查詢不到狀態(tài),則嘗試設(shè)置狀態(tài)為進(jìn)行中并設(shè)置10秒左右的過(guò)期時(shí)間,如果設(shè)置成功,則放到數(shù)據(jù)庫(kù)層面進(jìn)行數(shù)據(jù)處理;如果設(shè)置失敗,也拋異常給第三方,等待下次重試
- 等數(shù)據(jù)庫(kù)曾處理完成后,將redis中的流水號(hào)狀態(tài)改為已完結(jié)。
避免重復(fù)請(qǐng)求給我們帶來(lái)的數(shù)據(jù)庫(kù)的壓力。這其實(shí)也算是一部分原因但還是不算主要原因。
引起mongo卡頓的還有發(fā)布版本,有一段時(shí)間隔三差五發(fā)布版本,就會(huì)出現(xiàn)卡頓,但是查看更新的代碼也都是一些無(wú)關(guān)痛癢理論上不會(huì)引起問(wèn)題的內(nèi)容。
后來(lái)發(fā)現(xiàn)是發(fā)布版本時(shí)每次同時(shí)關(guān)閉和啟動(dòng)的機(jī)器從原來(lái)的一臺(tái)改成了兩臺(tái)(一臺(tái)一臺(tái)發(fā)布太慢,所以運(yùn)維改成了兩臺(tái)兩臺(tái)一起發(fā)),感覺(jué)原因應(yīng)該就在這里,后來(lái)想到會(huì)不會(huì)和優(yōu)雅關(guān)閉有關(guān),當(dāng)機(jī)器關(guān)閉時(shí)仍然有mq消費(fèi)者以及內(nèi)置循環(huán)腳本在執(zhí)行,當(dāng)進(jìn)程殺死時(shí),會(huì)產(chǎn)生大量需要立馬回滾的事務(wù),從而導(dǎo)致mongo卡頓。
后來(lái)經(jīng)過(guò)和運(yùn)維小伙伴的溝通發(fā)現(xiàn),在優(yōu)雅關(guān)閉方面確實(shí)存在問(wèn)題,他們關(guān)閉容器時(shí)會(huì)小容器內(nèi)的主進(jìn)程發(fā)一個(gè)容器即將關(guān)閉的信號(hào),然后等待幾十秒后,如果主進(jìn)程沒(méi)有自己關(guān)閉,則會(huì)直接殺死進(jìn)程。
為此我們需要在程序中實(shí)現(xiàn)對(duì)關(guān)閉信號(hào)的監(jiān)聽(tīng),并實(shí)現(xiàn)優(yōu)雅關(guān)閉的邏輯,在spring中,我們可以通過(guò)spring的時(shí)間拿到外部即將關(guān)閉的信號(hào):
@Volatile private var consumeSwitch = true /** * 銷(xiāo)毀邏輯 */ @EventListener fun close(event: ContextClosedEvent){ consumeSwitch = false logger.info("----------------------rabbitmq停止消費(fèi)----------------------") }
可以通過(guò)如上方式,對(duì)系統(tǒng)中的mq消費(fèi)者或者其他內(nèi)置程序進(jìn)行優(yōu)雅關(guān)??刂?,對(duì)優(yōu)雅關(guān)閉問(wèn)題優(yōu)化后,服務(wù)器關(guān)閉重啟導(dǎo)致的數(shù)據(jù)庫(kù)卡頓確實(shí)得到了有效解決。
上面的融云問(wèn)題優(yōu)化過(guò)后,后來(lái)融云再次卡頓的時(shí)候,還是會(huì)出現(xiàn)mongo卡頓,由此可見(jiàn),肯定和第三方有關(guān),但上面說(shuō)的問(wèn)題肯定不是主要原因。
后來(lái)我看到我們調(diào)用第三方的邏輯很多都在@Transactional代碼塊中間,后來(lái)去看了第三方sdk里的邏輯,其實(shí)就是封裝了一個(gè)http請(qǐng)求,但是http請(qǐng)求的請(qǐng)求超時(shí)時(shí)間長(zhǎng)達(dá)60秒,那就會(huì)有一個(gè)問(wèn)題,如果這個(gè)時(shí)候第三方服務(wù)器卡頓了,這個(gè)請(qǐng)求就會(huì)不斷地等,知道60s超時(shí),而由于這個(gè)操作是在事務(wù)塊中,意味著這個(gè)事務(wù)也不會(huì)commit掉,那等于這個(gè)事務(wù)所占用的tickets也一直不會(huì)放掉,至此根本原因似乎找到了,是因?yàn)槭聞?wù)本身被卡住了,導(dǎo)致tickets耗盡,從而后面新的事務(wù)全部都在等待狀態(tài),全部都卡住了。
其實(shí)這次找的原因,同樣也可以解釋前面mq卡頓導(dǎo)致的數(shù)據(jù)庫(kù)卡頓,因?yàn)橥瑯佑写罅康陌l(fā)送mq的操作在事務(wù)塊中,因?yàn)槎虝r(shí)間瘋狂發(fā)mq,導(dǎo)致mq服務(wù)端卡頓,從而導(dǎo)致發(fā)mq的操作出現(xiàn)卡頓,這就會(huì)出現(xiàn)整個(gè)事務(wù)被卡住,接著tickets被消耗殆盡,整個(gè)數(shù)據(jù)庫(kù)卡頓。
找到確定問(wèn)題后就好對(duì)癥下藥了,第三方的問(wèn)題由于我們不能保證第三方的穩(wěn)定性,所以當(dāng)?shù)谌匠霈F(xiàn)問(wèn)題時(shí)的思路應(yīng)該是進(jìn)行服務(wù)降級(jí),允許部分功能不可用,確定核心業(yè)務(wù)不受影響,我們基于java線程池進(jìn)行了同步改異步處理,并且由于第三方的工作是給用戶推送im消息,所以配置的舍棄策略是當(dāng)阻塞隊(duì)列堆積滿之后,將最老的進(jìn)行丟棄。
而如果是mq導(dǎo)致的這種情況,我們這邊沒(méi)有進(jìn)行額外的處理,因?yàn)檫@種情況是有自身的bug導(dǎo)致的,這需要做好整理分享工作,避免再次出現(xiàn)這樣的bug。
//自己實(shí)現(xiàn)的runnable abstract class RongCloudRunnable( private val taskDesc: String, private val params: Map<String, Any?> ) : Runnable { override fun toString(): String { return "任務(wù)名稱(chēng):${taskDesc};任務(wù)參數(shù):${params}" } } //構(gòu)建線程池 private val rongCloudThreadPool = ThreadPoolExecutor( externalProps.rongCloud.threadPoolCoreCnt, externalProps.rongCloud.threadPoolMaxCnt, 5, TimeUnit.MINUTES, LinkedBlockingQueue<Runnable>(externalProps.rongCloud.threadPoolQueueLength), RejectedExecutionHandler { r, executor -> if (!executor.isShutdown) { val item = executor.queue.poll() logger.warn("當(dāng)前融云阻塞任務(wù)過(guò)多,舍棄最老的任務(wù):${item}") executor.execute(r) } } ) //封裝線程池任務(wù)處理方法 fun taskExecute(taskDesc: String, params: Map<String,Any?>, handle: ()-> Unit){ rongCloudThreadPool.execute(object :RongCloudRunnable(taskDesc, params){ override fun run() { handle() } }) } //具體使用 taskExecute("發(fā)送消息", mapOf( "from_id" to fromId, "target_ids" to targetIds, "data" to data, "is_include_sender" to isIncludeSender )){ sendMessage(BatchSendData(fromId, targetIds, data, isIncludeSender)) }
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
mongodb中隨機(jī)獲取1條記錄的實(shí)現(xiàn)方法
這篇文章運(yùn)用實(shí)例給大家演示了如何在mongodb中隨機(jī)獲取1條記錄,文中介紹的很詳細(xì),有需要的朋友們可以參考借鑒。下面來(lái)一起看看吧。2016-09-09MongoDB數(shù)據(jù)庫(kù)條件查詢技巧總結(jié)
查詢是數(shù)據(jù)庫(kù)的基本操作之一,下面這篇文章主要給大家介紹了關(guān)于MongoDB數(shù)據(jù)庫(kù)條件查詢技巧的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06MongoDB中數(shù)據(jù)的替換方法實(shí)現(xiàn)類(lèi)Replace()函數(shù)功能詳解
這篇文章主要介紹了MongoDB中數(shù)據(jù)的替換方法實(shí)現(xiàn)類(lèi)Replace()函數(shù)功能詳解,需要的朋友可以參考下2020-02-02MongoDB添加仲裁節(jié)點(diǎn)報(bào)錯(cuò):replica set IDs do not match的解決方法
這篇文章主要給大家介紹了關(guān)于MongoDB添加仲裁節(jié)點(diǎn)報(bào)錯(cuò):replica set IDs do not match的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11