Discord 是如何存儲數(shù)十億條消息

Discord的增長速度及用戶生成的內(nèi)容超出了我們的預(yù)期顽素。有了更多的用戶,更多的聊天信息就會出現(xiàn)徒蟆。7月胁出,我們計(jì)劃每天有4000萬條消息,12月我們每天達(dá)到1億條消息段审,截至本博客帖子全蝶,我們每天數(shù)據(jù)量已經(jīng)超過了1.2億條。我們很早就決定永遠(yuǎn)保存所有聊天記錄寺枉,這樣用戶就可以隨時回來抑淫,在任何設(shè)備上都可以使用他們的數(shù)據(jù)。這大量的數(shù)據(jù)姥闪,其速度始苇、大小都在不斷增加,我們必須保證他們的可用性筐喳。我們該怎么做呢催式?答案就是Cassandra!

我們在做什么

Discord的原始版本是在2015年初的不到兩個月內(nèi)構(gòu)建的避归∪僭拢可以說,最快的迭代數(shù)據(jù)庫之一是MongoDB梳毙。 Discord上的所有內(nèi)容都存儲在一個MongoDB副本集中哺窄,這是有意的,但我們還計(jì)劃了一切顿天,以便輕松遷移到新數(shù)據(jù)庫(我們知道我們不會使用MongoDB分片堂氯,因?yàn)樗褂闷饋砗軓?fù)雜而且不知道其穩(wěn)定性)。這實(shí)際上是我們公司文化的一部分:快速實(shí)現(xiàn)產(chǎn)品功能去對產(chǎn)品進(jìn)行試錯牌废,但始終通向更強(qiáng)大的解決方案咽白。

這些消息存儲在MongoDB集合中,在channel_id和created_at字段上具有單個復(fù)合索引鸟缕。 2015年11月左右晶框,我們存儲了1億條消息,此時我們開始看到預(yù)期出現(xiàn)的問題:數(shù)據(jù)和索引不再適合RAM懂从,延遲開始變得不可預(yù)測授段。是時候遷移到更適合該任務(wù)的數(shù)據(jù)庫了。

選擇正確的合適數(shù)據(jù)庫

在選擇新數(shù)據(jù)庫之前番甩,我們必須了解讀/寫模式以及我們當(dāng)前的解決方案會導(dǎo)致出現(xiàn)問題的原因侵贵。

  • 很快就發(fā)現(xiàn)我們的讀取非常隨機(jī),我們的讀/寫比率約為50/50缘薛。
  • Discord 的語音聊天服務(wù)幾乎不發(fā)送任何消息窍育。這意味著他們每隔幾天發(fā)一兩條信息卡睦。在一年之內(nèi),這種服務(wù)器消息量在1000條以內(nèi)漱抓。問題是表锻,盡管這是一小部分消息,但它使向用戶提供這些數(shù)據(jù)變得更加困難乞娄。比如只向用戶返回50條消息會導(dǎo)致在磁盤上進(jìn)行許多隨機(jī)查找瞬逊,從而導(dǎo)致磁盤緩存收回。
  • Discord的私有文本聊天服務(wù)發(fā)送了相當(dāng)多的消息仪或,很容易達(dá)到每年10萬到100萬條消息确镊。他們請求的數(shù)據(jù)通常只是最近產(chǎn)生的。問題是范删,由于這些服務(wù)器的成員通常不到100個骚腥,因此請求此數(shù)據(jù)的速度很低,不太可能位于磁盤緩存中瓶逃。
  • 大型公共Discord服務(wù)器發(fā)送大量消息束铭。他們有成千上萬的會員每天發(fā)送數(shù)千封郵件,每年輕松收集數(shù)百萬封郵件厢绝。他們幾乎總是在獲取過去一小時內(nèi)發(fā)送的消息契沫,并且他們經(jīng)常獲取這些消息。因此昔汉,這些數(shù)據(jù)通常位于磁盤緩存中懈万。
  • 我們知道,在接下來的一年里靶病,我們將為用戶提供更多的隨機(jī)閱讀方式:查看過去30天內(nèi)您提到的內(nèi)容会通,然后跳到歷史上的那個點(diǎn),查看并跳到固定郵件娄周,以及全文搜索涕侈。所有這些拼寫都更隨意!煤辨!
接著下來是我們系統(tǒng)的要求
  • 線性的可擴(kuò)展性 - 我們不希望稍后重新考慮解決方案或手動重新分片數(shù)據(jù)裳涛。
  • 故障自動轉(zhuǎn)移 - 我們喜歡在晚上睡覺,并盡可能地建立Discord自我修復(fù)功能众辨。
  • 維護(hù)成本低 - 一旦我們設(shè)置它就應(yīng)該工作端三。我們只需要在數(shù)據(jù)增長時添加更多節(jié)點(diǎn)。
  • 事實(shí)證明(Proven to work) - 我們喜歡嘗試新技術(shù)鹃彻,但不是太新郊闯。
    -可預(yù)測的性能 - 當(dāng)我們的API響應(yīng)時間第95百分位數(shù)超過80毫秒時,我們會發(fā)出警報(bào)。我們也不想在Redis或Memcached中緩存消息团赁。
  • 存儲方式不是blob(Not a blob store?) - 如果我們必須不斷地反序列化blob并將其附加到Blob存儲區(qū)中旋奢,那么每秒寫入數(shù)千條消息將不會很好。
  • 開源 - 我們相信控制自己的命運(yùn)然痊,不想依賴第三方公司。

Cassandra是唯一滿足我們所有要求的數(shù)據(jù)庫屉符。我們可以添加節(jié)點(diǎn)來擴(kuò)展節(jié)點(diǎn)剧浸,它可以容忍節(jié)點(diǎn)丟失而不會對應(yīng)用程序產(chǎn)生任何影響。 Netflix和Apple等大公司擁有數(shù)千個Cassandra節(jié)點(diǎn)矗钟。相關(guān)數(shù)據(jù)連續(xù)存儲在磁盤上唆香,提供最小的搜索并在群集周圍輕松分發(fā)。它得到了DataStax的支持吨艇,但仍然是開源和社區(qū)驅(qū)動的躬它。

做出選擇后,我們需要證明我們選擇的確是對的东涡。

數(shù)據(jù)模型

將Cassandra為新手描述的最佳方式是它是KKV存儲冯吓。兩個K包括主鍵。第一個K是分區(qū)鍵疮跑,用于確定數(shù)據(jù)所在的節(jié)點(diǎn)以及磁盤上的位置组贺。分區(qū)中包含多個行,分區(qū)中的行由第二個K標(biāo)識祖娘,第二個K是集群key失尖。群集key既充當(dāng)分區(qū)中的主鍵,又充當(dāng)行的排序方式渐苏。您可以將分區(qū)視為有序字典掀潮。結(jié)合這些屬性可實(shí)現(xiàn)非常強(qiáng)大的數(shù)據(jù)建模。

你還記得我們之前使用channel_id和created_at在MongoDB中索引消息? channel_id成為分區(qū)鍵琼富,因?yàn)樗胁樵兌荚谝粋€通道上運(yùn)行仪吧,但created_at沒有成為一個很好的集群鍵,因?yàn)閮蓚€消息可以具有相同的創(chuàng)建時間鞠眉。幸運(yùn)的是邑商,Discord上的每個ID實(shí)際上都是Snowflake(按時間順序排序)人断,所以我們可以使用它們恶迈。主鍵變?yōu)椋╟hannel_id,message_id)步做,其中message_id是Snowflake全度。這意味著在加載channel 時将鸵,我們可以準(zhǔn)確地告訴Cassandra掃描消息的范圍顶掉。

這是我們的消息表的簡化模式(這省略了大約10列)痒筒。

CREATE TABLE messages (
  channel_id bigint,
  message_id bigint,
  author_id bigint,
  content text,
  PRIMARY KEY (channel_id, message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);

Cassandra的schemas 與關(guān)系數(shù)據(jù)庫不同簿透,它們的改變成本很低廉萎战,并且不會對臨時性能產(chǎn)生任何影響蚂维。我們得到了最好的blob存儲和關(guān)系存儲虫啥。

當(dāng)我們開始將現(xiàn)有消息導(dǎo)入Cassandra時涂籽,我們立即在日志中看到警告评雌,告訴我們分區(qū)的大小超過100MB景东。發(fā)生什么事了斤吐?和措! Cassandra宣稱它可以支持2GB分區(qū)派阱!顯然贫母,僅僅因?yàn)樗梢酝瓿砂涠溃⒉灰馕吨鼞?yīng)該誓酒。在壓縮靠柑,集群擴(kuò)展等過程中歼冰,大型分區(qū)會對Cassandra造成很大的GC壓力隔嫡。擁有大分區(qū)也意味著其中的數(shù)據(jù)無法在群集中分布腮恩。很明顯秸滴,我們必須以某種方式限制分區(qū)的大小荡含,因?yàn)閱蝹€Discord通道可以存在多年并且不斷增大释液。

我們決定按時間播放我們的消息。我們查看了Discord上最大的channel(頻道)找前,并確定我們是否在一個存儲桶中存儲了大約10天的消息躺盛,我們可以輕松地保持在100MB以下槽惫。存儲桶必須可以從message_id或時間戳中導(dǎo)出界斜。

DISCORD_EPOCH = 1420070400000
BUCKET_SIZE = 1000 * 60 * 60 * 24 * 10

def make_bucket(snowflake):
   if snowflake is None:
       timestamp = int(time.time() * 1000) - DISCORD_EPOCH
   else:
       # When a Snowflake is created it contains the number of
       # seconds since the DISCORD_EPOCH.
       timestamp = snowflake_id >> 22
   return int(timestamp / BUCKET_SIZE)
  
def make_buckets(start_id, end_id=None):
return range(make_bucket(start_id), make_bucket(end_id) + 1)

Cassandra分區(qū)鍵可以組合项贺,因此我們的新主鍵變?yōu)椋ǎ╟hannel_id开缎,bucket),message_id)完残。

CREATE TABLE messages (
   channel_id bigint,
   bucket int,
   message_id bigint,
   author_id bigint,
   content text,
   PRIMARY KEY ((channel_id, bucket), message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);

為了查詢頻道中的最近消息坏怪,我們生成從當(dāng)前時間到channel_id的桶范圍(它也是Snowflake并且必須比第一條消息更舊)。然后我們依次查詢分區(qū)鹏秋,直到收集到足夠的消息侣夷。這種方法的缺點(diǎn)是很少有活動的Discords必須查詢多個桶以隨時間收集足夠的消息琴锭。在實(shí)踐中决帖,這被證明是好的,因?yàn)閷τ诨顒拥腄iscords刻像,通常在第一個分區(qū)中找到足夠的消息细睡,并且它們占大多數(shù)。

將消息導(dǎo)入Cassandra順利進(jìn)行萌京,我們已準(zhǔn)備好投入正式使用靠瞎。

Dark Launch(注: 是Fackbook使用的一種測試產(chǎn)品新功能的測試方法)

將新系統(tǒng)引入生產(chǎn)總是很可怕佳窑,所以嘗試在不影響用戶的情況下進(jìn)行測試是個好主意神凑。我們設(shè)置代碼以對MongoDB和Cassandra進(jìn)行雙重 讀/寫操作。

啟動后我們立即開始在錯誤跟蹤器中收到錯誤瓣喊,告訴我們author_id為空洪橘。怎么會是null熄求?這是一個必填字段抡四!

最終一致性

Cassandra是一個AP數(shù)據(jù)庫,這意味著它可以提供強(qiáng)大的可用性一致性,這是我們想要的勉耀。它是Cassandra中的read-before-write(讀取更昂貴)的反模式便斥,因此即使你只提供某些列黎棠,Cassandra所做的一切本質(zhì)上都是一個upsert木西。您還可以寫入任何節(jié)點(diǎn),它將使用每列的“l(fā)ast write wins”語義來自動解決沖突恋捆。那我們會遇到坑么?


1 t7VkLRKZVeHb_c6Tl1heew.gif

在用戶同時編輯消息而另一個用戶刪除相同消息的情況下,我們最終得到的行缺少除主鍵和文本之外的所有數(shù)據(jù)室奏,因?yàn)樗蠧assandra寫入都是upserts占业。處理此問題有兩種可能的解決方案:
1.編輯消息時寫回整個消息。這有可能恢復(fù)被刪除的消息,并為并發(fā)寫入其他列的沖突增加更多機(jī)會。
2.弄清楚該消息是否是臟數(shù)據(jù)并從數(shù)據(jù)庫中刪除它瞳氓。

我們選擇了第二個選項(xiàng)策彤,我們選擇了一個必需的列(在本例中為author_id)并刪除了該消息(如果它為null)。

在解決這個問題時顿膨,我們注意到我們的寫入效率非常低锅锨。由于Cassandra最終是一致的叽赊,它不能立即刪除數(shù)據(jù)恋沃。它必須將刪除復(fù)制到其他節(jié)點(diǎn),即使其他節(jié)點(diǎn)暫時不可用也要執(zhí)行此操作必指。 Cassandra通過將刪除視為一種稱為“墓碑”的寫入形式來實(shí)現(xiàn)這一點(diǎn)囊咏。在閱讀時,它只是跳過它遇到的墓碑塔橡。'墓碑' 在配置的時間內(nèi)(默認(rèn)為10天)存在梅割,并且在該時間到期時會在執(zhí)行壓縮操作后被永久刪除。

刪除列或?qū)ull寫入列里他們效果完全相同葛家,他們都會生成墓碑户辞。由于Cassandra中的所有寫入都是upserts,這意味著即使在第一次寫入null時癞谒,您也會生成一個邏輯刪除底燎。實(shí)際上刃榨,我們的整個消息模式包含16列,但平均消息只設(shè)置了4個值双仍。這樣我們大部分時間都無緣無故地向Cassandra寫了12個墓碑枢希。解決方案很簡單:只向Cassandra寫入非空值。

性能

眾所周知朱沃,Cassandra的寫入速度比讀取速度快苞轿,我們確實(shí)觀察到了這一點(diǎn)。寫入是亞毫秒逗物,讀取時間不到5毫秒搬卒。我們觀察到這一點(diǎn),無論訪問什么數(shù)據(jù)翎卓,并且在一周的測試期間性能保持一致秀睛。沒有什么是令人驚訝的,和我們預(yù)期效果差不多莲祸。


Read/Write Latency via Datadog.png

In line with fast蹂安,一致的讀性能,這里是一個一年前在包含數(shù)百萬條消息的頻道中跳轉(zhuǎn)到消息的示例:


bb.gif

一個大驚喜

一切順利锐帜,所以我們將其作為主要數(shù)據(jù)庫推出田盈,并在一周內(nèi)逐步淘汰MongoDB。它繼續(xù)完美地工作......大約6個月缴阎,直到有一天Cassandra反應(yīng)變得很遲鈍允瞧。

我們注意到Cassandra經(jīng)常運(yùn)行10秒“stop-the-world”GC,但我們不知道為什么蛮拔。我們開始研究并找到一個耗時20秒的Discord頻道述暂。由于它是公開的,我們加入它看看情況建炫。令我們驚訝的是畦韭,該頻道只有一條消息。就在那一刻肛跌,很明顯他們使用我們的API刪除了數(shù)百萬條消息艺配,在頻道中只留下了一條消息。

如果您一直在關(guān)注衍慎,您可能還記得Cassandra如何使用墓碑處理刪除(在最終一致性中提到)转唉。當(dāng)用戶加載此頻道時,即使只有一條消息稳捆,Cassandra也必須有效地掃描數(shù)百萬條消息(注:cassandra讀取到消息到再和墓碑里的delete消息進(jìn)行合并操作赠法,如果墓碑里的delete操作有成千上萬那么他合并操作就會有點(diǎn)費(fèi)時)邏輯刪除(生成垃圾的速度比JVM可以收集的速度快)。

我們通過以下方式解決了這個問題:

  • 我們將墓碑的壽命從10天縮短到2天乔夯,因?yàn)槲覀兠刻焱砩显谖覀兊南⒓荷线M(jìn)行Cassandra修復(fù)(repair操作砖织,他會進(jìn)行壓縮操作)原朝。
  • 我們更改了查詢代碼以跟蹤空桶,并在將來避免使用它們作為通道镶苞。這意味著如果用戶再次引發(fā)此查詢喳坠,那么在最糟糕的情況下,Cassandra將僅在最近的桶中進(jìn)行掃描茂蚓。

結(jié)論

自從我們進(jìn)行轉(zhuǎn)換以來已經(jīng)過去了一年多壕鹉,盡管“出人意料”,但它一帆風(fēng)順聋涨。我們從處理總量為1億條消息到每天超過1.2億條消息晾浴,性能和穩(wěn)定性保持很好。

由于該項(xiàng)目的成功牍白,我們已將線上的數(shù)據(jù)移至Cassandra脊凰,這也取得了成功。
在本文的后續(xù)內(nèi)容中茂腥,我們將探討如何使數(shù)十億條消息可搜索狸涌。

我們還沒有專門的DevOps工程師(只有4個后端工程師),因此擁有一個我們不必?fù)?dān)心的系統(tǒng)非常棒最岗。我們正在招聘帕胆,所以如果這種類型的東西刺激你的想法加入我們。

原文:https://blog.discordapp.com/how-discord-stores-billions-of-messages-7fa6ec7ee4c7

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末般渡,一起剝皮案震驚了整個濱河市懒豹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌驯用,老刑警劉巖脸秽,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蝴乔,居然都是意外死亡记餐,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門淘这,熙熙樓的掌柜王于貴愁眉苦臉地迎上來剥扣,“玉大人巩剖,你說我怎么就攤上這事铝穷。” “怎么了佳魔?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵曙聂,是天一觀的道長。 經(jīng)常有香客問我鞠鲜,道長宁脊,這世上最難降的妖魔是什么断国? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮榆苞,結(jié)果婚禮上稳衬,老公的妹妹穿的比我還像新娘。我一直安慰自己坐漏,他們只是感情好薄疚,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赊琳,像睡著了一般街夭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上躏筏,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天板丽,我揣著相機(jī)與錄音,去河邊找鬼趁尼。 笑死埃碱,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的酥泞。 我是一名探鬼主播乃正,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼婶博!你這毒婦竟也來了瓮具?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤凡人,失蹤者是張志新(化名)和其女友劉穎名党,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挠轴,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡传睹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了岸晦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欧啤。...
    茶點(diǎn)故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖启上,靈堂內(nèi)的尸體忽然破棺而出邢隧,到底是詐尸還是另有隱情,我是刑警寧澤冈在,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布倒慧,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏纫谅。R本人自食惡果不足惜炫贤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望付秕。 院中可真熱鬧兰珍,春花似錦、人聲如沸询吴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽汰寓。三九已至口柳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間有滑,已是汗流浹背跃闹。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留毛好,地道東北人望艺。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像肌访,于是被迫代替她去往敵國和親找默。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容

  • ORA-00001: 違反唯一約束條件 (.) 錯誤說明:當(dāng)在唯一索引所對應(yīng)的列上鍵入重復(fù)值時吼驶,會觸發(fā)此異常惩激。 O...
    我想起個好名字閱讀 5,248評論 0 9
  • Apache Cassandra 是一個開源的、分布式蟹演、去中心化风钻、彈性可擴(kuò)展、高可用性酒请、容錯骡技、一致性可調(diào)、面向行的...
    梁睿坤閱讀 14,015評論 2 25
  • 一羞反、MySQL優(yōu)化 MySQL優(yōu)化從哪些方面入手: (1)存儲層(數(shù)據(jù)) 構(gòu)建良好的數(shù)據(jù)結(jié)構(gòu)布朦。可以大大的提升我們S...
    寵辱不驚丶?xì)q月靜好閱讀 2,418評論 1 8
  • 本書一開始用幾個案例說明組織內(nèi)部管理變得越來越復(fù)雜昼窗。原因是因?yàn)榻M織里的人際關(guān)系變得復(fù)雜是趴。因?yàn)榻M織的成員變得更加的專...
    劉敏捷閱讀 545評論 1 0
  • “房子是租來的,但生活不是”膏秫,這是我特別喜歡的一句話右遭。誰說不是呢做盅,誰規(guī)定咋們背井離鄉(xiāng)在外工作就非得住在潮濕陰冷的地...
    沒有故事的袁同學(xué)閱讀 295評論 1 1