一次排隊(duì)引起的優(yōu)化之旅

業(yè)務(wù)場景

我司聊天服務(wù)是基于開源的Ejabberd項(xiàng)目(v19.02)搭建,其中有一項(xiàng)以平臺部API形式提供給各項(xiàng)目組游戲服,用于發(fā)送Announce(即公告)到指定聊天室的功能。內(nèi)在實(shí)現(xiàn)是以一個(gè)平臺維護(hù)的、在Ejabberd已注冊的客戶端賬號(該賬號并不存在于各項(xiàng)目的各聊天室中)調(diào)用Ejabberd提供的API(

)發(fā)送指定信息至對應(yīng)聊天室。

所以該方案的實(shí)現(xiàn),實(shí)質(zhì)上是一個(gè)玩家(對應(yīng)一個(gè)Ejabberd客戶端賬號)發(fā)送聊天消息到指定聊天室。而對于單個(gè)玩家發(fā)送消息至指定聊天室的頻率,Ejabberd是有配置參數(shù)可以設(shè)置的,官方給出的參考值是:

,具體含義是:單個(gè)玩家在單個(gè)聊天室發(fā)言頻率不能高于0.4s/條。

原始解決方案

原始解決方案簡單粗暴;

1.對于每個(gè)請求(這里指調(diào)用公告API的HTTP請求,下同),將請求數(shù)據(jù)丟到以room_name(聊天室name)為key的redis單向隊(duì)列中(以RPush、LPop方式構(gòu)造),然后將room_name扔到全局的channel中;

2.在處理channel數(shù)據(jù)的routine函數(shù)中,以阻塞模式監(jiān)聽channel中是否有數(shù)據(jù)產(chǎn)生,如果有則已以獲取的room_name為key的redis單向隊(duì)列中pop一條數(shù)據(jù),而后強(qiáng)制sleep 0.5s, 再調(diào)用Ejabberd提供的API接口send_stanza發(fā)送剛才pop獲取的消息至指定聊天室。

明顯的缺陷

最大的缺陷就是,對于每個(gè)請求都強(qiáng)制sleep了0.5s,不論該請求將發(fā)往哪個(gè)聊天室。也就是全局只有一個(gè)channel,所有調(diào)用公告接口的請求都被強(qiáng)制排隊(duì)了。其次,redis隊(duì)列是全局的,而channel歸屬單個(gè)gochat服務(wù)實(shí)例,如此可能造成消息到達(dá)時(shí)失序等問題。至于測試時(shí)為什么沒有發(fā)現(xiàn)問題,當(dāng)然是沒有做壓測的緣故。

造成的后果

對于普通玩家而言,該限制對實(shí)際操作幾乎沒有任何影響(無論是正常玩家的發(fā)言頻率、還是客戶端的發(fā)言頻率限制都可以保證普通玩家發(fā)言頻率低于0.4s),而對于依賴該API進(jìn)行各類系統(tǒng)公告或有基于該API玩法的項(xiàng)目組而言,該方案的限制大大影響到了游戲的正常進(jìn)行。以C2為例,上線后對于單條公告的延遲最高竟達(dá)到了80分鐘。。

優(yōu)化之旅

經(jīng)過線上幾個(gè)小時(shí)的檢驗(yàn),原始方案顯然是行不通的,緊急回滾之后,便踏上優(yōu)化之旅。

優(yōu)化方案一

原始方案最大的缺陷當(dāng)然就是不分room,對每條公告請求都強(qiáng)制排隊(duì)并sleep 0.5s. 所以最先想到的優(yōu)化方案便是在保持現(xiàn)有Ejabberd發(fā)言頻率限制的前提下,將隊(duì)列的粒度細(xì)化為針對每個(gè)聊天室分別排隊(duì)。再經(jīng)分析,具體的方案是:

1.將最初設(shè)計(jì)的針對每個(gè)聊天室分別排隊(duì)方案修改為:建立100個(gè)channel,根據(jù)游戲服id對100取模,模相同的游戲服中所有聊天室公用一個(gè)channel(以C2項(xiàng)目為例,全服共600余服,每服所含聊天室數(shù)量不一,大到十幾萬,小至幾十),每個(gè)channel最多可容納1w個(gè)room;

2.針對1中的每個(gè)channel,再建立一個(gè)wait_channel,最多也可容納1w個(gè)room;

3.對于每個(gè)請求,首先將請求數(shù)據(jù)丟到以room_name(聊天室name)為key的redis單向隊(duì)列中(上文提到過),然后將該room丟到所在游戲服對應(yīng)的channel中(1提到的取模);

4.起100個(gè)routine,每個(gè)routine對應(yīng)處理1中對應(yīng)序列的channel. 阻塞模式監(jiān)聽channel,如果有數(shù)據(jù)則從獲取的room_name為key的redis單向隊(duì)列中pop一條數(shù)據(jù),并檢查redis中是否有以room_id(對應(yīng)room_name)為key的記錄,若沒有記錄,則表示當(dāng)前可以向該room發(fā)送公告消息,執(zhí)行對應(yīng)操作;

5.若4中redis有以room_id(對應(yīng)room_name)為key的記錄,則表示正有進(jìn)程對該room進(jìn)行公告操作,當(dāng)前操作需等待,此時(shí)便將4中pop獲取的數(shù)據(jù)丟到2建立的wait_channel中,在wait_channel中阻塞 模式監(jiān)聽數(shù)據(jù),對于獲取的每條數(shù)據(jù)都間隔0.1s查詢對應(yīng)rid在redis是否有記錄,直至返回沒有記錄后,則表示當(dāng)前可以向該room發(fā)送公告消息,執(zhí)行對應(yīng)操作。

缺陷

該方案理論上消除了全部請求都需要強(qiáng)制在一個(gè)隊(duì)列排隊(duì)0.5s的問題,但是卻沒有考慮另一個(gè)現(xiàn)實(shí):向游戲服提供公告API功能的服務(wù)本身是以多副本形式存在的,每個(gè)副本的內(nèi)存數(shù)據(jù)獨(dú)立,但共享redis數(shù)據(jù)。

當(dāng)前方案每個(gè)副本均維護(hù)了200個(gè)channel,每個(gè)副本接收到的請求都扔到了自己維護(hù)的channel,但是請求數(shù)據(jù)卻寫到了redis中。

在多個(gè)副本競爭redis中同一key下數(shù)據(jù)時(shí)并非原子操作,可能存在獲取數(shù)據(jù)(Get操作)后、設(shè)置key對應(yīng)TTL(Set操作)前,在進(jìn)行邏輯操作期間其它副本先執(zhí)行Set操作的可能,導(dǎo)致當(dāng)前副本一直拿不到執(zhí)行后續(xù)處理的機(jī)會,也就導(dǎo)致本身分發(fā)到當(dāng)前副本的請求遲遲得不到處理,公告消息產(chǎn)生不可預(yù)測的延遲。

優(yōu)化方案二

主要優(yōu)化多副本、多channel、單redis導(dǎo)致的請求可能產(chǎn)生的延遲等問題。既然實(shí)際請求數(shù)據(jù)在redis中,對于同一聊天室當(dāng)前是否可以發(fā)送數(shù)據(jù)的標(biāo)記也記錄在redis中,則實(shí)際上不需要副本中自行維護(hù)請求的channel隊(duì)列,應(yīng)將數(shù)據(jù)統(tǒng)一維護(hù)在redis中。具體方案:

1.redis中維護(hù)一張map,實(shí)際為map[string]queue類型,map中每個(gè)key對應(yīng)一個(gè)room_name,value是一個(gè)單向隊(duì)列,隊(duì)列中每個(gè)元素對應(yīng)一個(gè)請求數(shù)據(jù);維護(hù)一個(gè)list,每個(gè)元素對應(yīng)一個(gè)room_id。每當(dāng)有新的請求道來,就向map和listpush相應(yīng)數(shù)據(jù);

2.單獨(dú)起一routine(全局),采用redis的BLPop方法阻塞式的獲取1中l(wèi)ist的一個(gè)key,獲取之后就起一routine單獨(dú)處理該請求;

3.對于2中處理請求的routine,死循環(huán)采用redis的setNX方法獲取room_id對應(yīng)的鎖,TTL設(shè)置為0.4s(SetNX(room_id, 1, 0.4s)),目的是獲取當(dāng)前routine對該room發(fā)送聊天消息的權(quán)限。獲取權(quán)限后執(zhí)行相應(yīng)操作。

缺陷

對于單個(gè)副本而言,將所有接收到的HTTP請求都丟到了redis隊(duì)列匯總,然后另起一全局routine循環(huán)從該隊(duì)列中拿一個(gè)room_id,再起一臨時(shí)routine處理該room_id對應(yīng)的請求,實(shí)則有點(diǎn)多余。

最終方案

基于優(yōu)化方案二的最終方案:

接收到請求后,直接調(diào)用redis的setNX方法檢查當(dāng)前是否可以發(fā)送消息到指定room_id(setNX實(shí)質(zhì)上相當(dāng)于該room_id的鎖,拿到set的權(quán)限相當(dāng)于獲取鎖),若可以發(fā)送則執(zhí)行相應(yīng)操作;若不可以發(fā)送則直接返錯(cuò)給調(diào)用方,簡單直接。

同時(shí)評估項(xiàng)目組需求后,平臺將上文提到的0.4s的限制降到了0.1s,盡量降低該限制對項(xiàng)目組需求的影響。

測試結(jié)果

在項(xiàng)目組配合修改調(diào)用及重試邏輯后,針對該接口的調(diào)用進(jìn)行了貼近生產(chǎn)環(huán)境的壓測,測試結(jié)果表明最終方案達(dá)到了生產(chǎn)環(huán)境的性能要求。

彩蛋

最終方案的性能要求是達(dá)到了,但是Ejabberd官方對send_stanza接口的實(shí)現(xiàn)中,并沒有將以該接口發(fā)送的消息存檔,也就是說,客戶端重新登錄后,之前收到的系統(tǒng)公告消息就丟掉了。這個(gè)缺陷對于強(qiáng)依賴該公告接口的項(xiàng)目而言是不可接受的。當(dāng)兩種基于send_stanza的補(bǔ)救方案(修改Ejabberd源碼、構(gòu)造該消息格式手動存檔)因各種原因確認(rèn)不可行后,此次所謂優(yōu)化最終也被棄用,需要另尋出路。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容