流量拆分:如何通過架構設計緩解流量壓力?

今天,我打算以直播互動作爲例子,引領大家一同去了解在面對讀多寫多的情況時,應當怎樣去應對所產生的流量壓力。通常而言,這類服務在多數情況下都屬於實時互動服務。由於其對時效性有着極高的要求,這就致使在許多場景當中,我們沒辦法藉助讀緩存的方式來減輕核心數據所承受的壓力。

那麼,爲了有效降低這類互動服務器所面臨的壓力,我們能夠從架構層面着手,開展一些具有靈活性的拆分操作,並對其進行相應的設計改造。

實際上,這些設計是通過混合的方式來實現對外提供服務的。爲了能夠讓大家更爲清晰地理解這其中的原理,我將會針對直播互動裏特定的一些場景展開詳細講解。

一般來講,直播場景是可以被劃分成兩種不同類型的,即可以預估用戶量的場景以及不可預估用戶量的場景。這兩種場景下的設計存在着很大的差異,接下來,我們就分別對它們進行深入的探討。

可預估用戶量的服務:遊戲創建房間

想必不少熱衷於對戰遊戲的小夥伴都曾有過這樣的經歷:在聯網玩遊戲時,首先得創建房間纔行。這種遊戲設計方式呢,主要是依靠設定一臺服務器所能開啓的房間數量上限,以此來對一臺服務器可同時服務的用戶數量加以限制。

接下來,咱們從服務器端的資源分配這個角度出發,深入剖析一下創建房間這一設計究竟是怎樣進行資源調配的。

當房間創建完成之後,用戶憑藉房間號便能夠邀請其他夥伴加入遊戲,從而展開對戰。無論是房主呢,還是後續加入的夥伴,都會依據房間的標識,由調度服務統一安排到同一服務集羣之上,進而開展互動交流。

這裏得給大家提個醒哦,開房間這個動作並非一定要由遊戲用戶親自去完成呀,我們完全可以將其設置成用戶開啓遊戲之時就自動爲其分配房間。如此一來,不但能夠提前對用戶量進行預估,而且還能極爲出色地對我們的服務資源加以規劃與掌控呢。

那麼,要怎樣去評估一臺服務器能夠支持多少人同時在線呢?其實呀,我們可以通過壓力測試的方法,測出單臺服務器所能服務的在線人數,進而依據這個數據來精確地預估所需要的帶寬以及服務器資源,從而算出一個集羣(要知道,這個集羣可是包含了若干臺服務器哦)究竟需要多少資源,又能夠承擔多少人在線開展互動活動。最後呢,再借助調度服務來對資源進行分配,把新來的房主分配到尚有空閒的服務集羣當中。

下面給大家展示一下最終的實現效果:

就像前面所展示的那樣,在創建房間的這個階段呀,我們的客戶端在進入區域服務器集羣之前呢,都是依靠向調度服務發起請求,進而由調度服務來完成相應調度工作的。

調度服務器會按照一定的週期,去接收來自各個組服務器的服務用戶在線情況方面的信息哦。通過對這些信息的分析與處理,調度服務器就能夠評估出究竟需要調配多少用戶進入到不同的區域集羣當中啦。

與此同時呢,客戶端在收到調度指令之後呀,會拿着調度服務所給予的 token,前往不同的區域去申請創建房間呢。

等到房間成功創建之後呀,調度服務就會在本地的集羣內部,對這個房間的列表以及相關信息進行維護管理哦。這些信息呢,還會提供給其他那些想要加入遊戲的玩家進行查看展示呢。

而那些後續加入遊戲的玩家呀,同樣也會接入到對應房間所在的區域服務器當中,從而能夠和房主以及同房間的其他玩家開展實時的互動交流呢。這種通過限定配額房間個數的方式來進行服務器資源調度的設計呀,可不單單是在對戰遊戲裏面纔會用到哦,在很多其他的場景當中呀,也都採用了類似的設計呢,就比如說在線小課堂這類涉及教學互動的場景呀。

我們完全可以預見到呀,通過採用這樣的設計呢,就能夠對資源實現精準的把控啦,如此一來,用戶的數量也就不會超出我們服務器所設計的容量範圍啦。

不可預估用戶量的服務

然而呢,在很多場景當中,情況是具有隨機性的,我們根本沒辦法確切地把握會有多少用戶進入到這個服務器來進行互動交流。就拿全國直播來說吧,根本就無法確定究竟會有多少用戶來訪問呀。

鑑於這種情況呢,很多直播服務首先會依據主播過往的情況來預測用戶量哦。通過對這個預估量的分析呢,提前把他們的直播安排到相對比較空閒的服務器羣組裏面。同時呢,還會提前準備好一些調度工具哦,比如說通過控制曝光度的方式來延緩用戶進入直播。通過這樣的操作呢,就能夠爲服務器調度爭取到更多的時間,以便進行動態擴容啦。

由於這一類服務沒辦法預估到底會有多少用戶,所以之前那種服務器小組的模式呀,並不適用於這種情況哦,而是需要更高級別的調度呢。下面我們來分析一下這個場景哦。對於直播而言呢,用戶常見的交互形式包含了聊天、答題、點贊、打賞以及購物等等。考慮到這些交互形式各自具有不同的特點,我們接下來針對不同的關鍵點依次進行分析。

聊天:信息合併

聊天的內容普遍比較短,爲了提高吞吐能力,通常會把用戶的聊天內容放入分佈式隊列做傳輸,這樣能延緩寫入壓力。另外,在點贊或大量用戶輸入同樣內容的刷屏情境下,我們可以通過大數據實時計算分析用戶的輸入,並壓縮整理大量重複的內容,過濾掉一些無用信息。

壓縮整理後的聊天內容會被分發到多個聊天內容分發服務器上,直播間內用戶的聊天長連接會收到消息更新的推送通知,接着客戶端會到指定的內容分發服務器羣組裏批量拉取數據,拿到數據後會根據時間順序來回放。請注意,這個方式只適合用在瘋狂刷屏的情況,如果用戶量很少可以通過長鏈接進行實時互動。

答題:瞬時信息拉取高峯

除了交互流量極大的聊天互動信息之外,還存在一些特殊的互動形式,比如做題互動。在直播間裏,當老師發送一個題目時,題目消息會被廣播給所有用戶,而客戶端收到消息後會從服務端拉取題目的數據。

想象一下,如果有 10 萬用戶同時在線,那麼很有可能會出現瞬間有 10 萬人在線同時請求服務端拉取題目的情況。如此龐大的數據請求量,若要承受得住,就需要我們投入大量的服務器和帶寬資源,但這樣做的性價比其實並不高。

從理論上來說,我們可以將數據靜態化,並通過 CDN 來阻擋這個流量。然而,爲了避免出現瞬時的高峯情況,推薦在客戶端拉取時加入隨機延遲幾秒的操作,然後再發送請求。這樣做能夠大大延緩服務器的壓力,從而獲得更好的用戶體驗。

請務必牢記,對於客戶端而言,如果這種服務請求失敗了,就不要頻繁地進行請求重試,否則會將服務端 “打沉”。如果確實必須要進行重試,那麼建議對重試的時間採用退火算法。通過這樣的方式,可以保證服務端不會因爲一時的故障而收到大量的請求,進而避免服務器崩潰。

如果是在教學場景的直播中,有兩個可以緩解服務器壓力的技巧。

第一個技巧是在上課當天,把搶答題目提前交給客戶端做預加載下載。這樣一來,就能夠減少實時拉取的壓力。

第二個方式是針對題目搶答的情況。當老師發佈題目的時候,提前設定發送動作生效後 5 秒再彈出題目。如此操作,能夠讓所有直播用戶的接收端 “準時” 地收到題目信息,而不至於出現用戶題目接收時間不一致的情況。

至於非搶答類型的題目,當用戶回答完題目後,我們可以先在客戶端本地先做預判卷,把正確答案和解析展示給用戶。然後,在直播期間異步緩慢地將用戶答題結果提交到服務端。通過這樣的方式,能夠保證服務器不會因用戶瞬時的流量而被沖垮。

點贊:客戶端互動合併

接下來,針對點讚的場景,我打算從客戶端以及服務端這兩個不同的角度來爲大家詳細介紹一下。

咱們先來看客戶端這邊的情況。在很多時候呀,客戶端其實並不需要實時地去提交用戶所做出的全部交互動作哦。這是因爲呀,有不少交互動作屬於那種機械性的重複動作,它們對於實時性的要求並沒有那麼高呢。

給大家舉個例子吧,比如說用戶在本地特別快速地連續點擊了 100 下贊,在這種情況下呢,客戶端就完全可以把這些點贊操作合併起來,將其轉化爲一條消息進行處理呀,就好比是 “用戶在 3 秒內點贊 10 次” 這樣的表述形式。

我相信呀,像大家這麼聰明的人,肯定能夠把這種將互動動作進行合併的小妙招運用到更多的情景當中去哦。比如說,當用戶連續打賞 100 個禮物的時候,同樣也可以採用這樣的方式來處理呀。

通過運用這種方式呢,能夠極大幅度地降低服務器所承受的壓力哦。這樣一來呀,既可以確保直播間依舊保持那種火爆的氛圍,同時呢,還能夠節省下大量的流量資源呢,這可真是一件一舉多得的好事呀,大家何樂而不爲呢?

點贊:服務端樹形多層彙總架構

我們回頭再看看點讚的場景下,如何設計服務端才能緩解請求壓力。如果我們的集羣 QPS 超過十萬,服務端數據層已經無法承受這樣的壓力時,如何應對高併發寫、高併發讀呢?微博做過一個類似的案例,用途是緩解用戶的點贊請求流量,這種方式適合一致性要求不高的計數器,如下圖所示:

這個方式可以將用戶點贊流量隨機壓到不同的寫緩存服務上,通過第一層寫緩存本地的實時彙總來緩解大量用戶的請求,將更新數據週期性地彙總後,提交到二級寫緩存。之後,二級彙總所在分片的所有上層服務數值後,最終彙總同步給核心緩存服務。接着,通過核心緩存把最終結果彙總累加起來。最後通過主從複製到多個子查詢節點服務,供用戶查詢彙總結果。

打賞 & 購物:服務端分片及分片實時擴容

前面的互動只要保證最終一致性就可以,但打賞和購物的場景下,庫存和金額需要提供事務一致性的服務。因爲事務一致性的要求,這種服務我們不能做成多層緩衝方式提供服務,而且這種服務的數據特徵是讀多寫多,所以我們可以通過數據分片方式實現這一類服務,如下圖:

看過圖之後,是不是感覺理解起來輕鬆多了呀?下面我再詳細說一說哦。

我們可以依據用戶的 id 來進行 hash 拆分操作呢。具體做法是,通過網關把不同用戶的 uid 進行取模處理,然後按照取模所得的數值範圍,將用戶分配到不同的分片服務上去。之後呢,處於各個分片內的服務就會針對類似的請求開展內存實時計算更新的工作啦。

通過採用這樣的方式呀,能夠較爲快速且便捷地實現負載的切分哦。不過呢,這種 hash 分配的方式也存在一定的弊端哦,那就是容易出現個別熱點的情況呢。當我們面臨的流量壓力大到服務器扛不住的時候呀,就需要對服務器進行擴容處理啦。

而且呀,要是採用 hash 這種方式,一旦出現個別服務器發生故障的情況,就會導致 hash 映射出現錯誤哦,這樣一來,請求就可能會被髮送到錯誤的分片上去啦。

針對這些問題呀,其實是有很多類似的解決方案的哦。比如說一致性 hash 算法吧,這種算法的優勢在於它可以針對局部的區域進行擴容操作,而且不會對整個集羣的分片造成影響哦。但是呢,這個方法在很多時候呀,由於其算法本身不夠通用,並且無法由人來進行有效控制,所以使用起來就會顯得特別麻煩呢,還需要專門開發配套的工具纔行哦。

除此之外呀,我再給大家推薦另外一種方式哦,那就是樹形熱遷移切片法啦。這是一種類似於虛擬桶的方式哦。打個比方來說吧,我們可以把全量數據拆分成 256 份呀,每一份就代表一個桶哦。假如有 16 個服務器的話,那麼每個服務器就可以分到 16 個桶啦。

當我們發現個別服務器的壓力過大的時候呀,就可以給這個服務器增加兩個訂閱服務器哦,讓它們去做主從同步的工作呢,也就是把這個服務器上的 16 個桶的數據進行遷移操作啦。

等到同步遷移的工作成功完成之後呀,就可以把原本發送到這個服務器的請求流量進行拆分處理啦,然後分別轉發到兩個各有 8 個桶的服務器上去哦。之後呢,就讓這兩個訂閱服務器分別接收請求並繼續對外提供服務啦,而原來那個壓力過大的服務器呢,就可以把它摘除並進行回收處理啦。

在服務成功完成切換之後呀,因爲進行的是全量遷移,所以這兩個新的服務會同時同步到原本並不屬於它們各自的 8 個桶的數據哦。在這種情況下呢,新服務器只需要去遍歷自己所存儲的數據,然後把那些不屬於自己的數據給刪除掉就可以啦。

當然啦,還有另外一種做法哦,那就是在同步來自 16 桶服務的數據的時候呢,就直接把那些不屬於自身的相關數據給過濾掉呀。需要說明的是,這個方法對於 Redis、MySQL 等所有存在有狀態分片數據的服務來說,都是適用的哦。

不過呢,這個服務存在一個難點哦,那就是請求的客戶端並不會直接去請求分片哦,而是要通過代理服務來對數據服務發起請求呢。只有藉助代理服務呀,才能夠實現對調度流量進行動態更新,進而達到平滑且無損地轉發流量的目的哦。

最後呀,咱們再來探討一下這樣一個問題哦,那就是如何讓客戶端知道應該去請求哪個分片才能夠找到它所需要的數據呢?在這裏呀,我給大家分享兩種比較常見的方式哦。

第一種方式是這樣的哦,客戶端可以通過特定的算法來找到分片哦。比如說呢,可以採用這樣的算法:用戶 hash (uid) % 100 = 桶 id 哦。然後呢,在配置文件當中,通過這個桶 id 就能夠找到與之對應的分片啦。

第二種方式則是呢,當數據服務端接收到客戶端的請求之後呀,會把這個請求轉發到存有相關數據的分片那裏哦。比如說吧,客戶端一開始請求的是 A 分片,然後再根據相應的數據算法以及對應的分片配置,發現所需要的數據其實是在 B 分片那裏哦。這個時候呢,A 分片就會把這個請求轉發到 B 分片哦。等到 B 分片處理完這個請求之後呢,就會把數據返回給客戶端啦(這裏的數據返回方式呢,是由 A 返回還是由 B 返回,這就要取決於客戶端是進行跳轉操作還是由服務端來進行轉發操作啦)。

服務降級:分佈式隊列彙總緩衝

即使通過這麼多技術來優化架構,我們的服務仍舊無法完全承受過高的瞬發流量。對於這種情況,我們可以做一些服務降級的操作,通過隊列將修改合併或做網關限流。雖然這會犧牲一些實時性,但是實際上,很多數字可能沒有我們想象中那麼重要。像微博的點贊統計數據,如果客戶端點贊無法請求到服務器,那麼這些數據會在客戶端暫存一段時間,在用戶看數據時看到的只是短期歷史數字,不是實時數字。十萬零五的點贊數跟十萬零三千的點贊數,差異並不大,等之後服務器有空閒了,結果追上來最終是一致的。但作爲降級方案,這麼做能節省大量的服務器資源,也算是個好方法。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ZDRdLX4WJOQdb99Cn9429A