如何設(shè)計(jì)一個小而美的搶紅包系統(tǒng)?
點(diǎn)擊“藍(lán)字”,關(guān)注,置頂公眾號
每日技術(shù)干貨,第一時間送達(dá)!
現(xiàn)如今,春節(jié)搶紅包的活動已經(jīng)逐漸變成大家過年的新風(fēng)俗。親朋好友的相互饋贈,微信、微博、支付寶等各大平臺種類繁多的紅包讓大家收到手軟。雞年春節(jié),鏈家也想給15萬的全國員工包個大紅包,于是我們構(gòu)建了一套旨在支撐10萬每秒請求峰值的搶紅包系統(tǒng)。經(jīng)實(shí)踐證明,春節(jié)期間我們成功的為所有的小伙伴提供了高可靠的服務(wù),紅包總發(fā)放量近百萬,搶紅包的峰值流量達(dá)到3萬/秒,最快的一輪搶紅包活動3秒鐘所有紅包全部搶完,系統(tǒng)運(yùn)行0故障。
紅包系統(tǒng),類似于電商平臺的秒殺系統(tǒng),本質(zhì)上都是在一個很短的時間內(nèi)面對巨大的請求流量,將有限的庫存商品分發(fā)出去,并完成交易操作。比如12306搶票,庫存的火車票是有限的,但瞬時的流量非常大,且都是在請求相同的資源,這里面數(shù)據(jù)庫的并發(fā)讀寫沖突以及資源的鎖請求沖突非常嚴(yán)重。就我們實(shí)現(xiàn)這樣一個紅包系統(tǒng)本身來說,面臨著如下的一些挑戰(zhàn):
首先,到活動整點(diǎn)時刻,我們有15萬員工在固定時間點(diǎn)同時涌入系統(tǒng)搶某輪紅包,瞬間的流量是很大的,而目前我們整個鏈路上的系統(tǒng)和服務(wù)基礎(chǔ)設(shè)施,都沒有承受過如此高的吞吐量,要在短時間內(nèi)實(shí)現(xiàn)業(yè)務(wù)需求,在技術(shù)上的風(fēng)險較大。
其次,公司是第一次開展這樣的活動,我們很難預(yù)知大家參與活動的情況,極端情況下可能會出現(xiàn)某輪紅包沒搶完,需要合并到下輪接著發(fā)放。這就要求系統(tǒng)有一個動態(tài)的紅包發(fā)放策略和預(yù)算控制,其中涉及到的動態(tài)計(jì)算會是個較大的問題(這也是為系統(tǒng)高吞吐服務(wù)),實(shí)際的系統(tǒng)實(shí)現(xiàn)中我們采用了一些預(yù)處理機(jī)制。
最后,這個系統(tǒng)是為了春節(jié)的慶祝活動而研發(fā)的定制系統(tǒng),且只上線運(yùn)行一次,這意味著我們無法積累經(jīng)驗(yàn)去對服務(wù)做持續(xù)的優(yōu)化。并且相關(guān)的配套環(huán)境沒有經(jīng)過實(shí)際運(yùn)行檢驗(yàn),缺少參考指標(biāo),系統(tǒng)的薄弱環(huán)節(jié)發(fā)現(xiàn)的難度大。所以必須要追求設(shè)計(jì)至簡,盡量減少對環(huán)境的依賴(數(shù)據(jù)路徑越長,出問題的環(huán)節(jié)越多),并且實(shí)現(xiàn)高可伸縮性,需要盡一切努力保證可靠性,即使有某環(huán)節(jié)失誤,系統(tǒng)依然能夠保障核心的用戶體驗(yàn)正常。

系統(tǒng)架構(gòu)圖如圖所示。所有的靜態(tài)資源提前部署在了第三方的CDN服務(wù)上,系統(tǒng)的核心功能主要劃分到接入層和核心邏輯系統(tǒng)中,各自部署為集群模式并且獨(dú)立。接入層主要是對用戶身份鑒權(quán)和結(jié)果緩存,核心系統(tǒng)重點(diǎn)關(guān)注紅包的分發(fā),紅色實(shí)線的模塊是核心邏輯,為了保障其可靠性,我們做了包括數(shù)據(jù)預(yù)處理、水平分庫、多級緩存、精簡RPC調(diào)用、過載保護(hù)等多項(xiàng)設(shè)計(jì)優(yōu)化,并且在原生容器、MySQL等服務(wù)基礎(chǔ)設(shè)施上針對特殊的業(yè)務(wù)場景做了優(yōu)化,后面將為讀者一一道來。
紅包本身的信息通過預(yù)處理資源接口獲取。運(yùn)行中用戶和紅包的映射關(guān)系動態(tài)生成。底層使用內(nèi)部開發(fā)的DB中間件在MySQL數(shù)據(jù)庫集群上做紅包發(fā)放結(jié)果持久化,以供異步支付紅包金額到用戶賬戶使用。整個系統(tǒng)的絕大部分模塊都有性能和?;畋O(jiān)控。
優(yōu)化方案中最重要的目標(biāo)是保障關(guān)鍵流程在應(yīng)對大量請求時穩(wěn)定運(yùn)行,這需要很高的系統(tǒng)可用性。所以,業(yè)務(wù)流程和數(shù)據(jù)流程要盡量精簡,減少容易出錯的環(huán)節(jié)。此外,緩存、DB、網(wǎng)絡(luò)、容器環(huán)境,任何一個部分都要假設(shè)可能會短時出現(xiàn)故障,要有處理預(yù)案。針對以上的目標(biāo)難點(diǎn),我們總結(jié)了如下的實(shí)踐經(jīng)驗(yàn)。
紅包本身的屬性信息(金額,狀態(tài),祝福語,發(fā)放策略),我們結(jié)合活動預(yù)案要求,使用一定的算法提前生成好所有的信息,數(shù)據(jù)總的空間不是很大。為了最大化提升性能,這些紅包數(shù)據(jù),我們事先存儲在數(shù)據(jù)庫中,然后在容器加載服務(wù)啟動時,直接加載到本地緩存中當(dāng)作只讀數(shù)據(jù)。另外,我們的員工信息,我們也做了一定的裁剪,最基本的信息也和紅包數(shù)據(jù)一樣,預(yù)先生成,服務(wù)啟動時加載。
此外,我們的活動頁面,有很多視頻和圖片資源,如果這么多的用戶從我們的網(wǎng)關(guān)實(shí)時訪問,很可能我們的帶寬直接就被這些大流量的請求占滿了,用戶體驗(yàn)可想而知。最后這些靜態(tài)資源,我們都部署在了CDN上,通過數(shù)據(jù)預(yù)熱的方式加速客戶端的訪問速度,網(wǎng)關(guān)的流量主要是來自于搶紅包期間的小數(shù)據(jù)請求。
通常的服務(wù)請求流程,是在接入層訪問用戶中心進(jìn)行用戶鑒權(quán),然后轉(zhuǎn)發(fā)請求到后端服務(wù),后端服務(wù)根據(jù)業(yè)務(wù)邏輯調(diào)用其他上游服務(wù),并且查詢數(shù)據(jù)庫資源,再更新服務(wù)/數(shù)據(jù)庫的數(shù)據(jù)。每一次RPC調(diào)用都會有額外的開銷,所以,比如上一點(diǎn)所說的預(yù)加載,使得系統(tǒng)在運(yùn)行期間每個節(jié)點(diǎn)都有全量的查詢數(shù)據(jù)可在本地訪問,搶紅包的核心流程就被簡化為了生成紅包和人的映射關(guān)系,以及發(fā)放紅包的后續(xù)操作。再比如,我們采用了異步拉的方式進(jìn)行紅包發(fā)放到賬,用戶搶紅包的請求不再經(jīng)過發(fā)放這一步,只記錄關(guān)系,性能得到進(jìn)一步提升。

實(shí)際上有些做法的可伸縮性是極強(qiáng)的。例如紅包數(shù)據(jù)的預(yù)生成信息,在當(dāng)時的場景下我們是能夠作為本地內(nèi)存緩存加速訪問的。當(dāng)紅包數(shù)據(jù)量很大的時候,在每個服務(wù)節(jié)點(diǎn)上使用本地?cái)?shù)據(jù)庫,或者本地?cái)?shù)據(jù)文件,甚至是本地Redis/MC緩存服務(wù),都是可以保證空間足夠的,并且還有額外的好處,越少的RPC,越少的服務(wù)抖動,只需要關(guān)注系統(tǒng)本身的健壯性即可,不需要考慮外部系統(tǒng)QoS。
春節(jié)整點(diǎn)時刻,同一個紅包會被成千上萬的人同時請求,如何控制并發(fā)請求,確保紅包會且僅會被一個用戶搶到?
做法一,使用加鎖操作先占有鎖資源,再占有紅包。
可以使用分布式全局鎖的方式(各種分布式鎖組件或者數(shù)據(jù)庫鎖),申請lock該紅包資源成功后再做后續(xù)操作。優(yōu)點(diǎn)是,不會出現(xiàn)臟數(shù)據(jù)問題,某一個時刻只有一個應(yīng)用線程持有l(wèi)ock,紅包只會被至多一個用戶搶到,數(shù)據(jù)一致性有保障。缺點(diǎn)是,所有請求同一時刻都在搶紅包A,下一個時刻又都在搶紅包B,并且只有一個搶成功,其他都失敗,效率很低。
做法二,單獨(dú)開發(fā)請求排隊(duì)調(diào)度模塊。
排隊(duì)模塊接收用戶的搶紅包請求,以FIFO模式保存下來,調(diào)度模塊負(fù)責(zé)FIFO隊(duì)列的動態(tài)調(diào)度,一旦有空閑資源,便從隊(duì)列頭部把用戶的訪問請求取出后交給真正提供服務(wù)的模塊處理。優(yōu)點(diǎn)是,具有中心節(jié)點(diǎn)的統(tǒng)一資源管理,對系統(tǒng)的可控性強(qiáng),可深度定制。缺點(diǎn)是,所有請求流量都會有中心節(jié)點(diǎn)參與,效率必然會比分布式無中心系統(tǒng)低,并且,中心節(jié)點(diǎn)也很容易成為整個系統(tǒng)的性能瓶頸。
做法三,巧用Redis特性,使其成為分布式序號生成器。(我們最終采用的做法)。
前文已經(jīng)提到,紅包系統(tǒng)所使用的紅包數(shù)據(jù)都是預(yù)先生成好的,我們使用數(shù)字ID來標(biāo)識,這個ID是全局唯一的,所有圍繞紅包的操作都使用這個ID作為數(shù)據(jù)的關(guān)聯(lián)項(xiàng)。在實(shí)際的請求流量過來時,我們采用了“分組”處理流量的方式,如下圖所示。

訪問請求被LB分發(fā)到每個分組,一個分組包含若干臺應(yīng)用容器、獨(dú)立的數(shù)據(jù)庫和Redis節(jié)點(diǎn)。Redis節(jié)點(diǎn)內(nèi)存儲的是這個分組可以分發(fā)的紅包ID號段,利用Redis單進(jìn)程的自減數(shù)值特性實(shí)現(xiàn)分布式紅包ID生成器,服務(wù)通過此獲取當(dāng)前拆到的紅包。落地?cái)?shù)據(jù)都持久化在獨(dú)立的數(shù)據(jù)庫中,相當(dāng)于是做了水平分庫。某個分組內(nèi)處理的請求,只會訪問分組內(nèi)部的Redis和數(shù)據(jù)庫,和其他分組隔離開。
分組的方式使得整個系統(tǒng)實(shí)現(xiàn)了高內(nèi)聚,低耦合的原則,能將數(shù)據(jù)流量分而治之,提升了系統(tǒng)的可伸縮性,當(dāng)面臨更大流量的需求時,通過線性擴(kuò)容的方法,即可應(yīng)對。并且當(dāng)單個節(jié)點(diǎn)出現(xiàn)故障時,影響面能夠控制在單個分組內(nèi)部,系統(tǒng)也就具有了較好的隔離性。
由于是首次開展活動,我們?nèi)狈?shí)際的運(yùn)營數(shù)據(jù),一切都是摸著石頭過河。所以從項(xiàng)目伊始,我們便強(qiáng)調(diào)對系統(tǒng)各個層次的預(yù)估,既包括了活動參與人數(shù)、每個功能feature用戶的高峰流量、后端請求的峰值、緩存系統(tǒng)請求峰值和數(shù)據(jù)庫讀寫請求峰值等,還包括了整個業(yè)務(wù)流程和服務(wù)基礎(chǔ)設(shè)施中潛在的薄弱環(huán)節(jié)。后者的難度更大因?yàn)楹茈y量化。此前我們連超大流量的全鏈路性能壓測工具都較缺乏,所以還是有很多實(shí)踐的困難的。
在這里內(nèi)心真誠的感謝開源社區(qū)的力量,在我們制定完系統(tǒng)的性能指標(biāo)參考值后,借助如wrk等優(yōu)秀的開源工具,我們在有限的資源里實(shí)現(xiàn)了對整個系統(tǒng)的端到端全鏈路壓測。實(shí)測中,我們的核心接口在單個容器上可以達(dá)到20,000以上的QPS,整個服務(wù)集群在110,000以上的QPS壓力下依然能穩(wěn)定工作。
正是一次次的全鏈路壓測參考指標(biāo),幫助我們了解了性能的基準(zhǔn),并以此做了代碼設(shè)計(jì)層面、容器層面、JVM層面、MySQL數(shù)據(jù)庫層面、緩存集群層面的種種優(yōu)化,極大的提升了系統(tǒng)的可用性。具體做法限于篇幅不在此贅述,有興趣的讀者歡迎交流。
此外,為了確保線上有超預(yù)估流量時系統(tǒng)穩(wěn)定,我們做了過載保護(hù)。超過性能上限閾值的流量,系統(tǒng)會快速返回特定的頁面結(jié)果,將此部分流量清理掉,保障已經(jīng)接受的有效流量可以正常處理。
系統(tǒng)在線上運(yùn)行過程中,我們很需要對其實(shí)時的運(yùn)行情況獲取信息,以便能夠?qū)Τ霈F(xiàn)的問題進(jìn)行排查定位,及時采取措施。所以我們必須有一套有效的監(jiān)控系統(tǒng),能夠幫我們觀測到關(guān)鍵的指標(biāo)。在實(shí)際的操作層面,我們主要關(guān)注了如下指標(biāo):
服務(wù)接口的性能指標(biāo)
借助系統(tǒng)的請求日志,觀測服務(wù)接口的QPS,接口總的實(shí)時響應(yīng)時間。同時通過HTTP的狀態(tài)碼觀測服務(wù)的語義層面的可用性。
系統(tǒng)健康度
結(jié)合總的性能指標(biāo)以及各個模塊應(yīng)用層的性能日志,包括模塊接口返回耗時,和應(yīng)用層日志的邏輯錯誤日志等,判斷系統(tǒng)的健康度。
整體的網(wǎng)絡(luò)狀況
盡量觀測每個點(diǎn)到點(diǎn)之間的網(wǎng)絡(luò)狀態(tài),包括應(yīng)用服務(wù)器的網(wǎng)卡流量、Redis節(jié)點(diǎn)、數(shù)據(jù)庫節(jié)點(diǎn)的流量,以及入口帶寬的占用情況。如果某條線路出現(xiàn)過高流量,便可及時采取擴(kuò)容等措施緩解。
服務(wù)基礎(chǔ)設(shè)施
應(yīng)用服務(wù)器的CPU、Memory、磁盤IO狀況,緩存節(jié)點(diǎn)和數(shù)據(jù)庫的相應(yīng)的數(shù)據(jù),以及他們的連接數(shù)、連接時間、資源消耗檢測數(shù)據(jù),及時的去發(fā)現(xiàn)資源不足的預(yù)警信息。
對于關(guān)鍵的數(shù)據(jù)指標(biāo),在超過預(yù)估時制定的閾值時,還需要監(jiān)控系統(tǒng)能夠?qū)崟r的通過手機(jī)和郵件實(shí)時通知的方式讓相關(guān)人員知道。另外,我們在系統(tǒng)中還做了若干邏輯開關(guān),當(dāng)某些資源出現(xiàn)問題并且自動降級和過載保護(hù)模塊失去效果時,我們可以根據(jù)狀況直接人工介入,在服務(wù)不停機(jī)的前提前通過手動觸發(fā)邏輯開關(guān)改變系統(tǒng)邏輯,達(dá)到快速響應(yīng)故障,讓服務(wù)盡快恢復(fù)穩(wěn)定的目的。
當(dāng)服務(wù)器壓力劇增的時候,如果某些依賴的服務(wù)設(shè)施或者基礎(chǔ)組件超出了工作負(fù)荷能力,發(fā)生了故障,這時候極其需要根據(jù)當(dāng)前的業(yè)務(wù)運(yùn)行情況對系統(tǒng)服務(wù)進(jìn)行有策略的降級運(yùn)行措施,使得核心的業(yè)務(wù)流程能夠順利進(jìn)行,并且減輕服務(wù)器資源的壓力,最好在壓力減小后還能自動恢復(fù)升級到原工作機(jī)制。
我們在開發(fā)紅包系統(tǒng)時,考慮到原有IDC機(jī)房的解決方案對于彈性擴(kuò)容和流量帶寬支持不太完美,選擇了使用AWS的公有云作為服務(wù)基礎(chǔ)環(huán)境。對于第三方的服務(wù),缺少實(shí)踐經(jīng)驗(yàn)的把握,于是從開發(fā)到運(yùn)維過程中,我們都保持了一種防御式的思考方式,包括數(shù)據(jù)庫、緩存節(jié)點(diǎn)故障,以及應(yīng)用服務(wù)環(huán)境的崩潰、網(wǎng)絡(luò)抖動,我們都認(rèn)為隨時可能出問題,都需要對應(yīng)的自動替換降級策略,嚴(yán)重時甚至可通過手動觸發(fā)配置開關(guān)修改策略。當(dāng)然,如果組件自身具有降級功能,可以給上層業(yè)務(wù)節(jié)約很多成本資源,要自己實(shí)現(xiàn)全部環(huán)節(jié)的降級能力的確是一件比較耗費(fèi)資源的事情,這也是一個公司技術(shù)慢慢積累的過程。
以上是我們整個系統(tǒng)研發(fā)運(yùn)維的一些體會。這次春節(jié)紅包活動,在資源有限的情況下成功抵抗超乎平常的流量峰值壓力,對于技術(shù)而言是一次很大的挑戰(zhàn),也是一件快樂的事情,讓我們從中積累了很多實(shí)踐經(jīng)驗(yàn)。未來我們將不斷努力,希望能夠?qū)⒉糠洲D(zhuǎn)化成較為通用的技術(shù),去更好的推動業(yè)務(wù)成功。真誠希望本文的分享能夠?qū)Υ蠹业募夹g(shù)工作有所幫助。
來源:https://blog.csdn.net/liangyihuai/article/details/60115538
往期推薦
