一個(gè)“扛住100億次請求”的春晚紅包系統(tǒng)
上一篇:最近一些想法
偶然看到了《扛住100億次請求——如何做一個(gè)“有把握”的春晚紅包系統(tǒng)》一文,看完以后,感慨良多,收益很多。正所謂他山之石,可以攻玉,雖然此文發(fā)表于2015年,我看到時(shí)已經(jīng)過去良久,但是其中的思想仍然可以為很多后端設(shè)計(jì)借鑒。
注:本文以及作者所有內(nèi)容,僅代表個(gè)人理解和實(shí)踐,過程和微信團(tuán)隊(duì)沒有任何關(guān)系,真正的線上系統(tǒng)也不同,只是從一些技術(shù)點(diǎn)進(jìn)行了實(shí)踐,請讀者進(jìn)行區(qū)分。
背景知識
QPS:Queries per second 每秒的請求數(shù)目。
PPS:Packets per second 每秒數(shù)據(jù)包數(shù)目。?
搖紅包:客戶端發(fā)出一個(gè)搖紅包的請求,如果系統(tǒng)有紅包就會返回,用戶獲得紅包。
發(fā)紅包:產(chǎn)生一個(gè)紅包里面含有一定金額,紅包指定數(shù)個(gè)用戶,每個(gè)用戶會收到紅包信息,用戶可以發(fā)送拆紅包的請求,獲取其中的部分金額。
確定目標(biāo)
在一切系統(tǒng)開始以前,我們應(yīng)該搞清楚我們的系統(tǒng)在完成以后,應(yīng)該有一個(gè)什么樣的負(fù)載能力。
但是目前中國肯定不會有14億用戶同時(shí)在線,參考 http://qiye.qianzhan.com/show/detail/160818-b8d1c700.html的說法,2016年Q2 微信用戶大概是8億,月活在5.4 億左右。所以在2015年春節(jié)期間,雖然使用的用戶會很多,但是同時(shí)在線肯定不到5.4億。
| 單機(jī)需要支持的負(fù)載數(shù)
| 單機(jī)峰值QPS
| 發(fā)放紅包
最后考慮到系統(tǒng)的真實(shí)性,還至少有用戶登錄的動作,真實(shí)的系統(tǒng)還會包括聊天這樣的服務(wù)業(yè)務(wù)。
最后整體看一下 100億次搖紅包這個(gè)需求,假設(shè)它是均勻地發(fā)生在春節(jié)聯(lián)歡晚會的4個(gè)小時(shí)里,那么服務(wù)器的QPS 應(yīng)該是10000000000/600/3600/4.0=1157。也就是單機(jī)每秒1000多次,這個(gè)數(shù)值其實(shí)并不高。
如果完全由峰值速度1400萬消化 10000000000/(1400*10000) = 714秒,也就是說只需要峰值堅(jiān)持11分鐘,就可以完成所有的請求。可見互聯(lián)網(wǎng)產(chǎn)品的一個(gè)特點(diǎn)就是峰值非常高,持續(xù)時(shí)間并不會很長。
| 總結(jié)
①?支持至少100萬連接用戶。
②?每秒至少能處理2.3萬的QPS,這里我們把目標(biāo)定得更高一些 ,分別設(shè)定到了3萬和6萬。
③?搖紅包:支持每秒83個(gè)的速度下發(fā)放紅包,也就是說每秒有2.3萬次搖紅包的請求,其中83個(gè)請求能搖到紅包,其余的2.29萬次請求會知道自己沒搖到。當(dāng)然客戶端在收到紅包以后,也需要確保客戶端和服務(wù)器兩邊的紅包數(shù)目和紅包內(nèi)的金額要一致。因?yàn)闆]有支付模塊,所以我們也把要求提高一倍,達(dá)到200個(gè)紅包每秒的分發(fā)速度。
④?支持用戶之間發(fā)紅包業(yè)務(wù),確保收發(fā)兩邊的紅包數(shù)目和紅包內(nèi)金額要一致。同樣也設(shè)定200個(gè)紅包每秒的分發(fā)速度為我們的目標(biāo)。
想要完整地模擬整個(gè)系統(tǒng)實(shí)在是太難了,首先需要海量的服務(wù)器,其次需要上億的模擬客戶端。這對我來說是辦不到,但是有一點(diǎn)可以確定,整個(gè)系統(tǒng)是可以水平擴(kuò)展的,所以我們可以模擬100萬客戶端,再模擬一臺服務(wù)器,那么就完成了1/600的模擬。
和現(xiàn)有系統(tǒng)區(qū)別:和大部分高QPS測試的不同,本系統(tǒng)的側(cè)重點(diǎn)有所不同。我對2者做了一些對比。

基礎(chǔ)軟件和硬件
服務(wù)器操作系統(tǒng):Ubuntu 12.04。
客戶端操作系統(tǒng):debian 5.0。
| 硬件環(huán)境
服務(wù)器硬件版本:

服務(wù)器CPU信息:

客戶端:esxi 5.0 虛擬機(jī),配置為4核5G內(nèi)存。一共17臺,每臺和服務(wù)器建立6萬個(gè)連接。完成100萬客戶端模擬。
技術(shù)分析和實(shí)現(xiàn)
| 單機(jī)實(shí)現(xiàn)100萬用戶連接
https://github.com/xiaojiaqi/C1000kPracticeGuide
https://github.com/xiaojiaqi/C1000kPracticeGuide/tree/master/docs/cn
這個(gè)問題需要分2個(gè)部分來看客戶端方面和服務(wù)器方面。
1)客戶端QPS
因?yàn)橛?00萬連接連在服務(wù)器上,QPS為3萬。這就意味著每個(gè)連接每33秒,就需要向服務(wù)器發(fā)一個(gè)搖紅包的請求。因?yàn)閱蜪P可以建立的連接數(shù)為6萬左右,有17臺服務(wù)器同時(shí)模擬客戶端行為。我們要做的就是保證在每一秒都有這么多的請求發(fā)往服務(wù)器即可。
其中技術(shù)要點(diǎn)就是客戶端協(xié)同。但是各個(gè)客戶端的啟動時(shí)間,建立連接的時(shí)間都不一致,還存在網(wǎng)絡(luò)斷開重連這樣的情況,各個(gè)客戶端如何判斷何時(shí)自己需要發(fā)送請求,各自該發(fā)送多少請求呢?
我是這樣解決的:利用NTP服務(wù),同步所有的服務(wù)器時(shí)間,客戶端利用時(shí)間戳來判斷自己的此時(shí)需要發(fā)送多少請求。
算法很容易實(shí)現(xiàn):假設(shè)有100萬用戶,則用戶id為0-999999.要求的QPS為5萬,客戶端得知QPS為5萬,總用戶數(shù)為100萬,它計(jì)算 100萬/5萬=20,所有的用戶應(yīng)該分為20組,如果 time() % 20 == 用戶id % 20,那么這個(gè)id的用戶就該在這一秒發(fā)出請求,如此實(shí)現(xiàn)了多客戶端協(xié)同工作。每個(gè)客戶端只需要知道總用戶數(shù)和QPS就能自行準(zhǔn)確發(fā)出請求了。另外,搜索公眾號互聯(lián)網(wǎng)架構(gòu)師后臺回復(fù)“面試”,獲取一份驚喜禮包。
擴(kuò)展思考:如果QPS是3萬這樣不能被整除的數(shù)目,該如何做?如何保證每臺客戶端發(fā)出的請求數(shù)目盡量的均衡呢?
服務(wù)器端的QPS相對簡單,它只需要處理客戶端的請求即可。但是為了客觀了解處理情況,我們還需要做2件事情。
第一:需要記錄每秒處理的請求數(shù)目,這需要在代碼里埋入計(jì)數(shù)器。
第二:需要監(jiān)控網(wǎng)絡(luò),因?yàn)榫W(wǎng)絡(luò)的吞吐情況,可以客觀的反映出QPS的真實(shí)數(shù)據(jù)。為此,我利用python腳本結(jié)合ethtool工具編寫了一個(gè)簡單的工具,通過它我們可以直觀地監(jiān)視到網(wǎng)絡(luò)的數(shù)據(jù)包通過情況如何。它可以客觀地顯示出我們的網(wǎng)絡(luò)有如此多的數(shù)據(jù)傳輸在發(fā)生。
工具截圖:?

因?yàn)閱螜C(jī)每秒有3萬的請求,所以大部分的請求會失敗。只需要處理好鎖的問題即可。
我為了減少競爭,將所有的用戶分在了不同的桶里。這樣可以減少對鎖的競爭。如果以后還有更高的性能要求,還可以使用高性能隊(duì)列——Disruptor來進(jìn)一步提高性能。
注意,在我的測試環(huán)境里是缺少支付這個(gè)核心服務(wù)的,所以實(shí)現(xiàn)的難度是大大地減輕了。另外提供一組數(shù)字:2016年淘寶的雙11的交易峰值僅僅為12萬/秒,微信紅包分發(fā)速度是5萬/秒,要做到這點(diǎn)是非常困難的。
參考鏈接:
http://mt.sohu.com/20161111/n472951708.shtml
參考鏈接:
https://github.com/xiaojiaqi/fakewechat
監(jiān)控顯示日志大概這樣:

代碼實(shí)現(xiàn)及分析
在代碼方面,使用到的技巧實(shí)在不多,主要是設(shè)計(jì)思想和Golang本身的一些問題需要考慮。
首先Golang的goroutine的數(shù)目控制,因?yàn)橹辽儆?00萬以上的連接,所以按照普通的設(shè)計(jì)方案,至少需要200萬或者300萬的goroutine在工作。這會造成系統(tǒng)本身的負(fù)擔(dān)很重。
其次就是100萬個(gè)連接的管理,無論是連接還是業(yè)務(wù)都會造成一些心智的負(fù)擔(dān)。
我的設(shè)計(jì)是這樣的:
架構(gòu)圖
首先將100萬連接分成多個(gè)不同的SET,每個(gè)SET是一個(gè)獨(dú)立、平行的對象。每個(gè)SET只管理幾千個(gè)連接,如果單個(gè)SET工作正常,我只需要添加SET就能提高系統(tǒng)處理能力。按照SET分還有一個(gè)好處,可以將一個(gè)SET作為一個(gè)業(yè)務(wù)單元,在不同性能服務(wù)器上可以負(fù)載不同的壓力,比如8核機(jī)器管理10個(gè)SET,4核機(jī)器管理5個(gè)SET 可以細(xì)粒度的分流壓力,并容易遷移處理。
其次謹(jǐn)慎地設(shè)計(jì)了每個(gè)SET里數(shù)據(jù)結(jié)構(gòu)的大小,保證每個(gè)SET的壓力不會太大,不會出現(xiàn)消息的堆積。
再次減少了gcroutine的數(shù)目,每個(gè)連接只使用一個(gè)goroutine,發(fā)送消息在一個(gè)SET里只有一個(gè)gcroutine負(fù)責(zé),這樣節(jié)省了100萬個(gè)goroutine。這樣整個(gè)系統(tǒng)只需要保留 100萬零幾百個(gè)gcroutine就能完成業(yè)務(wù)。大量的節(jié)省了cpu 和內(nèi)存。
系統(tǒng)的工作流程大概是:每個(gè)客戶端連接成功后,系統(tǒng)會分配一個(gè)goroutine讀取客戶端的消息,當(dāng)消息讀取完成,將它轉(zhuǎn)化為消息對象放至在SET的接收消息隊(duì)列,然后返回獲取下一個(gè)消息。
在SET內(nèi)部,有一個(gè)工作goroutine,它只做非常簡單而高效的事情,它做的事情如下,檢查SET的接受消息,它會收到3類消息:
客戶端的搖紅包請求消息;
客戶端的其他消息,比如聊天好友這一類;
服務(wù)器端對客戶端消息的回應(yīng)。
對于第1種消息是這樣處理的,從客戶端拿到搖紅包請求消息,試圖從SET的紅包隊(duì)列里獲取一個(gè)紅包,如果拿到了就把紅包信息返回給客戶端,否則構(gòu)造一個(gè)沒有搖到的消息,返回給對應(yīng)的客戶端。
對于第2種消息,只需簡單地從隊(duì)列里拿走消息,轉(zhuǎn)發(fā)給后端的聊天服務(wù)隊(duì)列即可,其他服務(wù)會把消息轉(zhuǎn)發(fā)出去。
對于第3種消息,SET 只需要根據(jù)消息里的用戶id,找到SET里保留的用戶連接對象,發(fā)回去就可以了。
對于紅包產(chǎn)生服務(wù),它的工作很簡單,只需要按照順序輪流在每個(gè)SET的紅包產(chǎn)生隊(duì)列里放置紅包對象就可以了。這樣可以保證每個(gè)SET里都是公平的,其次它的工作強(qiáng)度很低,可以保證業(yè)務(wù)穩(wěn)定。
參考鏈接:
https://github.com/xiaojiaqi/10billionhongbaos
實(shí) 踐
實(shí)踐的過程分為3個(gè)階段。
| 階段1
命令如下:
Alias ss2=Ss –ant | grep 1025 | grep EST | awk –F: “{print \$8}” | sort | uniq –c’結(jié)果如下:


觀察網(wǎng)絡(luò)監(jiān)控和監(jiān)控端反饋,發(fā)現(xiàn)QPS 達(dá)到預(yù)期數(shù)據(jù),網(wǎng)絡(luò)監(jiān)控截圖:
在服務(wù)器端啟動一個(gè)產(chǎn)生紅包的服務(wù),這個(gè)服務(wù)會以200個(gè)每秒的速度下發(fā)紅包,總共4萬個(gè)。此時(shí)觀察客戶端在監(jiān)控上的日志,會發(fā)現(xiàn)基本上以200個(gè)每秒的速度獲取到紅包。

等到所有紅包下發(fā)完成后,再啟動一個(gè)發(fā)紅包的服務(wù),這個(gè)服務(wù)系統(tǒng)會生成2萬個(gè)紅包,每秒也是200個(gè),每個(gè)紅包隨機(jī)指定3位用戶,并向這3個(gè)用戶發(fā)出消息,客戶端會自動來拿紅包,最后所有的紅包都被拿走。

| 階段3
如法炮制,在服務(wù)器端,啟動一個(gè)產(chǎn)生紅包的服務(wù),這個(gè)服務(wù)會以200個(gè)每秒的速度下發(fā)紅包,總共4萬個(gè)。此時(shí)觀察客戶端在監(jiān)控上的日志,會發(fā)現(xiàn)基本上以200個(gè)每秒的速度獲取到紅包。
等到所有紅包下發(fā)完成后,再啟動一個(gè)發(fā)紅包的服務(wù),這個(gè)服務(wù)系統(tǒng)會生成2萬個(gè)紅包,每秒也是200個(gè),每個(gè)紅包隨機(jī)指定3位用戶,并向這3個(gè)用戶發(fā)出消息,客戶端會自動來拿紅包,最后所有的紅包都被拿走。
最后,實(shí)踐完成。
分析數(shù)據(jù)
在實(shí)踐過程中,服務(wù)器和客戶端都將自己內(nèi)部的計(jì)數(shù)器記錄發(fā)往監(jiān)控端,成為了日志。我們利用簡單python 腳本和gnuplt 繪圖工具,將實(shí)踐的過程可視化,由此來驗(yàn)證運(yùn)行過程。
第一張是客戶端的QPS發(fā)送數(shù)據(jù):

這張圖的橫坐標(biāo)是時(shí)間,單位是秒,縱坐標(biāo)是QPS,表示這時(shí)刻所有客戶端發(fā)送的請求的QPS。
圖的第一區(qū)間,幾個(gè)小的峰值,是100萬客戶端建立連接的, 圖的第二區(qū)間是3萬QPS 區(qū)間,我們可以看到數(shù)據(jù)比較穩(wěn)定地保持在3萬這個(gè)區(qū)間。最后是6萬QPS區(qū)間。但是從整張圖可以看到QPS不是完美地保持在我們希望的直線上。這主要是以下幾個(gè)原因造成的:
①?當(dāng)非常多goroutine 同時(shí)運(yùn)行的時(shí)候,依靠sleep 定時(shí)并不準(zhǔn)確,發(fā)生了偏移。我覺得這是golang本身調(diào)度導(dǎo)致的。當(dāng)然如果cpu比較強(qiáng)勁,這個(gè)現(xiàn)象會消失。
②?因?yàn)榫W(wǎng)絡(luò)的影響,客戶端在發(fā)起連接時(shí),可能發(fā)生延遲,導(dǎo)致在前1秒沒有完成連接。
③?服務(wù)器負(fù)載較大時(shí),1000M網(wǎng)絡(luò)已經(jīng)出現(xiàn)了丟包現(xiàn)象,可以通過ifconfig 命令觀察到這個(gè)現(xiàn)象,所以會有QPS的波動。
第二張是服務(wù)器處理的QPS圖:

和客戶端相對應(yīng),服務(wù)器也存在3個(gè)區(qū)間,和客戶端的情況很接近。但是我們看到了在大概22:57分,系統(tǒng)的處理能力就有一個(gè)明顯的下降,隨后又提高的尖狀。這說明代碼還需要優(yōu)化。另外,搜索公眾號互聯(lián)網(wǎng)架構(gòu)師后臺回復(fù)“2T”,獲取一份驚喜禮包。
整體觀察可以發(fā)現(xiàn),在3萬QPS區(qū)間,服務(wù)器的QPS比較穩(wěn)定,在6萬QSP時(shí)候,服務(wù)器的處理就不穩(wěn)定了。我相信這和我的代碼有關(guān),如果繼續(xù)優(yōu)化的話,還應(yīng)該能有更好的效果。
將2張圖合并起來 :

基本是吻合的,這也證明系統(tǒng)是符合預(yù)期設(shè)計(jì)的。
這是紅包生成數(shù)量的狀態(tài)變化圖:

非常穩(wěn)定。
這是客戶端每秒獲取的搖紅包狀態(tài):

可以發(fā)現(xiàn)3萬QPS區(qū)間,客戶端每秒獲取的紅包數(shù)基本在200左右,在6萬QPS的時(shí)候,以及出現(xiàn)劇烈的抖動,不能保證在200這個(gè)數(shù)值了。我覺得主要是6萬QPS時(shí)候,網(wǎng)絡(luò)的抖動加劇了,造成了紅包數(shù)目也在抖動。
最后是Golang 自帶的pprof 信息,其中有g(shù)c 時(shí)間超過了10ms, 考慮到這是一個(gè)7年前的硬件,而且非獨(dú)占模式,所以還是可以接受。

總 結(jié)
按照設(shè)計(jì)目標(biāo),我們模擬和設(shè)計(jì)了一個(gè)支持100萬用戶,并且每秒至少可以支持3萬QPS,最多6萬QPS的系統(tǒng),簡單模擬了微信的搖紅包和發(fā)紅包的過程。可以說達(dá)到了預(yù)期的目的。
如果600臺主機(jī)每臺主機(jī)可以支持6萬QPS,只需要7分鐘就可以完成100億次搖紅包請求。
雖然這個(gè)原型簡單地完成了預(yù)設(shè)的業(yè)務(wù),但是它和真正的服務(wù)會有哪些差別呢?我羅列了一下:

參考資料
單機(jī)百萬的實(shí)踐
https://github.com/xiaojiaqi/C1000kPracticeGuide
如何在AWS上進(jìn)行100萬用戶壓力
https://github.com/xiaojiaqi/fakewechat/wiki/Stress-Testing-in-the-Cloud
構(gòu)建一個(gè)你自己的類微信系統(tǒng)
https://github.com/xiaojiaqi/fakewechat/wiki/Design
http://techblog.cloudperf.net/2016/05/2-million-packets-per-second-on-public.html
@火丁筆記
? ? ? ? ? ?http://huoding.com/2013/10/30/296
https://gobyexample.com/non-blocking-channel-operations
相關(guān)閱讀:2T架構(gòu)師學(xué)習(xí)資料干貨分享
全棧架構(gòu)社區(qū)交流群
?「全棧架構(gòu)社區(qū)」建立了讀者架構(gòu)師交流群,大家可以添加小編微信進(jìn)行加群。歡迎有想法、樂于分享的朋友們一起交流學(xué)習(xí)。
看完本文有收獲?請轉(zhuǎn)發(fā)分享給更多人
往期資源:
