幾年前,我擼了一套R(shí)abbitMQ的客戶端
掃描二維碼
隨時(shí)隨地手機(jī)看文章
不好意思,又好多天沒(méi)更文章了……
眼看著離過(guò)年越來(lái)越近了,很多工作都要在年前沖刺、收個(gè)尾。比如:工作總結(jié)、績(jī)效考核、獎(jiǎng)金、確定今年 KPI……
由于我負(fù)責(zé)的部門一百多人,雖然有下面的各位 Leader 幫忙,但是我的工作量還是很大的,每天一腦門子雜七雜八的事情,還有大大小小的各種會(huì)議……真沒(méi)時(shí)間輸出文章。
這不,在我的讀者群里,都被大家催更了。
在此感謝:
阿德、enjoy.day、Genos等等(不一一列舉了,我都記在心里了)各位老鐵催更。
RabbitMQ 的新文章總算寫好了。
我在上篇文章說(shuō)過(guò),如果使用 RabbitMQ,盡可能使用框架,而不要去使用 RabbitMQ 提供的 Java 版客戶端。
細(xì)說(shuō)起來(lái),其實(shí)還是因?yàn)?RabbitMQ 客戶端的使用有很多的注意事項(xiàng),稍微不注意,就容易翻車。
我是 2013 年就開始用起了 RabbitMQ,一路使用,一路和它一起成長(zhǎng)。當(dāng)時(shí),由于用的早,市面上也沒(méi)有特別成熟的 RabbitMQ 客戶端框架。所以,不得已之下,只好自己做了一套客戶端。
在這其中,正好也有了許多獨(dú)特的經(jīng)驗(yàn)也和大家分享一下,以免后來(lái)者陷入“后人哀之而不鑒之,亦使后人而復(fù)哀后人也”的套娃中。
一、那么,就先從網(wǎng)絡(luò)連接開始吧
1. 應(yīng)該長(zhǎng)久生存的連接
在 RabbitMQ 中,由于需要客戶端和服務(wù)器端進(jìn)行握手,所以導(dǎo)致客戶端和服務(wù)器端的連接如果要成功創(chuàng)建,需要很高的成本。
每一個(gè)連接的創(chuàng)建至少需要 7 個(gè) TCP 包,這還只是普通連接。如果需要 TLS 的參與,則 TCP 包會(huì)更多。
而且,RabbitMQ 中主要是以 Channel 方式通信,所以,每次創(chuàng)建完 Connection 網(wǎng)絡(luò)連接,還得創(chuàng)建 Channel,這又需要 2 個(gè) TCP 包。
如果,每次用完,再把連接關(guān)閉,首先還要關(guān)閉已經(jīng)創(chuàng)建的 Channel,這也需要 2 個(gè) TCP 包。
然后,再關(guān)閉已經(jīng)建立好的 Connection 連接,又需要 2 個(gè) TCP 包。
咱們算算,如果一個(gè)連接從創(chuàng)建到關(guān)閉,一共需要多少個(gè) TCP 包?
7 + 2 + 2 + 2 = 13
一共需要 13 個(gè)包。這個(gè)成本是很昂貴的。
所以,在 RabbitMQ 中,連接最好緩存起來(lái),重復(fù)使用更好。
2. Channel 還是獨(dú)占好
在 RabbitMQ 自己的客戶端中,Channel 出于性能原因,并不是線程安全的。
而如果咱們?yōu)榱司€程共用,給 Channel 人為的在外部加上鎖,本身就和 RabbitMQ 的 Channel 設(shè)計(jì)意圖是沖突的。
所以,最好的辦法就是一個(gè)線程一個(gè) Channel。
3. Channel 最好也別關(guān)
就像連接應(yīng)該緩存起來(lái)那樣,Channel 的打開和關(guān)閉也需要時(shí)間成本,而且沒(méi)有必要去重新創(chuàng)建 Channel,所以,Channel 也應(yīng)該緩存起來(lái)重用。
4. 別把消費(fèi)和發(fā)送的連接搞在一起
把消費(fèi)和發(fā)送的連接搞在一起,這是個(gè)很容易犯的錯(cuò)誤!
我們用 RabbitMQ 的時(shí)候,我們自己的系統(tǒng)本身大部分都是既要發(fā)消息也要收消息的。對(duì)于這種情況,有很多程序員走了極端:
他們覺(jué)得 RabbitMQ 連接成本高,所以省著用。于是就把發(fā)消息和收消息的連接混在一起,使用同一個(gè) TCP 連接。
這很可能會(huì)埋一個(gè)大雷。
因?yàn)椋?dāng)我們發(fā)消息很頻繁的時(shí)候,我們收消息也是走的同一個(gè) TCP 通道,收完了消息,客戶端還要給 RabbitMQ 服務(wù)器端一個(gè) ACK。
RabbitMQ 服務(wù)器端,對(duì)于每個(gè) TCP 連接都會(huì)分配專門的進(jìn)程,如果遇到這個(gè)進(jìn)程繁忙,這個(gè) ACK 很可能被丟棄,又或者等待處理的時(shí)間過(guò)長(zhǎng)。而這種情況又會(huì)導(dǎo)致 RabbitMQ 中的未確認(rèn)消息會(huì)被堆積的越來(lái)越多,影響到整套系統(tǒng)。
所以,消費(fèi)和發(fā)送的連接必須分開,各干各的事情。
5. 別搞太多連接和 Channel,RabbitMQ 的 Web 受不了
RabbitMQ 的 Web 插件會(huì)收集很多連接,和其對(duì)應(yīng) Channel 的相關(guān)數(shù)據(jù)。
如果連接和 Channel 堆積太多了,整個(gè) Web 打開會(huì)非常慢,幾乎無(wú)法對(duì) RabbitMQ 進(jìn)行管理。所以,要注意限制連接和 Channel 的數(shù)量。
二、消息很寶貴,千萬(wàn)別亂拋棄哦
用來(lái)通信的消息是很寶貴的。
因?yàn)槊織l消息都可能攜帶了關(guān)鍵的數(shù)據(jù)和信息。所以,保證消息不丟失,需要根據(jù)消息的重要性,采取很多的措施。
1. 小心,Queue 存在再發(fā)消息
一條消息,在 RabbitMQ 中會(huì)先發(fā)到 Exchange,再由 Exchange 交給對(duì)應(yīng)的 Queue。
而當(dāng) Queue 不存在,或者沒(méi)匹配到合適的 Queue 的時(shí)候,默認(rèn)就會(huì)把消息發(fā)到系統(tǒng)中的 /dev/null 中。
而且還不會(huì)報(bào)錯(cuò)。
這個(gè)坑當(dāng)年把我坑慘了!我猜這個(gè)坑無(wú)數(shù)人踩過(guò)吧。
所以,在發(fā)送消息的時(shí)候,最好通過(guò) declare passive 這種方法去探測(cè)下隊(duì)列是否存在,保證消息發(fā)送不會(huì)丟的莫名其妙。
2. 收到消息請(qǐng)告訴我
在使用 RabbitMQ 客戶端的時(shí)候,發(fā)送消息,一定要考慮使用 confirm 機(jī)制。
這個(gè)機(jī)制就是當(dāng)消息收到了,RabbitMQ 會(huì)往客戶端發(fā)送一個(gè)通知,客戶端收到這個(gè)通知后,如果存在一個(gè) confirm 處理器,那么就會(huì)回調(diào)這個(gè)處理器處理。這時(shí)候,我們就能確保消息是被中間件收到了。
所以,一定要考慮使用 confirm 處理器去確保消息被 RabbitMQ 服務(wù)器收到。
3. 有時(shí)候消息出了問(wèn)題我也需要知道
在某些業(yè)務(wù)里,可能需要知道消息發(fā)送失敗的場(chǎng)景,以便執(zhí)行失敗的處理邏輯。這時(shí)候,就要考慮 RabbitMQ 客戶端的 return 機(jī)制。
這個(gè)機(jī)制就是當(dāng)消息在服務(wù)器端路由的時(shí)候出現(xiàn)了錯(cuò)誤,比如沒(méi)有 Exchange、或者 RoutingKey 不存在,則 RabbitMQ 會(huì)返回一個(gè)響應(yīng)給客戶端。客戶端收到后會(huì)回調(diào) return 的處理器。這時(shí)候,客戶端所在系統(tǒng)就能感知到這種錯(cuò)誤了,從而進(jìn)行對(duì)應(yīng)的處理。
4. 為了一定不丟消息我也是拼了
還有的時(shí)候,消息需要處理強(qiáng)一致性這種事務(wù)性質(zhì)的業(yè)務(wù)。這時(shí)候,就必須開啟 RabbitMQ 的事務(wù)模式。但是,這個(gè)模式會(huì)導(dǎo)致整體 RabbitMQ 的性能下降 250 倍。
一般沒(méi)有必要,不建議開啟。
5. 把消息寫到磁盤上
一般來(lái)說(shuō),為了防止消息丟失,需要在 RabbitMQ 服務(wù)器收到消息的時(shí)候,先持久化消息到磁盤上,防止服務(wù)器狀態(tài)出現(xiàn)問(wèn)題,消息丟失。
但是,持久化消息,必須先持久化隊(duì)列,持久化隊(duì)列完還不行,還必須把消息的 delivery mode 設(shè)置為 2,這樣才能把消息存到磁盤。但是,這種行為會(huì)讓整個(gè) RabbitMQ 的性能下降 60%。
這種可以根據(jù)實(shí)際情況進(jìn)行抉擇。
三、對(duì)于收消息這件事,別由著性子來(lái)
1. 能一次拿多個(gè)干嘛要一次只拿一個(gè)
很多時(shí)候,一些 RabbitMQ 的新手,覺(jué)得如果在一個(gè) mainloop 類似的無(wú)限循環(huán)里,去主動(dòng)獲取消息,會(huì)更加及時(shí)的獲取到消息,也會(huì)擁有更加出色的性能。所以,他們會(huì)使用 get 這種行為去取代 consume 這種行為。
這時(shí)候,他們其實(shí)已經(jīng)踩進(jìn)了大坑。
為了能主動(dòng) get 服務(wù)器消息,很多新手會(huì)去寫一個(gè)無(wú)限循環(huán),然后不斷嘗試去 RabbitMQ 服務(wù)器端獲取消息。但是,get 方法,其實(shí)是只去獲取了隊(duì)列中的第一條消息。
而采用 consume 方式呢,它的默認(rèn)方式是只要有消息,就會(huì)批量的拿,直到拿光所有還沒(méi)消費(fèi)過(guò)的消息。
一個(gè)是一條條拿,一個(gè)是批量拿,哪個(gè)效率更高一目了然。
所以,盡量采用 consume 方式獲取消息。
2. 拿消息也要講方法論的
消費(fèi)消息的時(shí)候,其實(shí)最難掌握的就是:
一次我們到底要取多少條消息?
對(duì)于 RabbitMQ 來(lái)講,如果我們不對(duì)消費(fèi)行為做限制,他會(huì)有多少消息就獲取多少消息。這就造成了一個(gè)問(wèn)題:
如果消息過(guò)多,我們一次性把消息讀取到內(nèi)存,很可能就會(huì)把應(yīng)用的內(nèi)存擠崩掉。
所以,我們要對(duì)這種情況做一些限制。
這時(shí)候,需要限制一次獲取消息的數(shù)量,一般來(lái)講,當(dāng)我們的業(yè)務(wù)是異步發(fā)送,異步消費(fèi),不需要實(shí)時(shí)給回響應(yīng)的時(shí)候,經(jīng)驗(yàn)數(shù)據(jù)是一次獲取 1000 條。
當(dāng)然,系統(tǒng)和系統(tǒng)不一樣,硬件條件也不一樣,大家可以根據(jù)實(shí)際的情況來(lái)設(shè)置一次性獲取的消息數(shù)量。
重點(diǎn)要說(shuō)說(shuō)同步。
在很多時(shí)候,我們需要通過(guò) RabbitMQ 傳送消息,并能通過(guò)臨時(shí)隊(duì)列等技巧去實(shí)時(shí)返回處理結(jié)果。這時(shí)候,就沒(méi)辦法一次抓多條數(shù)據(jù)進(jìn)行處理了,因?yàn)椋邪l(fā)送端在等處理結(jié)果,依次處理,再依次返回,黃花菜都涼了。
而且大部分時(shí)候,這種同步等待響應(yīng)的業(yè)務(wù)是有順序要求的。所以,也不能并行同時(shí)抓出多條信息處理。那么,彼時(shí),設(shè)置每次只消費(fèi)一條消息就是理所應(yīng)當(dāng)?shù)牧恕?/span>
最后
從上面的內(nèi)容中,你也看到了,RabbitMQ 客戶端如果要使用,對(duì)新手是多可惡的一件事情,各種坑,各種復(fù)雜性。
所以,如果你覺(jué)得 Spring 之類的 AMQP 客戶端框架合你心意,那么你就使用它。
但是,Spring 的東西有個(gè)毛病,如果你要用它,你的應(yīng)用必須也都要用 Spring。有些時(shí)候,也沒(méi)有這種必要。這時(shí)候,你就可以根據(jù)我說(shuō)的這些注意事項(xiàng)和經(jīng)驗(yàn),自己開發(fā)一套 RabbitMQ 的封裝框架,去降低 RabbitMQ 的使用門檻。
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!