自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

救命!Java Web項(xiàng)目中的MQ消息堆積讓我抓狂

開(kāi)發(fā) 前端
使用線(xiàn)程池消費(fèi) MQ 消息并不是一個(gè)通用的解決方案。它存在一些缺點(diǎn),比如可能會(huì)導(dǎo)致消息順序性問(wèn)題,以及服務(wù)器 CPU 使用率的飆升風(fēng)險(xiǎn)。此外,如果在多線(xiàn)程環(huán)境中調(diào)用第三方接口,可能會(huì)造成第三方服務(wù)的超負(fù)荷甚至崩潰。

我之前參與開(kāi)發(fā)了一個(gè)餐廳系統(tǒng),該系統(tǒng)在午餐和晚餐高峰時(shí)段面臨巨大的并發(fā)需求。

為了確保系統(tǒng)順暢運(yùn)行,公司要求各個(gè)部門(mén)在用餐時(shí)間輪流值班,以便及時(shí)解決在線(xiàn)問(wèn)題。

我所在的團(tuán)隊(duì)負(fù)責(zé)廚房展示系統(tǒng),這是訂單系統(tǒng)的下游服務(wù)。

當(dāng)用戶(hù)下單后,訂單系統(tǒng)會(huì)向我們的系統(tǒng)發(fā)送一條 Kafka 消息。我們的系統(tǒng)讀取消息,處理業(yè)務(wù)邏輯,保存訂單和菜品數(shù)據(jù),然后在菜品管理客戶(hù)端上顯示出來(lái)。

通過(guò)這種方式,廚師們可以了解每個(gè)訂單所需的菜品,一旦有菜品準(zhǔn)備好,系統(tǒng)會(huì)通知服務(wù)員上菜。

上菜后,服務(wù)員會(huì)更新菜品狀態(tài),這樣用戶(hù)就能知道哪些菜已上,哪些菜還在準(zhǔn)備中。

這個(gè)系統(tǒng)極大地提高了從廚房到顧客的效率。

圖片圖片

這其中的關(guān)鍵是消息中間件 Kafka。如果它出現(xiàn)問(wèn)題,將直接影響廚房展示系統(tǒng)的正常運(yùn)作。

在本文中,我將分享我們?cè)谔幚怼跋⒎e壓?jiǎn)栴}”時(shí)的經(jīng)驗(yàn),希望能對(duì)你有所幫助。

初次遇到消息積壓?jiǎn)栴}

最初,我們的用戶(hù)量較小,系統(tǒng)上線(xiàn)后的一段時(shí)間里,消息隊(duì)列(MQ)的消息通信非常順暢。

隨著用戶(hù)量的增長(zhǎng),每個(gè)商戶(hù)每天都生成大量訂單數(shù)據(jù),每個(gè)訂單包含多個(gè)菜品。這導(dǎo)致我們菜品管理系統(tǒng)的數(shù)據(jù)量顯著增加。

某天下午,我們收到了商戶(hù)的投訴,用戶(hù)下單后,菜品列表在平板上出現(xiàn)延遲。

廚房只有在幾分鐘后才能看到這些菜品。

我們立即開(kāi)始調(diào)查原因。

菜品展示延遲問(wèn)題通常與 Kafka 有關(guān),因此我們首先檢查了 Kafka。

果不其然,存在 消息積壓。

消息積壓的常見(jiàn)原因有以下兩種:

  1. MQ 消費(fèi)者服務(wù)宕機(jī)。
  2. MQ 生產(chǎn)者產(chǎn)生消息的速率超過(guò) MQ 消費(fèi)者消費(fèi)消息的速率。

我檢查了監(jiān)控系統(tǒng),發(fā)現(xiàn) MQ 消費(fèi)者服務(wù)運(yùn)行正常,沒(méi)有異常。

剩下的原因可能是 MQ 消費(fèi)者的消息處理速度變慢了。

接下來(lái),我檢查了菜品管理表,發(fā)現(xiàn)只有幾十萬(wàn)條記錄。

似乎有必要優(yōu)化 MQ 消費(fèi)者的處理邏輯。

我在代碼中添加了一些日志,打印出 MQ 消費(fèi)者中各個(gè)關(guān)鍵節(jié)點(diǎn)的耗時(shí)。

發(fā)現(xiàn)兩處存在明顯的延遲:

  1. 在 for 循環(huán)中逐條查詢(xún)數(shù)據(jù)庫(kù)的代碼。
  2. 進(jìn)行多條件數(shù)據(jù)查詢(xún)的代碼。

我針對(duì)性地進(jìn)行了優(yōu)化:

對(duì)于 for 循環(huán)逐條查詢(xún)數(shù)據(jù)庫(kù)的代碼,我改成了使用參數(shù)集合的 批量查詢(xún)。

有時(shí),我們需要查詢(xún)指定集合中的哪些用戶(hù)已經(jīng)存在于數(shù)據(jù)庫(kù)中。實(shí)現(xiàn)方式如下:

publicList<User>queryUser(List<User> searchList){
if(CollectionUtils.isEmpty(searchList)){
returnCollections.emptyList();
}

List<User> result = Lists.newArrayList();
    searchList.forEach(user -> result.add(userMapper.getUserById(user.getId())));
return result;
}

如果有 50 個(gè)用戶(hù),這種方法需要查詢(xún)數(shù)據(jù)庫(kù) 50 次。眾所周知,每次數(shù)據(jù)庫(kù)查詢(xún)都是一次遠(yuǎn)程調(diào)用。

查詢(xún)數(shù)據(jù)庫(kù) 50 次意味著需要進(jìn)行 50 次遠(yuǎn)程調(diào)用,耗時(shí)非常長(zhǎng)。

那么,如何優(yōu)化呢?

優(yōu)化后的代碼如下:

publicList<User>queryUser(List<User> searchList){
if(CollectionUtils.isEmpty(searchList)){
returnCollections.emptyList();
}
List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList());
return userMapper.getUserByIds(ids);
}

這種方式提供了一個(gè)基于用戶(hù) ID 集合進(jìn)行批量查詢(xún)的接口,僅需一次遠(yuǎn)程調(diào)用就可以獲取所有數(shù)據(jù)。

對(duì)于多條件數(shù)據(jù)查詢(xún),我添加了一個(gè) 組合索引,解決了這一問(wèn)題。

經(jīng)過(guò)這些優(yōu)化后,MQ 消費(fèi)者的消息處理速度顯著提升,成功解決了消息積壓?jiǎn)栴}。

再次遇到消息積壓?jiǎn)栴}

幾個(gè)月后,我們?cè)俅斡龅搅讼⒎e壓?jiǎn)栴}。

這次問(wèn)題是偶發(fā)性的,只在某些時(shí)候出現(xiàn),大部分時(shí)間沒(méi)有問(wèn)題。

積壓持續(xù)時(shí)間很短,對(duì)用戶(hù)的影響較小,商戶(hù)也沒(méi)有投訴。

我檢查了菜品管理表,此時(shí)已有幾百萬(wàn)條記錄。

通過(guò)監(jiān)控和 DBA 提供的每日慢查詢(xún)郵件,我發(fā)現(xiàn)了一些異常。

我注意到有些 SQL 語(yǔ)句的 WHERE 條件相同,但僅參數(shù)值不同,卻使用了不同的索引。

例如,order_id=123 使用索引 a,而 order_id=124 使用索引 b。

該表存在多種查詢(xún)場(chǎng)景,為滿(mǎn)足不同的業(yè)務(wù)需求,添加了多個(gè)組合索引。

MySQL 根據(jù)以下幾種因素來(lái)選擇索引:

  1. 通過(guò)數(shù)據(jù)采樣估算掃描的行數(shù)。掃描的行數(shù)越多,I/O 操作和 CPU 使用率越高。
  2. 是否使用臨時(shí)表,臨時(shí)表會(huì)影響查詢(xún)速度。
  3. 是否需要排序,排序也會(huì)影響查詢(xún)速度。

綜合這些因素,MySQL 優(yōu)化器選擇其認(rèn)為最合適的索引。

MySQL 優(yōu)化器通過(guò)采樣估算掃描的行數(shù),這涉及從數(shù)據(jù)頁(yè)中選擇一些進(jìn)行統(tǒng)計(jì)估算,這種方法會(huì)帶來(lái)一定的誤差。

由于 MVCC 機(jī)制,數(shù)據(jù)頁(yè)存在多個(gè)版本,例如被刪除的數(shù)據(jù)在其他事務(wù)中仍然可見(jiàn),因此索引并未真正刪除。這可能導(dǎo)致統(tǒng)計(jì)數(shù)據(jù)不準(zhǔn)確,影響優(yōu)化器的決策。

這些因素可能導(dǎo)致 MySQL 在執(zhí)行 SQL 語(yǔ)句時(shí) 選擇了錯(cuò)誤的索引。

即便使用索引 a 更為高效,MySQL 也可能使用了索引 b。

為了解決 MySQL 選擇錯(cuò)誤索引的問(wèn)題,我們使用了 FORCE INDEX 關(guān)鍵字,強(qiáng)制 SQL 查詢(xún)使用索引 a。

經(jīng)過(guò)這一優(yōu)化,消息的輕微積壓?jiǎn)栴}也得到了解決。

第三次遭遇消息積壓

六個(gè)月后,某天晚上大約六點(diǎn),幾位商家投訴菜品管理系統(tǒng)出現(xiàn)延遲。

他們反饋說(shuō)下單后菜品要過(guò)幾分鐘才會(huì)顯示。

檢查監(jiān)控系統(tǒng)后,我發(fā)現(xiàn) Kafka 消息再次堆積了。

我復(fù)查了 MySQL 的索引,發(fā)現(xiàn)索引是正確的,但數(shù)據(jù)查詢(xún)依然很慢。

接著查看了菜品管理表,發(fā)現(xiàn)表中的數(shù)據(jù)量在短短六個(gè)月內(nèi)竟然增長(zhǎng)到了 3000 萬(wàn)條記錄。

通常情況下,當(dāng)單表數(shù)據(jù)過(guò)多時(shí),查詢(xún)和寫(xiě)入性能都會(huì)下降。

這次查詢(xún)變慢的原因就是因?yàn)閿?shù)據(jù)量過(guò)于龐大。

為了解決這個(gè)問(wèn)題,我們需要:

  1. 實(shí)施數(shù)據(jù)庫(kù)和表的分區(qū)
  2. 備份歷史數(shù)據(jù)

在當(dāng)時(shí),實(shí)施數(shù)據(jù)庫(kù)和表分區(qū)的成本太高,而且商家數(shù)量尚不足以支持這樣的解決方案。

因此,我們決定備份歷史數(shù)據(jù)。

在與產(chǎn)品經(jīng)理和 DBA 討論后,我們決定菜品管理表僅保留最近 30 天的數(shù)據(jù),超過(guò) 30 天的數(shù)據(jù)會(huì)被移動(dòng)到一個(gè)“歷史表”中。

經(jīng)過(guò)這個(gè)優(yōu)化后,菜品管理表在 30 天內(nèi)只積累了幾百萬(wàn)條數(shù)據(jù),對(duì)性能的影響較小。

這樣一來(lái),消息堆積問(wèn)題得以解決。

第四次遇到消息堆積問(wèn)題

在上述優(yōu)化之后,系統(tǒng)長(zhǎng)時(shí)間內(nèi)運(yùn)行順利,沒(méi)有出現(xiàn)消息堆積的問(wèn)題。

然而,一年后的一天下午,一些商家又來(lái)投訴。

我查閱了公司郵件,發(fā)現(xiàn)有大量關(guān)于 Kafka 消息堆積的監(jiān)控告警郵件。

由于我當(dāng)時(shí)正在開(kāi)會(huì),錯(cuò)過(guò)了這些告警。

這次問(wèn)題的時(shí)間點(diǎn)比較奇怪。

通常高并發(fā)都是在午餐或晚餐的高峰時(shí)段,但這次消息堆積卻發(fā)生在“下午”。

這很不尋常。

一開(kāi)始,我沒(méi)有任何線(xiàn)索能找到問(wèn)題的原因。

于是,我詢(xún)問(wèn)了訂單團(tuán)隊(duì)是否在下午發(fā)布了新版本或者執(zhí)行了某些特定操作。

因?yàn)槲覀兊牟似饭芾硐到y(tǒng)是他們的下游系統(tǒng),直接和他們的操作相關(guān)。

一位同事提到,半小時(shí)前他們進(jìn)行了一個(gè)批量更新數(shù)萬(wàn)個(gè)訂單狀態(tài)的作業(yè)。

更改訂單狀態(tài)會(huì)自動(dòng)發(fā)送 MQ 消息。

這導(dǎo)致他們的程序在極短時(shí)間內(nèi)產(chǎn)生了大量 MQ 消息。

我們的 MQ 消費(fèi)端無(wú)法快速處理這些消息,因而出現(xiàn)了消息堆積。

我們查看了 Kafka 消息堆積情況,發(fā)現(xiàn)有數(shù)十萬(wàn)條消息在排隊(duì)等待處理。

為了快速提高 MQ 消費(fèi)端的處理速度,我們考慮了兩個(gè)解決方案:

  1. 增加分區(qū)數(shù)量。
  2. 使用線(xiàn)程池處理消息。

然而,由于消息已經(jīng)堆積在現(xiàn)有分區(qū)中,增加新的分區(qū)并不會(huì)有太大幫助。

因此,我們決定重構(gòu)代碼,使用線(xiàn)程池來(lái)處理消息。

為了解決堆積消息,我們將線(xiàn)程池的核心線(xiàn)程數(shù)和最大線(xiàn)程數(shù)增加到 50。

這些參數(shù)是可以動(dòng)態(tài)配置的。

經(jīng)過(guò)這個(gè)調(diào)整后,堆積的數(shù)十萬(wàn)條消息在大約 20 分鐘內(nèi)被處理完畢。

這次突發(fā)的消息堆積問(wèn)題得到了妥善解決。

解決問(wèn)題后,我們保留了線(xiàn)程池的消息消費(fèi)邏輯,將核心線(xiàn)程數(shù)設(shè)置為 8,最大線(xiàn)程數(shù)設(shè)置為 10。

這樣在遇到消息堆積問(wèn)題時(shí),我們可以臨時(shí)調(diào)整線(xiàn)程數(shù)以快速應(yīng)對(duì),而不會(huì)對(duì)用戶(hù)造成明顯影響。

注意:使用線(xiàn)程池消費(fèi) MQ 消息并不是一個(gè)通用的解決方案。它存在一些缺點(diǎn),比如可能會(huì)導(dǎo)致消息順序性問(wèn)題,以及服務(wù)器 CPU 使用率的飆升風(fēng)險(xiǎn)。此外,如果在多線(xiàn)程環(huán)境中調(diào)用第三方接口,可能會(huì)造成第三方服務(wù)的超負(fù)荷甚至崩潰。

總結(jié)來(lái)說(shuō),MQ 消息堆積不是一個(gè)簡(jiǎn)單的問(wèn)題。

根本原因是 MQ 生產(chǎn)端的消息生產(chǎn)速率超過(guò)了消費(fèi)端的消息消費(fèi)速率,但具體原因可能有多種。

在實(shí)際場(chǎng)景中,我們需要根據(jù)不同的業(yè)務(wù)情況進(jìn)行優(yōu)化。

對(duì) MQ 隊(duì)列消息堆積的監(jiān)控和告警至關(guān)重要,能夠及時(shí)發(fā)現(xiàn)問(wèn)題。

沒(méi)有完美的解決方案,只有最適合當(dāng)前業(yè)務(wù)場(chǎng)景的方案。

責(zé)任編輯:武曉燕 來(lái)源: 路條編程
相關(guān)推薦

2024-06-06 11:57:44

2024-05-14 08:20:59

線(xiàn)程CPU場(chǎng)景

2021-09-30 07:26:15

MQ消息丟失

2022-10-12 07:38:24

SQL語(yǔ)句異常

2023-12-21 08:01:41

RocketMQ消息堆積

2022-12-15 17:13:22

MQRocketMQ架構(gòu)

2009-06-14 21:41:23

Java Web框架

2012-09-04 09:55:22

代碼抓狂的代碼開(kāi)發(fā)

2021-11-08 15:38:15

消息延遲堆積

2020-06-15 14:36:15

2019-06-18 16:40:26

NodeJS前端項(xiàng)目

2022-07-26 00:00:00

MQ消息中間件

2014-07-01 09:43:55

程序員算法

2009-06-29 15:51:48

Spring容器

2021-10-26 08:22:38

消息堆積擴(kuò)容RocketMQ

2021-11-23 09:00:59

消息堆積擴(kuò)容RocketMQ

2021-04-14 09:24:59

App內(nèi)存后臺(tái)

2025-01-10 08:20:00

MQ消息架構(gòu)

2009-06-14 17:18:55

ibmdwWebSphereMQ

2023-06-29 10:10:06

Rocket MQ消息中間件
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)