流量拆分:如何通過架構(gòu)設(shè)計(jì)緩解流量壓力?
今天,我打算以直播互動作為例子,引領(lǐng)大家一同去了解在面對讀多寫多的情況時,應(yīng)當(dāng)怎樣去應(yīng)對所產(chǎn)生的流量壓力。通常而言,這類服務(wù)在多數(shù)情況下都屬于實(shí)時互動服務(wù)。由于其對時效性有著極高的要求,這就致使在許多場景當(dāng)中,我們沒辦法借助讀緩存的方式來減輕核心數(shù)據(jù)所承受的壓力。
那么,為了有效降低這類互動服務(wù)器所面臨的壓力,我們能夠從架構(gòu)層面著手,開展一些具有靈活性的拆分操作,并對其進(jìn)行相應(yīng)的設(shè)計(jì)改造。
實(shí)際上,這些設(shè)計(jì)是通過混合的方式來實(shí)現(xiàn)對外提供服務(wù)的。為了能夠讓大家更為清晰地理解這其中的原理,我將會針對直播互動里特定的一些場景展開詳細(xì)講解。
一般來講,直播場景是可以被劃分成兩種不同類型的,即可以預(yù)估用戶量的場景以及不可預(yù)估用戶量的場景。這兩種場景下的設(shè)計(jì)存在著很大的差異,接下來,我們就分別對它們進(jìn)行深入的探討。
可預(yù)估用戶量的服務(wù):游戲創(chuàng)建房間
想必不少熱衷于對戰(zhàn)游戲的小伙伴都曾有過這樣的經(jīng)歷:在聯(lián)網(wǎng)玩游戲時,首先得創(chuàng)建房間才行。這種游戲設(shè)計(jì)方式呢,主要是依靠設(shè)定一臺服務(wù)器所能開啟的房間數(shù)量上限,以此來對一臺服務(wù)器可同時服務(wù)的用戶數(shù)量加以限制。
接下來,咱們從服務(wù)器端的資源分配這個角度出發(fā),深入剖析一下創(chuàng)建房間這一設(shè)計(jì)究竟是怎樣進(jìn)行資源調(diào)配的。
當(dāng)房間創(chuàng)建完成之后,用戶憑借房間號便能夠邀請其他伙伴加入游戲,從而展開對戰(zhàn)。無論是房主呢,還是后續(xù)加入的伙伴,都會依據(jù)房間的標(biāo)識,由調(diào)度服務(wù)統(tǒng)一安排到同一服務(wù)集群之上,進(jìn)而開展互動交流。
這里得給大家提個醒哦,開房間這個動作并非一定要由游戲用戶親自去完成呀,我們完全可以將其設(shè)置成用戶開啟游戲之時就自動為其分配房間。如此一來,不但能夠提前對用戶量進(jìn)行預(yù)估,而且還能極為出色地對我們的服務(wù)資源加以規(guī)劃與掌控呢。
那么,要怎樣去評估一臺服務(wù)器能夠支持多少人同時在線呢?其實(shí)呀,我們可以通過壓力測試的方法,測出單臺服務(wù)器所能服務(wù)的在線人數(shù),進(jìn)而依據(jù)這個數(shù)據(jù)來精確地預(yù)估所需要的帶寬以及服務(wù)器資源,從而算出一個集群(要知道,這個集群可是包含了若干臺服務(wù)器哦)究竟需要多少資源,又能夠承擔(dān)多少人在線開展互動活動。最后呢,再借助調(diào)度服務(wù)來對資源進(jìn)行分配,把新來的房主分配到尚有空閑的服務(wù)集群當(dāng)中。
下面給大家展示一下最終的實(shí)現(xiàn)效果:
圖片
就像前面所展示的那樣,在創(chuàng)建房間的這個階段呀,我們的客戶端在進(jìn)入?yún)^(qū)域服務(wù)器集群之前呢,都是依靠向調(diào)度服務(wù)發(fā)起請求,進(jìn)而由調(diào)度服務(wù)來完成相應(yīng)調(diào)度工作的。
調(diào)度服務(wù)器會按照一定的周期,去接收來自各個組服務(wù)器的服務(wù)用戶在線情況方面的信息哦。通過對這些信息的分析與處理,調(diào)度服務(wù)器就能夠評估出究竟需要調(diào)配多少用戶進(jìn)入到不同的區(qū)域集群當(dāng)中啦。
與此同時呢,客戶端在收到調(diào)度指令之后呀,會拿著調(diào)度服務(wù)所給予的 token,前往不同的區(qū)域去申請創(chuàng)建房間呢。
等到房間成功創(chuàng)建之后呀,調(diào)度服務(wù)就會在本地的集群內(nèi)部,對這個房間的列表以及相關(guān)信息進(jìn)行維護(hù)管理哦。這些信息呢,還會提供給其他那些想要加入游戲的玩家進(jìn)行查看展示呢。
而那些后續(xù)加入游戲的玩家呀,同樣也會接入到對應(yīng)房間所在的區(qū)域服務(wù)器當(dāng)中,從而能夠和房主以及同房間的其他玩家開展實(shí)時的互動交流呢。這種通過限定配額房間個數(shù)的方式來進(jìn)行服務(wù)器資源調(diào)度的設(shè)計(jì)呀,可不單單是在對戰(zhàn)游戲里面才會用到哦,在很多其他的場景當(dāng)中呀,也都采用了類似的設(shè)計(jì)呢,就比如說在線小課堂這類涉及教學(xué)互動的場景呀。
我們完全可以預(yù)見到呀,通過采用這樣的設(shè)計(jì)呢,就能夠?qū)Y源實(shí)現(xiàn)精準(zhǔn)的把控啦,如此一來,用戶的數(shù)量也就不會超出我們服務(wù)器所設(shè)計(jì)的容量范圍啦。
不可預(yù)估用戶量的服務(wù)
然而呢,在很多場景當(dāng)中,情況是具有隨機(jī)性的,我們根本沒辦法確切地把握會有多少用戶進(jìn)入到這個服務(wù)器來進(jìn)行互動交流。就拿全國直播來說吧,根本就無法確定究竟會有多少用戶來訪問呀。
鑒于這種情況呢,很多直播服務(wù)首先會依據(jù)主播過往的情況來預(yù)測用戶量哦。通過對這個預(yù)估量的分析呢,提前把他們的直播安排到相對比較空閑的服務(wù)器群組里面。同時呢,還會提前準(zhǔn)備好一些調(diào)度工具哦,比如說通過控制曝光度的方式來延緩用戶進(jìn)入直播。通過這樣的操作呢,就能夠?yàn)榉?wù)器調(diào)度爭取到更多的時間,以便進(jìn)行動態(tài)擴(kuò)容啦。
由于這一類服務(wù)沒辦法預(yù)估到底會有多少用戶,所以之前那種服務(wù)器小組的模式呀,并不適用于這種情況哦,而是需要更高級別的調(diào)度呢。下面我們來分析一下這個場景哦。對于直播而言呢,用戶常見的交互形式包含了聊天、答題、點(diǎn)贊、打賞以及購物等等??紤]到這些交互形式各自具有不同的特點(diǎn),我們接下來針對不同的關(guān)鍵點(diǎn)依次進(jìn)行分析。
聊天:信息合并
聊天的內(nèi)容普遍比較短,為了提高吞吐能力,通常會把用戶的聊天內(nèi)容放入分布式隊(duì)列做傳輸,這樣能延緩寫入壓力。另外,在點(diǎn)贊或大量用戶輸入同樣內(nèi)容的刷屏情境下,我們可以通過大數(shù)據(jù)實(shí)時計(jì)算分析用戶的輸入,并壓縮整理大量重復(fù)的內(nèi)容,過濾掉一些無用信息。

壓縮整理后的聊天內(nèi)容會被分發(fā)到多個聊天內(nèi)容分發(fā)服務(wù)器上,直播間內(nèi)用戶的聊天長連接會收到消息更新的推送通知,接著客戶端會到指定的內(nèi)容分發(fā)服務(wù)器群組里批量拉取數(shù)據(jù),拿到數(shù)據(jù)后會根據(jù)時間順序來回放。請注意,這個方式只適合用在瘋狂刷屏的情況,如果用戶量很少可以通過長鏈接進(jìn)行實(shí)時互動。
答題:瞬時信息拉取高峰
除了交互流量極大的聊天互動信息之外,還存在一些特殊的互動形式,比如做題互動。在直播間里,當(dāng)老師發(fā)送一個題目時,題目消息會被廣播給所有用戶,而客戶端收到消息后會從服務(wù)端拉取題目的數(shù)據(jù)。
想象一下,如果有 10 萬用戶同時在線,那么很有可能會出現(xiàn)瞬間有 10 萬人在線同時請求服務(wù)端拉取題目的情況。如此龐大的數(shù)據(jù)請求量,若要承受得住,就需要我們投入大量的服務(wù)器和帶寬資源,但這樣做的性價比其實(shí)并不高。
從理論上來說,我們可以將數(shù)據(jù)靜態(tài)化,并通過 CDN 來阻擋這個流量。然而,為了避免出現(xiàn)瞬時的高峰情況,推薦在客戶端拉取時加入隨機(jī)延遲幾秒的操作,然后再發(fā)送請求。這樣做能夠大大延緩服務(wù)器的壓力,從而獲得更好的用戶體驗(yàn)。
請務(wù)必牢記,對于客戶端而言,如果這種服務(wù)請求失敗了,就不要頻繁地進(jìn)行請求重試,否則會將服務(wù)端 “打沉”。如果確實(shí)必須要進(jìn)行重試,那么建議對重試的時間采用退火算法。通過這樣的方式,可以保證服務(wù)端不會因?yàn)橐粫r的故障而收到大量的請求,進(jìn)而避免服務(wù)器崩潰。
如果是在教學(xué)場景的直播中,有兩個可以緩解服務(wù)器壓力的技巧。
第一個技巧是在上課當(dāng)天,把搶答題目提前交給客戶端做預(yù)加載下載。這樣一來,就能夠減少實(shí)時拉取的壓力。
第二個方式是針對題目搶答的情況。當(dāng)老師發(fā)布題目的時候,提前設(shè)定發(fā)送動作生效后 5 秒再彈出題目。如此操作,能夠讓所有直播用戶的接收端 “準(zhǔn)時” 地收到題目信息,而不至于出現(xiàn)用戶題目接收時間不一致的情況。
至于非搶答類型的題目,當(dāng)用戶回答完題目后,我們可以先在客戶端本地先做預(yù)判卷,把正確答案和解析展示給用戶。然后,在直播期間異步緩慢地將用戶答題結(jié)果提交到服務(wù)端。通過這樣的方式,能夠保證服務(wù)器不會因用戶瞬時的流量而被沖垮。
點(diǎn)贊:客戶端互動合并
接下來,針對點(diǎn)贊的場景,我打算從客戶端以及服務(wù)端這兩個不同的角度來為大家詳細(xì)介紹一下。
咱們先來看客戶端這邊的情況。在很多時候呀,客戶端其實(shí)并不需要實(shí)時地去提交用戶所做出的全部交互動作哦。這是因?yàn)檠?,有不少交互動作屬于那種機(jī)械性的重復(fù)動作,它們對于實(shí)時性的要求并沒有那么高呢。
給大家舉個例子吧,比如說用戶在本地特別快速地連續(xù)點(diǎn)擊了 100 下贊,在這種情況下呢,客戶端就完全可以把這些點(diǎn)贊操作合并起來,將其轉(zhuǎn)化為一條消息進(jìn)行處理呀,就好比是 “用戶在 3 秒內(nèi)點(diǎn)贊 10 次” 這樣的表述形式。
我相信呀,像大家這么聰明的人,肯定能夠把這種將互動動作進(jìn)行合并的小妙招運(yùn)用到更多的情景當(dāng)中去哦。比如說,當(dāng)用戶連續(xù)打賞 100 個禮物的時候,同樣也可以采用這樣的方式來處理呀。
通過運(yùn)用這種方式呢,能夠極大幅度地降低服務(wù)器所承受的壓力哦。這樣一來呀,既可以確保直播間依舊保持那種火爆的氛圍,同時呢,還能夠節(jié)省下大量的流量資源呢,這可真是一件一舉多得的好事呀,大家何樂而不為呢?
點(diǎn)贊:服務(wù)端樹形多層匯總架構(gòu)
我們回頭再看看點(diǎn)贊的場景下,如何設(shè)計(jì)服務(wù)端才能緩解請求壓力。如果我們的集群 QPS 超過十萬,服務(wù)端數(shù)據(jù)層已經(jīng)無法承受這樣的壓力時,如何應(yīng)對高并發(fā)寫、高并發(fā)讀呢?微博做過一個類似的案例,用途是緩解用戶的點(diǎn)贊請求流量,這種方式適合一致性要求不高的計(jì)數(shù)器,如下圖所示:
圖片
這個方式可以將用戶點(diǎn)贊流量隨機(jī)壓到不同的寫緩存服務(wù)上,通過第一層寫緩存本地的實(shí)時匯總來緩解大量用戶的請求,將更新數(shù)據(jù)周期性地匯總后,提交到二級寫緩存。之后,二級匯總所在分片的所有上層服務(wù)數(shù)值后,最終匯總同步給核心緩存服務(wù)。接著,通過核心緩存把最終結(jié)果匯總累加起來。最后通過主從復(fù)制到多個子查詢節(jié)點(diǎn)服務(wù),供用戶查詢匯總結(jié)果。
打賞 & 購物:服務(wù)端分片及分片實(shí)時擴(kuò)容
前面的互動只要保證最終一致性就可以,但打賞和購物的場景下,庫存和金額需要提供事務(wù)一致性的服務(wù)。因?yàn)槭聞?wù)一致性的要求,這種服務(wù)我們不能做成多層緩沖方式提供服務(wù),而且這種服務(wù)的數(shù)據(jù)特征是讀多寫多,所以我們可以通過數(shù)據(jù)分片方式實(shí)現(xiàn)這一類服務(wù),如下圖:
圖片
看過圖之后,是不是感覺理解起來輕松多了呀?下面我再詳細(xì)說一說哦。
我們可以依據(jù)用戶的 id 來進(jìn)行 hash 拆分操作呢。具體做法是,通過網(wǎng)關(guān)把不同用戶的 uid 進(jìn)行取模處理,然后按照取模所得的數(shù)值范圍,將用戶分配到不同的分片服務(wù)上去。之后呢,處于各個分片內(nèi)的服務(wù)就會針對類似的請求開展內(nèi)存實(shí)時計(jì)算更新的工作啦。
通過采用這樣的方式呀,能夠較為快速且便捷地實(shí)現(xiàn)負(fù)載的切分哦。不過呢,這種 hash 分配的方式也存在一定的弊端哦,那就是容易出現(xiàn)個別熱點(diǎn)的情況呢。當(dāng)我們面臨的流量壓力大到服務(wù)器扛不住的時候呀,就需要對服務(wù)器進(jìn)行擴(kuò)容處理啦。
而且呀,要是采用 hash 這種方式,一旦出現(xiàn)個別服務(wù)器發(fā)生故障的情況,就會導(dǎo)致 hash 映射出現(xiàn)錯誤哦,這樣一來,請求就可能會被發(fā)送到錯誤的分片上去啦。
針對這些問題呀,其實(shí)是有很多類似的解決方案的哦。比如說一致性 hash 算法吧,這種算法的優(yōu)勢在于它可以針對局部的區(qū)域進(jìn)行擴(kuò)容操作,而且不會對整個集群的分片造成影響哦。但是呢,這個方法在很多時候呀,由于其算法本身不夠通用,并且無法由人來進(jìn)行有效控制,所以使用起來就會顯得特別麻煩呢,還需要專門開發(fā)配套的工具才行哦。
除此之外呀,我再給大家推薦另外一種方式哦,那就是樹形熱遷移切片法啦。這是一種類似于虛擬桶的方式哦。打個比方來說吧,我們可以把全量數(shù)據(jù)拆分成 256 份呀,每一份就代表一個桶哦。假如有 16 個服務(wù)器的話,那么每個服務(wù)器就可以分到 16 個桶啦。
當(dāng)我們發(fā)現(xiàn)個別服務(wù)器的壓力過大的時候呀,就可以給這個服務(wù)器增加兩個訂閱服務(wù)器哦,讓它們?nèi)プ鲋鲝耐降墓ぷ髂?,也就是把這個服務(wù)器上的 16 個桶的數(shù)據(jù)進(jìn)行遷移操作啦。
等到同步遷移的工作成功完成之后呀,就可以把原本發(fā)送到這個服務(wù)器的請求流量進(jìn)行拆分處理啦,然后分別轉(zhuǎn)發(fā)到兩個各有 8 個桶的服務(wù)器上去哦。之后呢,就讓這兩個訂閱服務(wù)器分別接收請求并繼續(xù)對外提供服務(wù)啦,而原來那個壓力過大的服務(wù)器呢,就可以把它摘除并進(jìn)行回收處理啦。
在服務(wù)成功完成切換之后呀,因?yàn)檫M(jìn)行的是全量遷移,所以這兩個新的服務(wù)會同時同步到原本并不屬于它們各自的 8 個桶的數(shù)據(jù)哦。在這種情況下呢,新服務(wù)器只需要去遍歷自己所存儲的數(shù)據(jù),然后把那些不屬于自己的數(shù)據(jù)給刪除掉就可以啦。
當(dāng)然啦,還有另外一種做法哦,那就是在同步來自 16 桶服務(wù)的數(shù)據(jù)的時候呢,就直接把那些不屬于自身的相關(guān)數(shù)據(jù)給過濾掉呀。需要說明的是,這個方法對于 Redis、MySQL 等所有存在有狀態(tài)分片數(shù)據(jù)的服務(wù)來說,都是適用的哦。
不過呢,這個服務(wù)存在一個難點(diǎn)哦,那就是請求的客戶端并不會直接去請求分片哦,而是要通過代理服務(wù)來對數(shù)據(jù)服務(wù)發(fā)起請求呢。只有借助代理服務(wù)呀,才能夠?qū)崿F(xiàn)對調(diào)度流量進(jìn)行動態(tài)更新,進(jìn)而達(dá)到平滑且無損地轉(zhuǎn)發(fā)流量的目的哦。
最后呀,咱們再來探討一下這樣一個問題哦,那就是如何讓客戶端知道應(yīng)該去請求哪個分片才能夠找到它所需要的數(shù)據(jù)呢?在這里呀,我給大家分享兩種比較常見的方式哦。
第一種方式是這樣的哦,客戶端可以通過特定的算法來找到分片哦。比如說呢,可以采用這樣的算法:用戶 hash (uid) % 100 = 桶 id 哦。然后呢,在配置文件當(dāng)中,通過這個桶 id 就能夠找到與之對應(yīng)的分片啦。
第二種方式則是呢,當(dāng)數(shù)據(jù)服務(wù)端接收到客戶端的請求之后呀,會把這個請求轉(zhuǎn)發(fā)到存有相關(guān)數(shù)據(jù)的分片那里哦。比如說吧,客戶端一開始請求的是 A 分片,然后再根據(jù)相應(yīng)的數(shù)據(jù)算法以及對應(yīng)的分片配置,發(fā)現(xiàn)所需要的數(shù)據(jù)其實(shí)是在 B 分片那里哦。這個時候呢,A 分片就會把這個請求轉(zhuǎn)發(fā)到 B 分片哦。等到 B 分片處理完這個請求之后呢,就會把數(shù)據(jù)返回給客戶端啦(這里的數(shù)據(jù)返回方式呢,是由 A 返回還是由 B 返回,這就要取決于客戶端是進(jìn)行跳轉(zhuǎn)操作還是由服務(wù)端來進(jìn)行轉(zhuǎn)發(fā)操作啦)。
服務(wù)降級:分布式隊(duì)列匯總緩沖
即使通過這么多技術(shù)來優(yōu)化架構(gòu),我們的服務(wù)仍舊無法完全承受過高的瞬發(fā)流量。對于這種情況,我們可以做一些服務(wù)降級的操作,通過隊(duì)列將修改合并或做網(wǎng)關(guān)限流。雖然這會犧牲一些實(shí)時性,但是實(shí)際上,很多數(shù)字可能沒有我們想象中那么重要。像微博的點(diǎn)贊統(tǒng)計(jì)數(shù)據(jù),如果客戶端點(diǎn)贊無法請求到服務(wù)器,那么這些數(shù)據(jù)會在客戶端暫存一段時間,在用戶看數(shù)據(jù)時看到的只是短期歷史數(shù)字,不是實(shí)時數(shù)字。十萬零五的點(diǎn)贊數(shù)跟十萬零三千的點(diǎn)贊數(shù),差異并不大,等之后服務(wù)器有空閑了,結(jié)果追上來最終是一致的。但作為降級方案,這么做能節(jié)省大量的服務(wù)器資源,也算是個好方法。



























