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