Discord技術(shù)架構(gòu)調(diào)研(IM即時通訊技術(shù)架構(gòu)分析)
一、目標
- 調(diào)研 discord 的整體架構(gòu),發(fā)掘可為所用的設(shè)計思想
二、調(diào)研背景
- Discord作為目前比較火的一個在線聊天和語音通信平臺且具有豐富的功能。另外其 “超級”群 概念號稱可支持百萬級群聊 以及 永久保留用戶聊天記錄。探究其相關(guān)技術(shù)架構(gòu)與技術(shù)實現(xiàn)
三、產(chǎn)品介紹
- 目前廣泛使用的在線聊天和語音通信平臺。最初于2015年發(fā)布,旨在為游戲社區(qū)提供一個交流和協(xié)作的平臺,但現(xiàn)在已經(jīng)擴展到各種不同的領(lǐng)域。

3.1、主要功能
- 文字聊天:用戶可以在頻道中發(fā)送消息,與其他成員進行實時交流。這些消息可以包含文字、表情符號、圖片、鏈接等。
- 語音通話:用戶可以通過Discord內(nèi)置的語音通話功能與其他成員進行語音交流。這對于組織游戲團隊、進行遠程會議或與朋友進行語音聊天非常有用。
- 視頻通話:除了語音通話,Discord還提供了視頻通話功能,使用戶可以進行面對面的視頻交流。
- 服務(wù)器和頻道:用戶可以創(chuàng)建自己的服務(wù)器,并在服務(wù)器內(nèi)創(chuàng)建不同的頻道,以便根據(jù)主題或目的進行組織和交流。
- 社交功能:Discord具有添加好友、私信、創(chuàng)建群組等社交功能,讓用戶可以與其他用戶建立聯(lián)系和交流。
- 權(quán)限和角色管理:服務(wù)器所有者可以設(shè)置不同的權(quán)限和角色,以控制成員對頻道和服務(wù)器的訪問和操作權(quán)限。
- 集成和插件:Discord可以與其他應(yīng)用程序和服務(wù)進行集成,例如Twitch、YouTube、Spotify等,以便在聊天中共享內(nèi)容或接收通知。
- Bots(機器人):用戶可以添加各種機器人來執(zhí)行各種任務(wù),例如管理服務(wù)器、播放音樂、提供實用工具等。
3.2、發(fā)展歷程

3.3、數(shù)據(jù)情況
3.3.1、用戶及群數(shù)據(jù)
- 總用戶數(shù)未知,預(yù)計1.5 億月活躍用戶,平臺上有 1900 萬個服務(wù)器,涵蓋游戲、投資、政治、動漫等領(lǐng)域。2020 年,Discord 每周有 670 萬服務(wù)器處于活躍狀態(tài),基本上每周都有某個給定話題的對話討論。2021 年,Discord 每周活躍服務(wù)器數(shù)據(jù)增長到了 1900 萬。

3.3.2、活躍數(shù)據(jù)
- Discord 平臺上單個日活躍用戶(DAU)與平臺的平均互動時長,是游戲直播平臺 Twitch 的兩倍,同時還是 Facebook Gaming、TikTok、Reddit 以及 Snap 等頭部社交平臺的兩倍以上。

3.3.3、收入情況
- 較少的商業(yè)化的動作,Discord 的每用戶平均收入 (ARPU) 僅為 1.30 美元,在公共社交媒體公司中排名非常靠后。

四、調(diào)研方向
- 整體技術(shù)架構(gòu)
- 外部集成&開放能力
- 技術(shù)棧應(yīng)用情況
- 核心業(yè)務(wù)模塊設(shè)計
- 核心基礎(chǔ)組件設(shè)計與基礎(chǔ)建設(shè)
五、調(diào)研內(nèi)容來源
- 因discord是一個商業(yè)產(chǎn)品且并未開源,國內(nèi)相關(guān)資料也較少。所以只能通過閱讀官方博客與開發(fā)者平臺進行分析、推導(dǎo)與猜想
- 官方博客:https://discord.com/blog
- 開發(fā)者平臺:https://discord.com/developers/docs/reference
六、調(diào)研內(nèi)容
6.1、 整體架構(gòu)

discord開發(fā)團隊核心理念
- 擁抱開源的同時,對開源中間件做自己的特定優(yōu)化與增強
- 隨時保持基建可替換
- 盡可能降低架構(gòu)與業(yè)務(wù)開發(fā)復(fù)雜度
6.2、 外部集成&開放能力
6.2.1、open api

- 開放接口,其中主要包括 公會相關(guān)操作、表情符號、webhook等開放能力
6.2.1.1、公會相關(guān)操作
-
在Discord 中的公會(一般也叫“服務(wù)器”)代表用戶和頻道的集合
- 一個公會下可以有多個頻道,每個頻道消息隔離,成員共享
- 提供了公會增刪改查、語音狀態(tài)等管理接口
6.2.1.2、表情符號
- 支持開發(fā)者可自定義表情包與符號。并提供其管理能力
6.2.1.3、webhook
- Webhooks 是一種不需要用戶主動發(fā)起或者機器人交互在 Discord 中向頻道發(fā)布消息的方式
- 主要用于系統(tǒng)消息的主動發(fā)送
6.2.2、gateway

- 允許開發(fā)者通過WebSocket對關(guān)鍵事件進行訂閱與監(jiān)聽,最終推送給開發(fā)者
- 可以接收有關(guān)服務(wù)器/公會中發(fā)生的操作事件,例如更新頻道或創(chuàng)建角色。在某些情況下,應(yīng)用還會使用網(wǎng)關(guān)連接來更新或請求資源,例如更新語音狀態(tài)時。
6.2.3、機器人

-
Discord 提供了機器人用戶能力,這是一種自動化的用戶類型(每個類型的機器人背后都有一個運行程序,可通過sdk方式進行集成)
- 類似于微信公眾號的機器人功能
- 用戶可通過斜杠“/”命令與機器人進行交互。
6.2.4、GameSDK

- 通過提供GameSDK讓游戲開發(fā)者進行集成,來幫助游戲開發(fā)與discord進行交互
- 如:游戲狀態(tài)管理、網(wǎng)絡(luò)組件、用戶關(guān)系、邀請等相關(guān)交互功能
6.2.5、RPC(內(nèi)測中)

- Discord 客戶端會提供一個在本地主機上運行的 RPC 服務(wù)器,允許開發(fā)者在客戶端來控制本地 Discord
-
通過本地調(diào)用無服務(wù)器方式讓游戲開發(fā)的客戶端可直接與本地discord客戶端進行交互
- 如:rtc控制、公會/頻道 管理等
6.3、 技術(shù)棧說明
6.3.1、客戶端

- 提供移動端與桌面端兩種 客戶端形式供用戶使用
- 移動端使用react native進行跨端業(yè)務(wù)實現(xiàn)
- 桌面端使用electron進行的web套殼實現(xiàn)
- 底層組件使用rust進行開發(fā)
6.3.2、服務(wù)端
6.3.2.1、語言相關(guān)

- 多種主流語言進行混合開發(fā)
- 短鏈api使用python進行快速迭代
- 長鏈gateway使用erlang的變種語言Elixir進行開發(fā)
-
核心業(yè)務(wù)早期使用Elixir與golang兩種語言進行開發(fā)
- 其中g(shù)olang主要實現(xiàn)rtc與音頻相關(guān)業(yè)務(wù)服務(wù)
-
Elixir主要實現(xiàn)im、工會等核心業(yè)務(wù)相關(guān)服務(wù)
- 其中一些性能優(yōu)化,組件迭代使用rust增強實現(xiàn),Elixir通過NIF方式進行調(diào)用
- 經(jīng)過長久迭代,discord開發(fā)團隊認為golang gc問題是個詬病,正在逐步進行遷移至rust語言
- 底層組件服務(wù)使用rust實現(xiàn),以達到高性能服務(wù)提供
- 內(nèi)部服務(wù)使用grpc進行通信
6.3.2.2、應(yīng)用系統(tǒng)架構(gòu)
- 整體通過響應(yīng)式架構(gòu)進行設(shè)計開發(fā)
-
響應(yīng)式架構(gòu)特點:
- “流”式編程 (連續(xù)、異步、可觀察)
- 有效地處理并發(fā)請求,提高系統(tǒng)的吞吐量
- 消息/事件驅(qū)動,有助于系統(tǒng)的解耦,提高系統(tǒng)的擴展性和彈性。
6.3.2.3、基建相關(guān)

-
ETCD:
- grpc注冊中心與服務(wù)發(fā)現(xiàn)
- 組件集群注冊管理(如消息檢索es管理,后面會詳細說)
- redis: es負載信息存儲與緩存
-
數(shù)據(jù)存儲
- Mongo -> Cassandra -> ScyllaDB (存在演進過程,后面會詳細說)
6.4、核心業(yè)務(wù)模塊
6.4.1、消息模塊
- discord消息相關(guān)有兩個比較關(guān)鍵的點,一個是支持非常靈活的消息樣式,另外一個就是其“超級”群消息是如何扇出的
6.4.1.1、 消息組件(協(xié)議)
關(guān)鍵協(xié)議摘取
|
字段名
|
類型
|
作用
|
描述
|
|
mention_everyone
|
bool
|
是否提及所有人
|
at所有人
|
|
mentions
|
user數(shù)組
|
提及到的人
|
at的所有人
|
|
attachments
|
消息附件實體數(shù)組
|
附件
|
主要描述消息內(nèi)攜帶的文件內(nèi)容
|
|
pinned
|
bool
|
是否置頂
|
消息置頂
|
|
message_reference
|
消息實體
|
引用的消息
|
被引用的歷史消息
|
|
embeds
|
消息嵌入實體數(shù)組
|
嵌入信息
|
主要描述消息內(nèi)嵌入的圖片、視頻等內(nèi)容
|
|
type
|
integer
|
消息類型
|
消息的類型,詳情可看下文。
https://discord.com/developers/docs/resources/channel#message-object-message-types
|
|
content
|
string
|
消息內(nèi)容
|
消息內(nèi)容
|
|
components
|
消息組件實體數(shù)組
|
組件
|
消息內(nèi)嵌入的既定組件。下面會想說
|
- 其中components代表的是消息的定義擴展(消息組件),如消息卡片、選擇按鈕等都是基于此實現(xiàn)。其余字段均為關(guān)鍵業(yè)務(wù)字段。
- 此處只摘取了關(guān)鍵字段,次要未進行摘取。如有興趣可祥看:https://discord.com/developers/docs/resources/channel#message-object
消息組件
-
消息組件主要由組件類型與嵌入組件兩個屬性,具體的組件內(nèi)容不同組件類型均不一樣。
- 組件可以嵌入組件。如多選下拉菜單
eg:
{ "content": "This is a message with components", "components": [ { "type": 1, "components": [] } ] }
目前支持的組件類型

- 下面會對主要組件類型進行單一分析,因為其components字段是個數(shù)組所以實際應(yīng)用場景可進行組裝
- 下文會對主要類型進行分析,其他組件可祥見:https://discord.com/developers/docs/interactions/message-components
組件交互
- 組件分為交互與非交互組件
-
其中交互組件會與開發(fā)者服務(wù)進行交互,交互形式有兩種方式可選
- 上面提到的gateway進行監(jiān)聽與回復(fù)
- 上面提到的open api中的webhook進行接收與返回
Action Row (嵌入組件)
-
是其他類型組件的非action row組件的容器。
- 每條消息最多可以有 5 個Action Row
- 一個Action Row不能包含另一個Action Row
Button (按鈕組件)
組件協(xié)議

說明
按鈕有多種樣式來傳達不同類型的操作。這些樣式還定義哪些字段對按鈕有效。
- 非鏈接按鈕必須有
custom_id,并且不能有url - 鏈接按鈕必須有
url,并且不能有custom_id - 鏈接按鈕在點擊時不會向開發(fā)者的服務(wù)發(fā)起交互,僅會做鏈接跳轉(zhuǎn)
按鈕可選樣式

eg
{ "content": "This is a message with components", "components": [ { "type": 1, "components": [ { "type": 2, "label": "Click me!", "style": 1, "custom_id": "click_one" } ] } ] }
Select Menus(多選下拉菜單)
組件協(xié)議

說明
- 比較關(guān)鍵的就是options該屬性是多選下拉菜單的關(guān)鍵參數(shù),該參數(shù)定義了每個選項的具體內(nèi)容
option結(jié)構(gòu)

eg:
// This is a message { "content": "Mason is looking for new arena partners. What classes do you play?", "components": [ { "type": 1, "components": [ { "type": 3, "custom_id": "class_select_1", "options":[ { "label": "Rogue", "value": "rogue", "description": "Sneak n stab", "emoji": { "name": "rogue", "id": "625891304148303894" } }, { "label": "Mage", "value": "mage", "description": "Turn 'em into a sheep", "emoji": { "name": "mage", "id": "625891304081063986" } }, { "label": "Priest", "value": "priest", "description": "You get heals when I'm done doing damage", "emoji": { "name": "priest", "id": "625891303795982337" } } ], "placeholder": "Choose a class", "min_values": 1, "max_values": 3 } ] } ] }
6.4.1.2、 消息扇出流程
整體架構(gòu)

說明
- 一條消息從發(fā)出到扇出的流程是:api服務(wù)將消息發(fā)送到工會服務(wù)(有狀態(tài)節(jié)點,與公會進行綁定),工會服務(wù)將消息均勻發(fā)到中繼節(jié)點。中繼節(jié)點將消息發(fā)送給session網(wǎng)關(guān)。最終推送到用戶手機
- 其中公會服務(wù)負責消息權(quán)限等基礎(chǔ)功能校驗(也就是校驗該消息是否允許被發(fā)出)
- 公會服務(wù)校驗完畢,將消息均勻分布到中繼節(jié)點,由特定中繼節(jié)點來處理具體的扇出流程
- 其中扇出目標用戶僅為在線用戶,所以在用戶登錄后,discord相關(guān)服務(wù)會更新相關(guān)用戶所有服務(wù)器的在線列表,此時中繼服務(wù)僅需對在線成員進行扇出即可
- 中繼節(jié)點獲取到在線成員后,會校驗該用戶是否有權(quán)限進行接收,最終對可接受用戶將消息發(fā)送給session網(wǎng)關(guān)。最終推送到用戶手機
ETS(項式存儲)
- 因為部分公會在線成員可能較多,遠程獲取也會有較大的損耗,discord在初期會在中繼服務(wù)中緩存每個相關(guān)公會的成員信息,但這是恐怖的,在發(fā)展后期,達到了百萬計的成員。機器內(nèi)存成本極高,discord開發(fā)團隊對該部分進行了優(yōu)化
- 其中主要是將中繼服務(wù)內(nèi)緩存的公會成員信息優(yōu)化打到erlang 的ETS中進行存儲緩存(Erlang虛擬機級別共享內(nèi)存)
- 并啟動worker進行對ETS 中的數(shù)據(jù)進行統(tǒng)一管理(inserts, updates, deletes)

6.4.2、推送模塊(genstage)
背景
- discord為應(yīng)對突發(fā)通知過載問題,設(shè)計了genstage模塊用于消息推送
-
當時主要瓶頸在于向谷歌的 Firebase 云消息服務(wù)發(fā)送推送通知。
- Firebase 要求每個 XMPP 連接每次待處理的請求不得超過 100 個。如果有 100 個請求正在處理中,就必須等 Firebase 確認一個請求后再發(fā)送另一個請求。
- 由于一次只能有 100 個請求待處理,因此需要設(shè)計新系統(tǒng),使 XMPP 連接在突發(fā)情況下不會過載。
整體架構(gòu)

說明
-
將系統(tǒng)分為兩個 GenStage 階段。一個source,一個sink
-
階段1 - source(推送收集器)
- 是收集推送請求的生產(chǎn)者。每臺機器都會有一個推送收集器 Erlang 進程。
-
階段2 - sink(推送者)
- 是一個消費者,它從推送收集器獲取推送請求,并將請求推送到 Firebase。它一次只需要 100 個請求,以確保不會超過 Firebase 的待處理請求限制。每臺機器上有多個 Erlang 進程。
-
-
GenStage 還有有兩個關(guān)鍵功能可在突發(fā)情況下提供幫助:背壓與甩負荷。
-
背壓:
- source會詢問sink所能處理的最大請求數(shù)。這就確保了sink待處理的推送請求數(shù)量的上限。當 Firebase 確認請求時,sink會向source提出更多請求(sink知道 Firebase XMPP 連接所能處理的確切數(shù)量)。
- 除非sink提出請求,否則source絕不會向sink發(fā)送請求。這就保證了sink永遠都是無壓力的
-
甩負荷:
- 由于 sink會對source產(chǎn)生反向壓力,source就會有一個潛在的瓶頸。超大規(guī)模的突發(fā)可能會使source超載。
-
所以source 還會有一個內(nèi)置功能可以處理這個問題:緩沖事件。
- 在source中,可以指定緩沖多少個推送請求。一般情況下,緩沖區(qū)是空的,但在突發(fā)通知的情況下,緩沖區(qū)就會派上用場。用于緩沖sink無法處理的事件
-
如果系統(tǒng)中的消息太多,也就是緩沖區(qū)達到瓶頸,source就會停止接收推送請求(丟棄或降級)
- 思考:此處可以基于業(yè)務(wù)策略進行降級,如部分不重要推送進行直接丟棄,重要推送進行降級緩沖
-
效果指標
Sink推送數(shù)量/分鐘

Source 緩沖事件數(shù)量/分鐘

6.5、核心基礎(chǔ)組件與基礎(chǔ)建設(shè)
6.5.1、存儲演進過程與 服務(wù)架構(gòu)流程
6.5.1.1、 存儲db演進過程

早期:mongo單分片
- 單副本集的 MongoDB,沒有使用 MongoDB 的分片,他們給出的理由是當時 MongoDB 分片很難用,而且不夠穩(wěn)定(這里就不去深究了)。消息數(shù)到達一億條時,RAM 里已經(jīng)存不下這么多數(shù)據(jù)和索引,MongoDB 的延時開始變得不可控。
中期:從 MongoDB 到 Cassandra
業(yè)務(wù)背景
- 2017年,消息數(shù)過億,mongo延時變得不可控。
- 場景讀取極其隨機,讀寫比例整體大約為 50/50。不同業(yè)務(wù)場景群讀取比例不同。但又不想對每個場景做獨立解決方案,所以他們決定基于現(xiàn)有訴求,選擇新的存儲進行數(shù)據(jù)遷移
訴求
- 線性可擴展性: 不希望以后重新考慮解決方案或手動重新分揀數(shù)據(jù)。
- 自動故障轉(zhuǎn)移: 盡可能的進行自我修復(fù)
- 維護成本低: 一旦設(shè)置好,它就能正常工作。只需在數(shù)據(jù)增長時添加更多節(jié)點即可。
- 經(jīng)證明有效: 喜歡嘗試新技術(shù),但不能太新。
- 可預(yù)測的性能: 延時達到一定水位就會發(fā)出警報。并且不希望緩存消息。
- 開源: 相信自己的命運自己掌握,不想依賴第三方公司。
實施
- 基于以上訴求他們認為 Cassandra 是當時唯一能滿足他們要求的數(shù)據(jù)庫(后面也打臉了)
Cassandra特性
- Ap database
- 是一個KKV 存儲器。主鍵由兩個 K 組成。第一個 K 是分區(qū)鍵,用于確定數(shù)據(jù)所在的節(jié)點以及在磁盤上的位置。分區(qū)中包含多條記錄,分區(qū)內(nèi)的記錄由第二個 K(即聚類鍵)標識。聚類鍵既是分區(qū)內(nèi)的主鍵,也是行的排序方式。你可以把分區(qū)看成一個有序的字典。這些屬性結(jié)合在一起,可以實現(xiàn)非常強大的數(shù)據(jù)建模。
- 單分區(qū)大小不建議超過 100MB。Cassandra 宣稱它可以支持 2GB 分區(qū)!但雖然可以支持,但并不意味著應(yīng)該支持
數(shù)據(jù)建模
- 關(guān)鍵數(shù)據(jù)結(jié)構(gòu)
CREATE TABLE messages ( channel_id bigint, bucket bigint, message_id bigint, author_id bigint, content text, PRIMARY KEY ((channel_id, bucket), message_id) ) WITH CLUSTERING ORDER BY (message_id DESC);
-
基于Cassandra的特性他們將主鍵設(shè)計成((channel_id, bucket), message_id)
- channel_id: 服務(wù)器頻道id
- bucket: 基于時間的數(shù)據(jù)分桶(基于他們的統(tǒng)計,大約10天的聊天消息約100MB)
-
message_id: 基于雪花算法的消息id
- 這意味著在加載頻道時,可以告訴 Cassandra 準確掃描消息的范圍
- 消息發(fā)布時間與bucket可以通過message_id中提取【因為message_id是基于雪花算法的】
遷移期間與使用過程中遇到的問題
-
第一個遇到的就是100MB問題
- discord最初使用Cassandra存儲并沒有bucket概念。但在運行與遷移過程中Cassandra發(fā)出了100MB警告
- discord開發(fā)者通過分析歷史消息數(shù)據(jù)分布情況,定下來100MB可存儲其10天消息數(shù)據(jù),故將數(shù)據(jù)進行分桶
-
寫入順序問題,Cassandra的數(shù)據(jù)寫入處理邏輯是先讀后寫【本次寫入會依賴上次寫入結(jié)果】
-
在一個用戶編輯一條消息的同時,另一個用戶刪除了同一條消息,由于 Cassandra 寫入的所有內(nèi)容都是向上插入的,因此最終會發(fā)現(xiàn)一條記錄中除了主鍵和文本外,缺少其他所有數(shù)據(jù)
- 如: 在t1時間,進行了消息刪除。t2時間做了消息修改(消息體),此次修改會基于上次刪除結(jié)果,所以其他字段都為空了
- 他們采用的方案是啟動一個反熵進程對臟數(shù)據(jù)進行清理與刪除(可能是為了無鎖化)
-
遷移后指標情況

現(xiàn)在:從 Cassandra 到 ScyllaDB
業(yè)務(wù)背景
-
2022年,隨著業(yè)務(wù)場景和消息規(guī)模的增長, Cassandra 有 177 個節(jié)點,擁有數(shù)萬億條消息 ,Cassandra 也出現(xiàn)了嚴重的性能問題
- 熱分區(qū)
- 壓縮問題導(dǎo)致請求級聯(lián)延遲
- S-T-W (java)
訴求
- 可解決“熱分區(qū)” 問題
- 數(shù)據(jù)壓縮不會出現(xiàn)級聯(lián)延遲
- 避免S-T-W
ScyllaDB特性
- 一般來說屬于 AP database,更加側(cè)重于可用性和分區(qū)容錯性,但是 ScyllaDB 的一致性級別是可以調(diào)整的。
-
完全兼容 Cassandra,號稱是Cassandra CPP實現(xiàn)的替代品
- CPP編寫,無GC,所以也就不會出現(xiàn)S-T-W
-
相比Cassandra有更好的性能、更快的修復(fù)、通過每核分片架構(gòu)實現(xiàn)更強的工作負載隔離。
- -- 避免出現(xiàn)級聯(lián)延遲
ScyllaDB如何解決的壓縮問題?
- Compaction Strategy:ScyllaDB 使用不同的算法(稱為策略)來確定何時以及如何最好地運行壓縮。該策略決定了寫入、讀取和空間放大之間的權(quán)衡。ScyllaDB Enterprise 甚至支持一種稱為增量壓縮策略的獨特方法,該方法可以顯著節(jié)省磁盤開銷。
- 壓縮和解壓縮并行化:ScyllaDB 使用多線程并行化壓縮和解壓縮操作,以減少壓縮和解壓縮對整體性能的影響。這樣可以更好地利用多核處理器的能力,并減少由于壓縮和解壓縮而引起的延遲。
- 壓縮字典緩存:ScyllaDB 使用壓縮字典緩存來提高壓縮和解壓縮的性能。字典緩存存儲了一些常見的字符串和它們的壓縮形式,這樣可以減少壓縮和解壓縮時需要傳輸?shù)臄?shù)據(jù)量,從而降低延遲。
- 硬件加速:ScyllaDB 利用現(xiàn)代硬件特性,如 Intel 的 CPU 壓縮指令集(Intel ISA-L),來加速壓縮和解壓縮操作。這些硬件加速技術(shù)可以顯著提高壓縮和解壓縮的性能,從而減少級聯(lián)延遲。
缺點
-
當以與表排序相反的順序掃描數(shù)據(jù)庫時,有反向查詢性能不足的問題
- discord團隊識別該缺點可接受
-
未解決“熱分區(qū)” 問題
- 通過建立“存儲服務(wù)”進行解決
遷移方案
- 因為ScyllaDB完全兼容 Cassandra,所以可以直接進行數(shù)據(jù)遷移
-
但因為其未解決“熱分區(qū)” 問題。所以discord開發(fā)者基于ScyllaDB做了業(yè)務(wù)增強實現(xiàn)來解決該問題
- -- “存儲服務(wù)”
遷移效果
- 將運行 177 個 Cassandra 節(jié)點減少到僅運行 72 個 ScyllaDB 節(jié)點。每個 ScyllaDB 節(jié)點擁有 9TB 磁盤空間,高于每個 Cassandra 節(jié)點平均 4TB 的存儲空間。1774-729=60T,這么看的話他們的存儲空間也節(jié)省了一些。在 Cassandra 上獲取歷史消息的 p99 為 40-125 毫秒,而 ScyllaDB 的延遲為 15 毫秒,消息插入性能從 Cassandra 上的 5-70 毫秒 p99 到 ScyllaDB 上穩(wěn)定的 5 毫秒 p99。
- 當然此處效果與指標存儲服務(wù)也功不可沒。下面會說存儲服務(wù)的整體設(shè)計
6.5.1.2、 存儲服務(wù)架構(gòu)及流程
背景
- discord開發(fā)團隊為了解決數(shù)據(jù)查詢“熱分區(qū)”問題,Discord 采用的方案是:在 ScyllaDB 和業(yè)務(wù)服務(wù)之間加了一個中介服務(wù)(Rust 語言編寫),它不包含任何業(yè)務(wù)邏輯,主要功能就是合并請求。
整體架構(gòu)

架構(gòu)說明
- 存儲服務(wù)主要做了兩部分功能。合并請求與收斂請求
- 是一個有狀態(tài) 無數(shù)據(jù) 的中介服務(wù)
模塊設(shè)計
合并請求
- 如果多個用戶同時請求數(shù)據(jù)庫的同一行,那么只會查詢數(shù)據(jù)庫一次。
- 第一個發(fā)出請求的用戶會在該服務(wù)中啟動工作任務(wù), 后續(xù)請求將檢查該任務(wù)是否存在并訂閱它, 該工作任務(wù)將查詢數(shù)據(jù)庫并將該行返回給所有訂閱者。

收斂請求
- 同時根據(jù)一致性 hash 將同類查詢請求,比如同一個頻道的請求,進一步收斂到中介服務(wù),這樣可以讓請求合并的效果更好。

6.5.2、 消息檢索架構(gòu)流程
背景
-
因discord業(yè)務(wù)發(fā)展迅速,在2017年要推出消息檢索功能。其技術(shù)訴求如下
- 經(jīng)濟高效:Discord 的核心用戶體驗是文字和語音聊天。搜索是一項附屬功能,搜索的成本不應(yīng)高于信息的實際存儲成本。
- 自我修復(fù):需要能夠承受故障,只需極少的人為干預(yù),甚至無需人為干預(yù)。
- 可線性擴展:就像存儲信息一樣,只需在數(shù)據(jù)增長時添加更多節(jié)點即可
- 避免繁瑣的大型集群: 在集群中斷的情況下,只有受影響集群中包含的 消息無法用于搜索。或者一旦整個集群的數(shù)據(jù)無法恢復(fù),可以將其丟棄(系統(tǒng)可以重新索引 Discord 服務(wù)器數(shù)據(jù))。
整體架構(gòu)
- 基于以上背景,discord團隊最終選用es來支持消息檢索,但摒棄了es的分片能力(自行實現(xiàn)消息分區(qū)),因為使用es的分片能力以當時discord的數(shù)據(jù)量意味著要建立大型集群,這會帶來較高的維護成本與恢復(fù)成本

說明
-
應(yīng)用層進行分區(qū),分區(qū)維度為頻道id, es采用多集群方式,不同頻道id綁定不同es集群 (小集群)
- 這樣特定頻道數(shù)據(jù)就會被分到單一的es集群
- es中采用無分片單副本的索引模式(因使用了應(yīng)用分區(qū),所以不需要es的分區(qū)功能了,副本是用于集群異常數(shù)據(jù)恢復(fù)使用)
- 啟動his index worker 來用于歷史消息的索引與重新索引(如es集群產(chǎn)生故障,舊集群被摘除使用新的集群需要重新索引消息)
-
業(yè)務(wù) 服務(wù)只負責向queue里發(fā)送消息信息,由index worker進行拉取并索引進es中
- 能力解偶、提升主流程性能、消峰
- 因檢索場景大多數(shù)都是對歷史消息進行檢索,所以queue延遲與es的近實時特性是可接受的
- es集群注冊到etcd中來實現(xiàn)自動發(fā)現(xiàn),然后會在redis中存儲每個集群的負載情況,檢索sdk會使用負載最低的進群進行實時綁定
-
綁定數(shù)據(jù)使用業(yè)務(wù)db進行關(guān)系存儲并使用redis進行緩存
- 因為其使用的業(yè)務(wù)db無論是cassandra還是scylladb都存在高成本讀取,所以此處做了緩存 (與其前面說的不想使用緩存有出入)
- 下次產(chǎn)生新的消息時,直接使用既定的綁定關(guān)系進行存儲即可
七、結(jié)論
通過上述對discord的調(diào)研分析,獲得的一些啟發(fā):
功能上
discord 對開發(fā)者提供了豐富的集成方式:機器人、api、網(wǎng)關(guān)、client RPC(亮點)、GameSDK。
設(shè)計上
-
discord 使用了不同的技術(shù)棧來解決技術(shù)上的訴求,思路非常開闊
- 使用erlang開發(fā),因其天生的actor模型,故可以完美支持其響應(yīng)式架構(gòu)達到高吞吐目的
- 但erlang對于某些特定場景存在性能問題,又使用rust來解決。在不變業(yè)務(wù)服務(wù)的情況通過NIF 的形式進行調(diào)用(業(yè)務(wù)無感)
- api入口使用python可以達到快速迭代,因為內(nèi)部服務(wù)使用golang、rust、erlang又不會產(chǎn)生較大的性能問題
-
單一且高負載業(yè)務(wù),可通過進程分離的形式進行優(yōu)化。各司其職,壓力分攤
- 消息扇出流程:發(fā)送與扇出分離
- 推送模塊: 接收與推送分離
- 用戶上線時把用戶加入公會和在線狀態(tài)進行綁定,維護了公會內(nèi)的在線用戶,消息扇出時極大的減少了消息處理量
-
擁抱開源,在開源基礎(chǔ)上針對業(yè)務(wù)的訴求進行優(yōu)化或增強
- 存儲服務(wù)解決的熱分區(qū)問題。不僅大大緩解了db壓力,還降低了架構(gòu)與業(yè)務(wù)開發(fā)復(fù)雜度 (無緩存設(shè)計)
- 消息檢索業(yè)務(wù),discord為了避免繁瑣的大型集群實現(xiàn)了自己的分片/分區(qū)模式
- 隨時保持基建可替換,降低隨著業(yè)務(wù)迭代與數(shù)據(jù)發(fā)展帶來的基建升級/演進 成本(架構(gòu)一定是隨時演進的 )
-
基礎(chǔ)架構(gòu)與組件可作為未來參考
- 響應(yīng)式架構(gòu)來提升單機吞吐量
-
db壓力過載可通過合并請求&收斂請求的方式進行優(yōu)化,來緩解db壓力
- 不一定非要用于存儲,比如我們客戶端的一些非重要請求也可以作此優(yōu)化來降低帶寬使用與緩解服務(wù)器壓力
本文來自博客園,作者:房上的貓,轉(zhuǎn)載請注明原文鏈接:http://www.rzrgm.cn/lsy131479/p/18659629

浙公網(wǎng)安備 33010602011771號