MQ消息積壓,把我整吐血了
前言
我之前在一家餐飲公司待過(guò)兩年,每天中午和晚上用餐高峰期,系統(tǒng)的并發(fā)量不容小覷。為了保險(xiǎn)起見(jiàn),公司規(guī)定各部門都要在吃飯的時(shí)間輪流值班,防止出現(xiàn)線上問(wèn)題時(shí)能夠及時(shí)處理。
我當(dāng)時(shí)在后廚顯示系統(tǒng)團(tuán)隊(duì),該系統(tǒng)屬于訂單的下游業(yè)務(wù)。
用戶點(diǎn)完菜下單后,訂單系統(tǒng)會(huì)通過(guò)發(fā)kafka消息給我們系統(tǒng),系統(tǒng)讀取消息后,做業(yè)務(wù)邏輯處理,持久化訂單和菜品數(shù)據(jù),然后展示到劃菜客戶端。
這樣廚師就知道哪個(gè)訂單要做哪些菜,有些菜做好了,就可以通過(guò)該系統(tǒng)出菜。系統(tǒng)自動(dòng)通知服務(wù)員上菜,如果服務(wù)員上完菜,修改菜品上菜狀態(tài),用戶就知道哪些菜已經(jīng)上了,哪些還沒(méi)有上。這個(gè)系統(tǒng)可以大大提高后廚到用戶的效率。
圖片
這一切的關(guān)鍵是消息中間件:kafka,如果它出現(xiàn)問(wèn)題,將會(huì)直接影響到后廚顯示系統(tǒng)的用戶功能使用。
這篇文章跟大家一起聊聊,我們當(dāng)時(shí)出現(xiàn)過(guò)的消息積壓?jiǎn)栴},希望對(duì)你會(huì)有所幫助。
1 第一次消息積壓
剛開(kāi)始我們的用戶量比較少,上線一段時(shí)間,mq的消息通信都沒(méi)啥問(wèn)題。
隨著用戶量逐步增多,每個(gè)商家每天都會(huì)產(chǎn)生大量的訂單數(shù)據(jù),每個(gè)訂單都有多個(gè)菜品,這樣導(dǎo)致我們劃菜系統(tǒng)的劃菜表的數(shù)據(jù)越來(lái)越多。
在某一天中午,收到商家投訴說(shuō)用戶下單之后,在平板上出現(xiàn)的菜品列表有延遲。
廚房幾分鐘之后才能看到菜品。
我們馬上開(kāi)始查原因。
出現(xiàn)這種菜品延遲的問(wèn)題,必定跟kafka有關(guān),因此,我們先查看kafka。
果然出現(xiàn)了消息積壓。
通常情況下,出現(xiàn)消息積壓的原因有:
- mq消費(fèi)者掛了。
- mq生產(chǎn)者生產(chǎn)消息的速度,大于mq消費(fèi)者消費(fèi)消息的速度。
我查了一下監(jiān)控,發(fā)現(xiàn)我們的mq消費(fèi)者,服務(wù)在正常運(yùn)行,沒(méi)有異常。
剩下的原因可能是:mq消費(fèi)者消費(fèi)消息的速度變慢了。
接下來(lái),我查了一下劃菜表,目前不太多只有幾十萬(wàn)的數(shù)據(jù)。
看來(lái)需要優(yōu)化mq消費(fèi)者的處理邏輯了。
我在代碼中增加了一些日志,把mq消息者中各個(gè)關(guān)鍵節(jié)點(diǎn)的耗時(shí)都打印出來(lái)了。
發(fā)現(xiàn)有兩個(gè)地方耗時(shí)比較長(zhǎng):
- 有個(gè)代碼是一個(gè)for循環(huán)中,一個(gè)個(gè)查詢數(shù)據(jù)庫(kù)處理數(shù)據(jù)的。
- 有個(gè)多條件查詢數(shù)據(jù)的代碼。
于是,我做了有針對(duì)性的優(yōu)化。
將在for循環(huán)中一個(gè)個(gè)查詢數(shù)據(jù)庫(kù)的代碼,改成通過(guò)參數(shù)集合,批量查詢數(shù)據(jù)。
有時(shí)候,我們需要從指定的用戶集合中,查詢出有哪些是在數(shù)據(jù)庫(kù)中已經(jīng)存在的。
實(shí)現(xiàn)代碼可以這樣寫(xiě):
public List<User> queryUser(List<User> searchList) {
if (CollectionUtils.isEmpty(searchList)) {
return Collections.emptyList();
}
List<User> result = Lists.newArrayList();
searchList.forEach(user -> result.add(userMapper.getUserById(user.getId())));
return result;
}
這里如果有50個(gè)用戶,則需要循環(huán)50次,去查詢數(shù)據(jù)庫(kù)。我們都知道,每查詢一次數(shù)據(jù)庫(kù),就是一次遠(yuǎn)程調(diào)用。
如果查詢50次數(shù)據(jù)庫(kù),就有50次遠(yuǎn)程調(diào)用,這是非常耗時(shí)的操作。
那么,我們?nèi)绾蝺?yōu)化呢?
具體代碼如下:
public List<User> queryUser(List<User> searchList) {
if (CollectionUtils.isEmpty(searchList)) {
return Collections.emptyList();
}
List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList());
return userMapper.getUserByIds(ids);
}
提供一個(gè)根據(jù)用戶id集合批量查詢用戶的接口,只遠(yuǎn)程調(diào)用一次,就能查詢出所有的數(shù)據(jù)。
多條件查詢數(shù)據(jù)的地方,增加了一個(gè)聯(lián)合索引,解決了問(wèn)題。
這樣優(yōu)化之后, mq消費(fèi)者處理消息的速度提升了很多,消息積壓?jiǎn)栴}被解決了。
2 第二次消息積壓
沒(méi)想到,過(guò)了幾個(gè)月之后,又開(kāi)始出現(xiàn)消息積壓的問(wèn)題了。
但這次是偶爾會(huì)積壓,大部分情況不會(huì)。
這幾天消息的積壓時(shí)間不長(zhǎng),對(duì)用戶影響比較小,沒(méi)有引起商家的投訴。
我查了一下劃菜表的數(shù)據(jù)只有幾百萬(wàn)。
但通過(guò)一些監(jiān)控,和DBA每天發(fā)的慢查詢郵件,自己發(fā)現(xiàn)了異常。
我發(fā)現(xiàn)有些sql語(yǔ)句,執(zhí)行的where條件是一模一樣的,只有條件后面的參數(shù)值不一樣,導(dǎo)致該sql語(yǔ)句走的索引不一樣。
比如:order_id=123走了索引a,而order_id=124走了索引b。
有張表查詢的場(chǎng)景有很多,當(dāng)時(shí)為了滿足不同業(yè)務(wù)場(chǎng)景,加了多個(gè)聯(lián)合索引。
MySQL會(huì)根據(jù)下面幾個(gè)因素選擇索引:
- 通過(guò)采樣數(shù)據(jù)來(lái)估算需要掃描的行數(shù),如果掃描的行數(shù)多那可能io次數(shù)會(huì)更多,對(duì)cpu的消耗也更大。
- 是否會(huì)使用臨時(shí)表,如果使用臨時(shí)表也會(huì)影響查詢速度;
- 是否需要排序,如果需要排序則也會(huì)影響查詢速度。
綜合1、2、3以及其它的一些因素,MySql優(yōu)化器會(huì)選出它自己認(rèn)為最合適的索引。
MySQL優(yōu)化器是通過(guò)采樣來(lái)預(yù)估要掃描的行數(shù)的,所謂采樣就是選擇一些數(shù)據(jù)頁(yè)來(lái)進(jìn)行統(tǒng)計(jì)預(yù)估,這個(gè)會(huì)有一定的誤差。
由于MVCC會(huì)有多個(gè)版本的數(shù)據(jù)頁(yè),比如刪除一些數(shù)據(jù),但是這些數(shù)據(jù)由于還在其它的事務(wù)中可能會(huì)被看到,索引不是真正的刪除,這種情況也會(huì)導(dǎo)致統(tǒng)計(jì)不準(zhǔn)確,從而影響優(yōu)化器的判斷。
上面這兩個(gè)原因?qū)е翸ySQL在執(zhí)行SQL語(yǔ)句時(shí),會(huì)選錯(cuò)索引。
明明使用索引a的時(shí)候,執(zhí)行效率更高,但實(shí)際情況卻使用了索引b。
為了解決MySQL選錯(cuò)索引的問(wèn)題,我們使用了關(guān)鍵字force index,來(lái)強(qiáng)制查詢sql走索引a。
這樣優(yōu)化之后,這次小范圍的消息積壓?jiǎn)栴}被解決了。
3 第三次消息積壓
過(guò)了半年之后,在某個(gè)晚上6點(diǎn)多鐘。
有幾個(gè)商家投訴過(guò)來(lái),說(shuō)劃菜系統(tǒng)有延遲,下單之后,幾分鐘才能看到菜品。
我查看了一下監(jiān)控,發(fā)現(xiàn)kafka消息又出現(xiàn)了積壓的情況。
查了一下MySQL的索引,該走的索引都走了,但數(shù)據(jù)查詢還是有些慢。
此時(shí),我再次查了一下劃菜表,驚奇的發(fā)現(xiàn),短短半年表中有3千萬(wàn)的數(shù)據(jù)了。
通常情況下,單表的數(shù)據(jù)太多,無(wú)論是查詢,還是寫(xiě)入的性能,都會(huì)下降。
這次出現(xiàn)查詢慢的原因是數(shù)據(jù)太多了。
為了解決這個(gè)問(wèn)題,我們必須:
- 做分庫(kù)分表
- 將歷史數(shù)據(jù)備份
由于現(xiàn)階段做分庫(kù)分表的代價(jià)太大了,我們的商戶數(shù)量還沒(méi)有走到這一步。
因此,我們當(dāng)時(shí)果斷選擇了將歷史數(shù)據(jù)做備份的方案。
當(dāng)時(shí)我跟產(chǎn)品和DBA討論了一下,劃菜表只保留最近30天的數(shù)據(jù),超過(guò)幾天的數(shù)據(jù)寫(xiě)入到歷史表中。
這樣優(yōu)化之后,劃菜表30天只會(huì)產(chǎn)生幾百萬(wàn)的數(shù)據(jù),對(duì)性能影響不大。
消息積壓的問(wèn)題被解決了。
4 第四次消息積壓
通過(guò)上面這幾次優(yōu)化之后,很長(zhǎng)一段時(shí)間,系統(tǒng)都沒(méi)有出現(xiàn)消息積壓的問(wèn)題。
但在一年之后的某一天下午,又有一些商家投訴過(guò)來(lái)了。
此時(shí),我查看公司郵箱,發(fā)現(xiàn)kafka消息積壓的監(jiān)控報(bào)警郵件一大堆。
但由于剛剛一直在開(kāi)會(huì),沒(méi)有看到。
這次的時(shí)間點(diǎn)就有些特殊。
一般情況下,并發(fā)量大的時(shí)候,是中午或者晚上的用餐高峰期,而這次出現(xiàn)消息積壓?jiǎn)栴}的時(shí)間是下午。
這就有點(diǎn)奇怪了。
剛開(kāi)始查詢這個(gè)問(wèn)題一點(diǎn)頭緒都沒(méi)有。
我問(wèn)了一下訂單組的同事,下午有沒(méi)有發(fā)版,或者執(zhí)行什么功能?
因?yàn)槲覀兊膭澆讼到y(tǒng),是他們的下游系統(tǒng),跟他們有直接的關(guān)系。
某位同事說(shuō),他們半小時(shí)之前,執(zhí)行了一個(gè)批量修改訂單狀態(tài)的job,一次性修改了幾萬(wàn)個(gè)訂單的狀態(tài)。
而修改了訂單狀態(tài),會(huì)自動(dòng)發(fā)送mq消息。
這樣導(dǎo)致,他們的程序在極短的時(shí)間內(nèi),產(chǎn)生了大量的mq消息。
而我們的mq消費(fèi)者根本無(wú)法處理這些消息,所以才會(huì)產(chǎn)生消息積壓的問(wèn)題。
我們當(dāng)時(shí)一起查了kafka消息的積壓情況,發(fā)現(xiàn)當(dāng)時(shí)積壓了幾十萬(wàn)條消息。
要想快速提升mq消費(fèi)者的處理速度,我們當(dāng)時(shí)想到了兩個(gè)方案:
- 增加partion數(shù)量。
- 使用線程池處理消息。
但考慮到,當(dāng)時(shí)消息已經(jīng)積壓到幾個(gè)已有的partion中了,再新增partion意義不大。
于是,我們只能改造代碼,使用線程池處理消息了。
為了開(kāi)始消費(fèi)積壓的消息,我們將線程池的核心線程和最大線程數(shù)量調(diào)大到了50。
這兩個(gè)參數(shù)是可以動(dòng)態(tài)配置的。
這樣調(diào)整之后,積壓了幾十萬(wàn)的mq消息,在20分鐘左右被消費(fèi)完了。
這次突然產(chǎn)生的消息積壓?jiǎn)栴}被解決了。
解決完這次的問(wèn)題之后,我們還是保留的線程池消費(fèi)消息的邏輯,將核心線程數(shù)調(diào)到8,最大線程數(shù)調(diào)到10。
當(dāng)后面出現(xiàn)消息積壓?jiǎn)栴},可以及時(shí)通過(guò)調(diào)整線程數(shù)量,先臨時(shí)解決問(wèn)題,而不會(huì)對(duì)用戶造成太大的影響。
注意:使用線程池消費(fèi)mq消息不是萬(wàn)能的。該方案也有一些弊端,它有消息順序的問(wèn)題,也可能會(huì)導(dǎo)致服務(wù)器的CPU使用率飆升。此外,如果在多線程中調(diào)用了第三方接口,可能會(huì)導(dǎo)致該第三方接口的壓力太大,而直接掛掉。
總之,MQ的消息積壓?jiǎn)栴},不是一個(gè)簡(jiǎn)單的問(wèn)題。
雖說(shuō)產(chǎn)生的根本原因是:MQ生產(chǎn)者生產(chǎn)消息的速度,大于MQ消費(fèi)者消費(fèi)消息的速度,但產(chǎn)生的具體原因有多種。
我們?cè)趯?shí)際工作中,需要針對(duì)不同的業(yè)務(wù)場(chǎng)景,做不同的優(yōu)化。
我們需要對(duì)MQ隊(duì)列中的消息積壓情況,進(jìn)行監(jiān)控和預(yù)警,至少能夠及時(shí)發(fā)現(xiàn)問(wèn)題。
沒(méi)有最好的方案,只有最合適當(dāng)前業(yè)務(wù)場(chǎng)景的方案。