1.5億用戶、萬(wàn)億數(shù)據(jù),爆款社交平臺(tái)的兩次大型數(shù)據(jù)庫(kù)遷移
2017年,Discord 在技術(shù)博客中提到,由于 RAM 中無(wú)法再容納數(shù)據(jù)和索引,延遲開(kāi)始變得不可預(yù)測(cè),急速增長(zhǎng)的數(shù)據(jù)存儲(chǔ)亟待遷移。他們希冀找到一款可擴(kuò)展、容錯(cuò)且維護(hù)成本相對(duì)較低的數(shù)據(jù)庫(kù),以實(shí)現(xiàn)存儲(chǔ)數(shù)十億條消息的目標(biāo),最終完成了從 MongoDB 到 Cassandra 的遷移。
技術(shù)人員都希望,現(xiàn)行數(shù)據(jù)庫(kù)能夠滿足不斷增長(zhǎng)的存儲(chǔ)需求,同時(shí)保持較低的維護(hù)需求??上КF(xiàn)實(shí)往往事與愿違—— Discord 使用的 Cassandra 集群出現(xiàn)嚴(yán)重的性能問(wèn)題,技術(shù)人員耗費(fèi)越來(lái)越多的精力,致力于維護(hù)數(shù)據(jù)庫(kù),而非改進(jìn)性能。
時(shí)隔六年,Discord 消息存儲(chǔ)再面臨性能挑戰(zhàn),于是將數(shù)據(jù)庫(kù)遷移至 ScyllaDB。
這兩次數(shù)據(jù)庫(kù)遷移原因幾何?Discord 如何選型?遷移過(guò)程中問(wèn)題又將如何解決?
讓我們一同在下文中尋找答案。
2017:從MongoDB到Cassandra
Discord 的增長(zhǎng)速度和用戶生成的內(nèi)容數(shù)量超出了我們的預(yù)期,用戶越多,聊天信息也就越多。2016年7月,平臺(tái)消息量已達(dá)4000萬(wàn)條,12月增至1億條,而截至這篇文章發(fā)布(2017年1月)時(shí),信息量已超過(guò)1.2億條。
我們很早就決定永久存儲(chǔ)所有聊天記錄,以便用戶隨時(shí)返回查看,并在任何設(shè)備上使用他們的數(shù)據(jù)。而這些龐大數(shù)據(jù)的增長(zhǎng)速度及規(guī)模都在持續(xù)攀升,并且需要隨時(shí)保持可用。
這一點(diǎn)如何做到?我們的回答是遷移至 Cassandra 數(shù)據(jù)庫(kù)。
1.Discord 最初版本
2015年初,Discord 的最初版本在不到兩個(gè)月的時(shí)間成功創(chuàng)建。在當(dāng)時(shí)情況下,MongoDB 是快速迭代的最佳數(shù)據(jù)庫(kù)之一。Discord 的所有內(nèi)容都存儲(chǔ)在單個(gè) MongoDB 集群中,這是有意為之,但同時(shí)我們也做好一切后續(xù)規(guī)劃,以便將所有數(shù)據(jù)輕松地遷移到新數(shù)據(jù)庫(kù)(我們確信不會(huì)使用 MongoDB 分片,因?yàn)樗褂脧?fù)雜,且穩(wěn)定性不佳)。
實(shí)際上,這是我們公司文化的一部分:快速構(gòu)建以驗(yàn)證產(chǎn)品功能,但始終為更強(qiáng)大的解決方案留有后路。
消息存儲(chǔ)在 MongoDB 集合中,使用 channel_id 和 created_at 的單一復(fù)合索引。2015 年 11 月左右,存儲(chǔ)消息數(shù)量達(dá)到 1 億條,此時(shí),預(yù)期問(wèn)題發(fā)生了:RAM 無(wú)法再容納數(shù)據(jù)和索引,延遲開(kāi)始變得不可控,是時(shí)候遷移到更合適的數(shù)據(jù)庫(kù)了。
2.選擇合適的數(shù)據(jù)庫(kù)
選擇新數(shù)據(jù)庫(kù)之前,必須了解我們的讀/寫(xiě)模式,以及我們目前解決方案出現(xiàn)問(wèn)題的原因。
- 我們的讀取極其隨機(jī),讀寫(xiě)比例大約為 50/50。
- 管理語(yǔ)音聊天的重型 Discord 服務(wù)器幾乎不發(fā)送任何消息,這意味著它們每隔幾天只發(fā)送一兩條消息,這種服務(wù)器在一年內(nèi)發(fā)送的信息幾乎不到 1000 條。問(wèn)題是,雖然信息量很小,但卻增加了向用戶提供數(shù)據(jù)的難度。只要向用戶返回 50 條信息,就會(huì)在磁盤(pán)上產(chǎn)生許多隨機(jī)尋道,導(dǎo)致磁盤(pán)緩存被驅(qū)逐。
- 管理文字私聊的重型 Discord 服務(wù)器發(fā)送的信息數(shù)量相當(dāng)可觀,每年可輕松達(dá)到 10 萬(wàn)至 100 萬(wàn)條,請(qǐng)求數(shù)據(jù)通常都是最新的。問(wèn)題是,由于這些服務(wù)器通常只有不到 100 名成員,因此請(qǐng)求數(shù)據(jù)的速度很低,磁盤(pán)緩存中也不太可能具備這些數(shù)據(jù)。
- 大型公共聊天 Discord 服務(wù)器會(huì)發(fā)送大量信息,其中,成千上萬(wàn)的成員每天發(fā)送數(shù)千條信息,每年可發(fā)送數(shù)百萬(wàn)條信息。它們幾乎總是在請(qǐng)求最近一小時(shí)內(nèi)發(fā)送的信息,并且請(qǐng)求頻率很高。因此,數(shù)據(jù)通常都在磁盤(pán)緩存中。
- 在接下來(lái)的一年里,我們將為用戶提供更多隨機(jī)讀取數(shù)據(jù)的方式:查看過(guò)去 30 天內(nèi)的提及消息,并跳轉(zhuǎn)到歷史記錄的對(duì)應(yīng)內(nèi)容,查看并跳轉(zhuǎn)到標(biāo)記消息,以及全文搜索。以上這些功能都意味著更多的隨機(jī)讀??!
隨后,我們來(lái)定義下需求:
- 線性可擴(kuò)展性:不希望以后重新考慮解決方案或手動(dòng)重新分揀數(shù)據(jù)。
- 自動(dòng)故障轉(zhuǎn)移:我們不希望被夜間打擾,因此系統(tǒng)出現(xiàn)問(wèn)題時(shí),要盡可能讓 Discord 自動(dòng)修復(fù)。
- 低維護(hù):一旦配置完畢,它就能正常工作。我們只需在數(shù)據(jù)增長(zhǎng)時(shí)添加更多節(jié)點(diǎn)即可。
- 經(jīng)證明可行:我們喜歡嘗試新技術(shù),但不能太新。
- 可預(yù)測(cè)性:當(dāng) API 的響應(yīng)時(shí)間95%超過(guò) 80ms 時(shí)無(wú)需警報(bào),我們也不想在 Redis 或 Memcached 中緩存消息。
- 不使用 blob 存儲(chǔ):如果不斷對(duì) blob 存儲(chǔ)數(shù)據(jù)進(jìn)行反序列化并新增數(shù)據(jù),那么每秒寫(xiě)入數(shù)千條消息的速度將大打折扣。
- 開(kāi)源:將命運(yùn)掌握在自己手里,不依賴第三方公司。
Cassandra 是唯一能滿足所有要求的數(shù)據(jù)庫(kù)。添加節(jié)點(diǎn)即可擴(kuò)展,同時(shí)添加過(guò)程中可以容忍節(jié)點(diǎn)丟失,不會(huì)對(duì)應(yīng)用程序產(chǎn)生任何影響。Netflix 和蘋(píng)果等大公司已部署使用了數(shù)千個(gè) Cassandra 節(jié)點(diǎn)。相關(guān)數(shù)據(jù)連續(xù)存儲(chǔ)在磁盤(pán)上,這樣減少了數(shù)據(jù)訪問(wèn)尋址次數(shù),并且數(shù)據(jù)便于在集群中分布。Cassandra 由 DataStax 支持,但仍然是開(kāi)源的,由社區(qū)驅(qū)動(dòng)。
既然做出了選擇,我們就需要證明它確實(shí)可行。
3.數(shù)據(jù)建模
如何向新手介紹 Cassandra?最好的方式就是將其描述為一個(gè) KKV 存儲(chǔ)器,它的主鍵由兩個(gè) K 組成。
第一個(gè) K 是分區(qū)鍵,用于確定數(shù)據(jù)所在的節(jié)點(diǎn)以及在磁盤(pán)上的位置。分區(qū)中包含多行記錄,每行記錄由第二個(gè) K(即聚類(lèi)鍵)確定。聚類(lèi)鍵既是分區(qū)內(nèi)的主鍵,也是行的排序方式,可以將分區(qū)看作有序字典。這些屬性結(jié)合在一起,即可實(shí)現(xiàn)非常強(qiáng)大的數(shù)據(jù)建模。
前文提到, MongoDB 使用 channel_id 和 created_at 索引信息, 因?yàn)樗胁樵兌荚陬l道(channel)中進(jìn)行,所以channel_id 被設(shè)為分區(qū)鍵,但 created_at 并不是一個(gè)很好的聚類(lèi)鍵,因?yàn)椴煌⒌膭?chuàng)建時(shí)間可能相同。
好在Discord 的所有 ID都是雪花算法(可按時(shí)間排序),因此我們可以用它們來(lái)代替created_at。由此主鍵變成了(channel_id,message_id),其中 message_id 是雪花算法。這意味著加載頻道時(shí),可以告知 Cassandra 掃描消息的準(zhǔn)確范圍。
下面是消息表的簡(jiǎn)化模式(省略了約 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 的 schema 與關(guān)系數(shù)據(jù)庫(kù)的模式有很大差別,Cassandra 的 schema 更改成本相較更低,而且不會(huì)對(duì)性能造成任何臨時(shí)性影響,因此,我們獲得了 blob 存儲(chǔ)和關(guān)系型存儲(chǔ)的最佳效果。
當(dāng)我們開(kāi)始將現(xiàn)有信息導(dǎo)入 Cassandra 時(shí),日志立刻出現(xiàn)告警,提醒分區(qū)的大小超過(guò)了 100MB。這是怎么回事?Cassandra 宣稱可以單個(gè)分區(qū)可支持 2GB!顯然,理論性能并不意味著實(shí)際應(yīng)用效果。
在壓縮、集群擴(kuò)展等操作過(guò)程中,大分區(qū)會(huì)給 Cassandra 帶來(lái)很大的 GC 壓力。同時(shí),大分區(qū)還意味著其中的數(shù)據(jù)無(wú)法分布在集群中。由于 Discord 頻道(channel)將存在數(shù)年,且持續(xù)增長(zhǎng)擴(kuò)大,所以必須限制分區(qū)的大小。
我們決定按時(shí)間分類(lèi)信息,參考Discord 上最大頻道,確定如果在一個(gè)桶中存儲(chǔ)約 10 天的消息,就可以輕松地將容量控制在 100MB 以下。桶必須從消息 ID 或時(shí)間戳中歸并。
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ū)鍵可復(fù)合,因此新主鍵變成了((channel_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);
查詢通道中最近的消息,需要生成一個(gè)從當(dāng)前時(shí)間到 channel_id 的桶范圍(它也是雪花算法,必須比第一條消息更早)。然后,按順序查詢分區(qū),直到收集到足夠多的信息。
這種方法的缺點(diǎn)是,不活躍的 Discord 頻道需要查詢多個(gè)分區(qū)才能收集到足夠多的信息。不過(guò)該方法在實(shí)踐中證明可行,因?yàn)閷?duì)于活躍的 Discord 頻道來(lái)說(shuō),通常在第一個(gè)分區(qū)中就能找到足夠的消息,且這種情況占多數(shù)。
將消息導(dǎo)入 Cassandra 的過(guò)程非常順利,我們已做好了遷移到生產(chǎn)環(huán)境的準(zhǔn)備。
4.驚險(xiǎn)的啟動(dòng)
將一個(gè)新系統(tǒng)引入生產(chǎn)環(huán)境總是令人恐懼的,因此最好在不影響用戶的情況下進(jìn)行測(cè)試。我們將代碼設(shè)置為 MongoDB 和 Cassandra 的雙重讀/寫(xiě)。
啟動(dòng)后,我們的錯(cuò)誤(bug)跟蹤器立即收到錯(cuò)誤信息,提示稱 author_id 為空(null)。怎么回事?這是一個(gè)必填字段!
讓我們先一起回顧下問(wèn)題產(chǎn)生的背景。
5.最終一致性
Cassandra 是 AP 數(shù)據(jù)庫(kù),這意味著它犧牲了強(qiáng)一致性以換取可用性,而這正是我們想要的。在 Cassandra 中,“先讀后寫(xiě)”是一種反模式(讀取比寫(xiě)入成本更高),因此,即使只訪問(wèn)某些列,在 Cassandra 上本質(zhì)也會(huì)成為一個(gè)更新插入操作。你也可以向任何節(jié)點(diǎn)寫(xiě)入數(shù)據(jù),它將根據(jù)每一列的情況,使用“l(fā)ast write wins”的策略自動(dòng)解決沖突。這對(duì)我們有什么影響?
編輯/刪除 race condition 的例子
以上動(dòng)圖例子中,在一個(gè)用戶編輯消息的同時(shí),另一個(gè)用戶刪除了同一條消息。由于 Cassandra 寫(xiě)入時(shí)執(zhí)行更新插入操作,因此我們最終發(fā)現(xiàn)記錄中只有主鍵和文本外,缺少其余數(shù)據(jù)。
有兩種可能解決方案處理這個(gè)問(wèn)題:
- 編輯信息時(shí),寫(xiě)回整條信息。這有可能找回已刪除的信息,但也增加并發(fā)寫(xiě)入其他列時(shí)發(fā)生沖突的機(jī)會(huì)。
- 找出已損壞的信息并從數(shù)據(jù)庫(kù)中刪除。
我們采用了第二種方法,即選擇一個(gè)必填列(本例中為 author_id),如果該列為空,則刪除該消息。
解決這個(gè)問(wèn)題時(shí),我們注意到我們的寫(xiě)入效率非常低。由于 Cassandra 被設(shè)計(jì)為最終一致性,因此它執(zhí)行刪除操作時(shí)不會(huì)立即刪除數(shù)據(jù),必須將刪除復(fù)制到其他節(jié)點(diǎn)。即使其他節(jié)點(diǎn)暫時(shí)不可用,也要執(zhí)行刪除操作。
Cassandra 將刪除作為一種名為“墓碑”的寫(xiě)入形式來(lái)處理。在讀取時(shí),它會(huì)跳過(guò)遇到的墓碑。墓碑的存活時(shí)間可配置(默認(rèn)為 10 天),過(guò)期后會(huì)在壓縮過(guò)程中被永久刪除。
刪除列和將空值(null)寫(xiě)入列完全是一回事。它們都會(huì)生成墓碑。由于 Cassandra 中的所有寫(xiě)入都是更新插入,這意味著即使第一次寫(xiě)入空值也會(huì)生成墓碑。實(shí)際上,我們的整個(gè)消息模式包含 16 個(gè)列,但平均每條消息長(zhǎng)度僅有 4 個(gè)值。這導(dǎo)致插入新一行數(shù)據(jù)時(shí),大部分時(shí)間都在無(wú)緣無(wú)故地向 Cassandra 寫(xiě)入 12 個(gè)墓碑。
解決這個(gè)問(wèn)題的辦法很簡(jiǎn)單:只向 Cassandra 寫(xiě)入非空值。
6.性能
眾所周知,Cassandra 的寫(xiě)入速度比讀取速度快,我們的觀察結(jié)果也是如此:寫(xiě)入速度低于1毫秒,讀取速度低于 5 毫秒。無(wú)論訪問(wèn)什么數(shù)據(jù),觀察結(jié)果都一致,并且性能在一周的測(cè)試中始終如一。意料之中,我們得到了所期望的數(shù)據(jù)庫(kù)。
通過(guò) Datadog 查看讀寫(xiě)延遲
快速、一致的讀取性能可以通過(guò)以下例子表現(xiàn):在數(shù)百萬(wàn)條信息的頻道中跳轉(zhuǎn)到一年前的某條消息。
跳轉(zhuǎn)到一年前的聊天記錄
7.巨大的意外
一切都很順利,我們將 Cassandra 切換為主數(shù)據(jù)庫(kù),并在一周內(nèi)淘汰掉 MongoDB 。Cassandra 完美地運(yùn)行了約 6 個(gè)月,直到有一天變得反應(yīng)遲鈍。
我們注意到 Cassandra 持續(xù)出現(xiàn) 10 秒鐘的 GC 全停頓("stop-the-world "GC),但原因未知。我們開(kāi)始定位問(wèn)題,發(fā)現(xiàn)加載 Discord 頻道需要 20 秒。一個(gè)名為“Puzzles & Dragons Subreddit”的公共 Discord 服務(wù)器是罪魁禍?zhǔn)?。由于它是一個(gè)開(kāi)放的服務(wù)器,我們加入進(jìn)去一探原因。
令人驚訝的是,頻道里只有一條信息。同時(shí)我們發(fā)現(xiàn),他們使用我們的 API 刪除了數(shù)百萬(wàn)條信息,只在頻道中留下了 1 條信息。
上文(談及最終一致性時(shí))提到過(guò), Cassandra 是如何使用墓碑處理刪除操作。當(dāng)用戶加載該頻道時(shí),即使只有一條消息,Cassandra 也必須掃描數(shù)以百萬(wàn)計(jì)的消息墓碑(產(chǎn)生垃圾的速度比 JVM 收集垃圾的速度更快)。
我們通過(guò)以下方法解決了這個(gè)問(wèn)題:
- 因?yàn)槲覀兠客矶紩?huì)在消息集群上運(yùn)行 Cassandra 修復(fù)(反熵進(jìn)程),所以將墓碑的生命周期從 10 天縮短到 2 天。
- 修改查詢代碼,以跟蹤空桶,并在未來(lái)避免在頻道中出現(xiàn)空桶。這意味著,如果用戶再次進(jìn)行此查詢,即使是最壞的情況下,Cassandra 也只能掃描最近的數(shù)據(jù)桶。
8.未來(lái)發(fā)展規(guī)劃
目前正在運(yùn)行一個(gè)復(fù)制系數(shù)為 3 的 12 節(jié)點(diǎn)集群,并將根據(jù)意外所需繼續(xù)添加新的 Cassandra 節(jié)點(diǎn),我們相信在很長(zhǎng)一段時(shí)間內(nèi)這個(gè)集群可以持續(xù)高效運(yùn)行。
但隨著 Discord 的不斷發(fā)展,在遙遠(yuǎn)的未來(lái),有可能每天需要存儲(chǔ)數(shù)十億條消息。Netflix 和蘋(píng)果公司運(yùn)行著數(shù)百個(gè)節(jié)點(diǎn)的集群,因此我們知道這個(gè)階段不用過(guò)多顧慮。不過(guò),我們還是準(zhǔn)備了一些未雨綢繆的計(jì)劃。
1)近期計(jì)劃
- 將我們的消息集群從 Cassandra 2 升級(jí)到 Cassandra 3。Cassandra 3 有一種新的存儲(chǔ)格式,可將存儲(chǔ)大小減少 50% 以上。
- 新版本的 Cassandra 更善于在單個(gè)節(jié)點(diǎn)上處理更多數(shù)據(jù)。我們目前在每個(gè)節(jié)點(diǎn)上存儲(chǔ)近 1TB 的壓縮數(shù)據(jù)。我們相信,只要將其提升到 2TB,就能安全地減少集群中的節(jié)點(diǎn)數(shù)量。
2)長(zhǎng)期計(jì)劃
- 探索使用 C++ 編寫(xiě)的 Cassandra 兼容數(shù)據(jù)庫(kù) Scylla 。在正常運(yùn)行期間,我們的 Cassandra 節(jié)點(diǎn)占用的 CPU 并不多,但在非高峰時(shí)段,當(dāng)我們運(yùn)行修復(fù)(一個(gè)反熵進(jìn)程)時(shí),CPU 占用就非常高。同時(shí),在上一次修復(fù)后,修復(fù)持續(xù)時(shí)間和寫(xiě)入的數(shù)據(jù)量也會(huì)增加。Scylla 宣稱能大大縮短修復(fù)時(shí)間。
- 建立一個(gè)系統(tǒng),將未使用的頻道歸檔到谷歌云存儲(chǔ)的文件中,并按需加載回來(lái)。其實(shí)我們不希望這樣做,所以這一計(jì)劃未必會(huì)落實(shí)。
9.結(jié)論
盡管經(jīng)歷“巨大的意外”,但我們的切換過(guò)程一直很順利。每日信息總量從 1 億多條增加到 1.2 億多條,也一直保持著良好的性能與穩(wěn)定性。
由于這個(gè)項(xiàng)目的成功,我們已經(jīng)將其余的實(shí)時(shí)生產(chǎn)數(shù)據(jù)轉(zhuǎn)移到 Cassandra,并且也取得了成功。
2023:從Cassandra到 ScyllaDB
2023年,Discord 使用的 Cassandra 集群出現(xiàn)嚴(yán)重的性能問(wèn)題,技術(shù)人員耗費(fèi)越來(lái)越多的精力,致力于維護(hù)數(shù)據(jù)庫(kù),而非改進(jìn)性能。
時(shí)隔六年,Discord 消息存儲(chǔ)再面臨性能挑戰(zhàn),存儲(chǔ)遷移刻不容緩。
1.Cassandra的存儲(chǔ)痛點(diǎn)
Discord 將信息存儲(chǔ)在名為 cassandra-messages 的數(shù)據(jù)庫(kù)中。顧名思義,它運(yùn)行 Cassandra 以存儲(chǔ)信息。2017年,Discord 運(yùn)行12個(gè) Cassandra 節(jié)點(diǎn),存儲(chǔ)數(shù)十億條信息。
截至2022年初,以上系統(tǒng)擁有177個(gè)節(jié)點(diǎn)和數(shù)萬(wàn)億條消息,Cassandra 出現(xiàn)了嚴(yán)重的性能問(wèn)題。由于不可預(yù)測(cè)的數(shù)據(jù)庫(kù)延遲等問(wèn)題,技術(shù)團(tuán)隊(duì)必須隨時(shí)保持聯(lián)系,減少運(yùn)維操作,避免增加系統(tǒng)運(yùn)行成本。
痛點(diǎn)從何產(chǎn)生?讓我們來(lái)看以下這條消息。
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);
以上 CQL 語(yǔ)句是消息模式的最小版本。Discord 使用的每個(gè) ID 都通過(guò)Snowflake 生成,因此可按時(shí)間排序。根據(jù)消息發(fā)送的通道以及一個(gè)存儲(chǔ)桶(一個(gè)靜態(tài)時(shí)間窗口)來(lái)劃分消息。這種分區(qū)意味著,在 Cassandra 中,給定通道和存儲(chǔ)桶的所有消息都將存儲(chǔ)在一起,并跨三個(gè)節(jié)點(diǎn)(或設(shè)置的任何復(fù)制系數(shù))進(jìn)行復(fù)制。
在 Cassandra 中,讀取比寫(xiě)入的成本高。寫(xiě)入操作被附加到提交日志中,并寫(xiě)入名為內(nèi)存表(memtable)的內(nèi)存結(jié)構(gòu)中,最終被刷新到磁盤(pán)。然而,讀取需要查詢內(nèi)存表(memtable)和可能的多個(gè)磁盤(pán)文件(SSTable),操作成本相當(dāng)高。
用戶與服務(wù)器交互時(shí),大量并發(fā)讀取使分區(qū)出現(xiàn)熱點(diǎn),一般將其稱為“熱分區(qū)”。一旦數(shù)據(jù)集的規(guī)模與這些訪問(wèn)模式結(jié)合,導(dǎo)致 Cassandra 集群出現(xiàn)問(wèn)題。
熱分區(qū)經(jīng)常導(dǎo)致整個(gè)數(shù)據(jù)庫(kù)集群延遲。通道與存儲(chǔ)桶組合接收大量流量,隨著節(jié)點(diǎn)服務(wù)流量越發(fā)吃力,節(jié)點(diǎn)延遲就越發(fā)嚴(yán)重。
由于節(jié)點(diǎn)速度無(wú)法跟上,對(duì)該節(jié)點(diǎn)的其他查詢受到影響。由于我們使用仲裁一致性級(jí)別執(zhí)行讀寫(xiě)操作,因此服務(wù)于熱分區(qū)的節(jié)點(diǎn)的所有查詢都會(huì)影響,延遲增加,從而對(duì)最終用戶產(chǎn)生更廣泛的影響。
集群維護(hù)任務(wù)也經(jīng)常引起麻煩。我們很容易在壓縮上落后,Cassandra 會(huì)壓縮磁盤(pán)上的SSTable以獲得更高的性能讀取。這樣一來(lái),我們的讀取成本不僅會(huì)更高,并且由于節(jié)點(diǎn)試圖壓縮,還會(huì)產(chǎn)生級(jí)聯(lián)延遲。
我們經(jīng)常執(zhí)行一種被稱為“八卦舞蹈”的操作,即停止一個(gè)節(jié)點(diǎn)的輪換,讓它在不占用流量的情況下進(jìn)行壓縮,將其重新加入輪換,從 Cassandra 獲取切換提示,然后重復(fù)這個(gè)操作,直至壓縮積壓的信息被清空。由于 GC 暫停會(huì)導(dǎo)致顯著的延遲峰值,我們還花了大量時(shí)間調(diào)優(yōu) JVM 的垃圾收集器和堆設(shè)置。
2.架構(gòu)遷移
消息集群并非唯一的 Cassandra 數(shù)據(jù)庫(kù),我們還具備其他幾個(gè)集群,每個(gè)集群都表現(xiàn)出類(lèi)似的缺點(diǎn)(雖然可能沒(méi)有那么嚴(yán)重)。
ScyllaDB 引起了我們的興趣,這是一個(gè)用 C++ 編寫(xiě)的與 Cassandra 兼容的數(shù)據(jù)庫(kù)。它承諾提供更好的性能、更快的修復(fù)、通過(guò)核分片架構(gòu)實(shí)現(xiàn)更強(qiáng)的工作負(fù)載隔離,以及無(wú)垃圾回收,聽(tīng)起來(lái)相當(dāng)吸引人。
當(dāng)然ScyllaDB也存在不足之處,由于采用 C++ 編譯而不是 Java,所以它沒(méi)有垃圾收集器。過(guò)去,我們的團(tuán)隊(duì)在 Cassandra 上的垃圾收集器上遇到許多問(wèn)題,包括影響延遲的 GC 暫停、一直到超長(zhǎng)的連續(xù) GC 暫停,以至于操作員必須手動(dòng)重新啟動(dòng),問(wèn)題節(jié)點(diǎn)才能恢復(fù)健康狀態(tài)。這些問(wèn)題導(dǎo)致技術(shù)團(tuán)隊(duì)必須隨叫隨到,同時(shí),這些問(wèn)題也是影響消息集群穩(wěn)定性問(wèn)題的根源。
在對(duì) ScyllaDB 進(jìn)行試驗(yàn),并觀察到測(cè)試中的改進(jìn)成效后,我們決定遷移所有數(shù)據(jù)庫(kù)。雖然這項(xiàng)決定本身可以用一篇博客來(lái)介紹,但簡(jiǎn)而言之,截至2020年,我們已經(jīng)將所有數(shù)據(jù)庫(kù)遷移到 ScyllaDB,除了一個(gè)數(shù)據(jù)庫(kù)—— cassandra-messages 。
為什么我們還未開(kāi)始遷移?首先,集群規(guī)模巨大,包括數(shù)以萬(wàn)億計(jì)的消息和近200個(gè)節(jié)點(diǎn),任何遷移操作都非常復(fù)雜。此外,我們希望調(diào)整新數(shù)據(jù)庫(kù)時(shí),其性能達(dá)到最佳狀態(tài)。我們還想在生產(chǎn)環(huán)境中積累使用 ScyllaDB 的更多經(jīng)驗(yàn),了解其缺陷。
針對(duì)我們的用例,還改進(jìn)了 ScyllaDB 的性能。我們?cè)跍y(cè)試中發(fā)現(xiàn),反向查詢的性能不足以滿足需求。以與表排序相反的順序進(jìn)行數(shù)據(jù)庫(kù)掃描時(shí),例如按升序掃描消息時(shí),則執(zhí)行反向查詢。ScyllaDB 團(tuán)隊(duì)優(yōu)先對(duì)其進(jìn)行改進(jìn),實(shí)現(xiàn)了高性能的反向查詢,清除了我們遷移計(jì)劃中的“最后一個(gè)數(shù)據(jù)庫(kù)”的障礙。
我們擔(dān)心,在系統(tǒng)上添加新數(shù)據(jù)庫(kù)不太可能有翻天覆地的性能改進(jìn),“熱分區(qū)”的問(wèn)題依然存在于 ScyllaDB ,因此,我們寄希望于投資改進(jìn)數(shù)據(jù)庫(kù)上游系統(tǒng),以助于數(shù)據(jù)庫(kù)屏蔽和提升數(shù)據(jù)庫(kù)性能。
3.使用數(shù)據(jù)服務(wù)提供數(shù)據(jù)
針對(duì) Cassandra ,我們遇到了熱分區(qū)的難題,分給特定分區(qū)的高流量會(huì)引起無(wú)限并發(fā),進(jìn)而導(dǎo)致級(jí)聯(lián)延遲,延長(zhǎng)后續(xù)查詢時(shí)間。如果能夠控制熱分區(qū)的并發(fā)流量,就可保護(hù)數(shù)據(jù)庫(kù)免于重負(fù)。
為了完成這項(xiàng)任務(wù),我們編寫(xiě)了所謂的數(shù)據(jù)服務(wù)——位于 API 單體和數(shù)據(jù)庫(kù)集群之間的中介服務(wù)。編寫(xiě)數(shù)據(jù)服務(wù)時(shí),我們選擇了一種在 Discord 應(yīng)用越發(fā)廣泛的語(yǔ)言:Rust!它能在保障安全性的前提下,提供可與 C/ C++ 媲美的高速度。
Rust 的主要優(yōu)勢(shì)之一是無(wú)懼并發(fā)——該語(yǔ)言使編寫(xiě)安全并發(fā)代碼變得容易。它的庫(kù)也非常適合完成我們的其他工作,Tokio生態(tài)系統(tǒng)是構(gòu)建異步I/O系統(tǒng)的監(jiān)視基礎(chǔ),并且該語(yǔ)言也對(duì) Cassandra 和 ScyllaDB 提供驅(qū)動(dòng)程序支持。
此外,Rust 編譯器提供的幫助、清晰的錯(cuò)誤消息、語(yǔ)言結(jié)構(gòu)以及對(duì)安全性的重視,編寫(xiě)代碼變得很有趣。Rust 程序一旦通過(guò)編譯,就可以運(yùn)行,這讓我們很滿意。最重要的是,我們用Rust 進(jìn)行重寫(xiě)(meme/模因信譽(yù)非常重要)。
我們的數(shù)據(jù)服務(wù)位于 API 和 ScyllaDB 集群之間。每個(gè)數(shù)據(jù)庫(kù)查詢大約包含一個(gè)gRPC 端點(diǎn),并且故意不包含業(yè)務(wù)邏輯。數(shù)據(jù)服務(wù)一大特點(diǎn)是請(qǐng)求合并,如果多個(gè)用戶同時(shí)請(qǐng)求同一行,我們將只查詢一次數(shù)據(jù)庫(kù)。首個(gè)發(fā)出請(qǐng)求的用戶會(huì)觸發(fā)數(shù)據(jù)服務(wù)中的工作任務(wù),后續(xù)請(qǐng)求將檢查該任務(wù)是否存在并訂閱它,該工作任務(wù)將查詢數(shù)據(jù)庫(kù)并將該行返回給所有訂閱者。
這就是 Rust 的強(qiáng)大之處:它使編寫(xiě)安全的并發(fā)代碼變得輕松。
讓我們想象一下,大型服務(wù)器有一條@所有人的重要公告:用戶將打開(kāi)應(yīng)用程序并閱讀消息,向數(shù)據(jù)庫(kù)發(fā)送大量流量。以往,這可能會(huì)導(dǎo)致熱分區(qū),并且可能需要工程師隨時(shí)保持待機(jī),以幫助系統(tǒng)恢復(fù)。數(shù)據(jù)服務(wù)能夠顯著減少數(shù)據(jù)庫(kù)的流量峰值。
第二個(gè)神奇之處在于數(shù)據(jù)服務(wù)的上游。為實(shí)現(xiàn)更有效的合并,我們實(shí)現(xiàn)了一致的基于哈希的數(shù)據(jù)服務(wù)路由,為每個(gè)數(shù)據(jù)服務(wù)請(qǐng)求提供路由鍵。對(duì)消息來(lái)說(shuō),這是一個(gè)通道ID,因此對(duì)同一通道的所有請(qǐng)求都將轉(zhuǎn)到服務(wù)的同一實(shí)例,這種路由方式有助于進(jìn)一步減少數(shù)據(jù)庫(kù)的負(fù)載。
這些改進(jìn)頗有助益,但并未解決所有問(wèn)題。,但它們并不能解決我們所有的問(wèn)題。Cassandra集群上仍然存在熱分區(qū)和延遲增加,只是不那么頻繁了,這為我們贏得了一些時(shí)間,以便準(zhǔn)備最優(yōu)ScyllaDB集群并執(zhí)行遷移。
4.大規(guī)模數(shù)據(jù)遷移
我們的遷移需求非常簡(jiǎn)單:在不停機(jī)的情況下遷移數(shù)萬(wàn)億條消息,并快速完成。正如上文所述,雖然 Cassandra 的情況有所改善,但還常常出現(xiàn)問(wèn)題。
第一步很簡(jiǎn)單:使用超級(jí)磁盤(pán)存儲(chǔ)拓?fù)渑渲靡粋€(gè)新的 ScyllaDB 集群。
使用本地 SSD 提高速度,并利用 RAID 將數(shù)據(jù)鏡像到持久磁盤(pán),由此我們同時(shí)獲得了連接本地磁盤(pán)的速度和持久磁盤(pán)的持久性。集群建立后,就可以開(kāi)始向其遷移數(shù)據(jù)了。
我們的初版遷移技術(shù)旨在快速獲取價(jià)值。我們將開(kāi)始使用全新的 ScyllaDB 集群來(lái)處理新數(shù)據(jù),在切換時(shí)間內(nèi)遷移歷史數(shù)據(jù)。這項(xiàng)操作增加了復(fù)雜性,但每個(gè)大型項(xiàng)目都無(wú)法避免額外的復(fù)雜性,對(duì)吧?
然后,向 Cassandra 和 ScyllaDB 雙重寫(xiě)入新數(shù)據(jù),并同時(shí)開(kāi)始準(zhǔn)備 ScyllaDB 的 Spark 遷移器。這需要大量調(diào)整,一旦設(shè)置完成,我們預(yù)計(jì)三個(gè)月能夠完成遷移。
這個(gè)時(shí)間期限讓我們并不滿意,因?yàn)槲覀兿M飓@取價(jià)值。所以,我們組織了一場(chǎng)團(tuán)隊(duì)會(huì)議,集思廣益,思考如何增速——我們已經(jīng)編寫(xiě)了一個(gè)快速和高性能的數(shù)據(jù)庫(kù),可以對(duì)其進(jìn)行拓展。因而我們選擇參與了一些模因驅(qū)動(dòng)工程,用Rust重寫(xiě)數(shù)據(jù)遷移器。
某一天的下午,我們擴(kuò)展了數(shù)據(jù)服務(wù)庫(kù)以便執(zhí)行大規(guī)模數(shù)據(jù)遷移。它從數(shù)據(jù)庫(kù)讀取令牌范圍,通過(guò) SQLite 在本地檢查,然后將它們送入 ScyllaDB 。連接改進(jìn)后的新遷移器后,我們重新預(yù)估工期:9天!如果可以這么迅速地遷移數(shù)據(jù),就可以放棄基于時(shí)間的復(fù)雜方式,一次性切換所有數(shù)據(jù)。
啟動(dòng)遷移器并讓其保持運(yùn)行,以每秒320萬(wàn)的速度遷移信息。幾天后,遷移進(jìn)度達(dá)到100%,但我們意識(shí)到它停留在99.9999%的完成度。遷移器在讀取數(shù)據(jù)的最后幾個(gè)令牌范圍時(shí)超時(shí)了,因?yàn)樗鼈儼?Cassandra 中未壓縮過(guò)的巨大墓碑范圍。我們將這個(gè)令牌范圍壓實(shí),幾秒鐘后,遷移完成!
執(zhí)行自動(dòng)數(shù)據(jù)驗(yàn)證,即通過(guò)向兩個(gè)數(shù)據(jù)庫(kù)發(fā)送一小部分讀取數(shù)據(jù)并比較結(jié)果,一切看起來(lái)都很好。在全生產(chǎn)流量的情況下,集群表現(xiàn)良好,而 Cassandra 卻遭受越來(lái)越頻繁的延遲問(wèn)題。我們團(tuán)隊(duì)聚集在現(xiàn)場(chǎng),打開(kāi)開(kāi)關(guān)使 ScyllaDB 成為主數(shù)據(jù)庫(kù)。
5.數(shù)據(jù)庫(kù)遷移效果
Discord 在2022年5月切換了消息數(shù)據(jù)庫(kù),遷移效果如何?
運(yùn)行的177個(gè) Cassandra 節(jié)點(diǎn)減少到72個(gè) ScyllaDB 節(jié)點(diǎn),每個(gè) ScyllaDB 節(jié)點(diǎn)有9tb的磁盤(pán)空間,高于每個(gè) Cassandra 節(jié)點(diǎn)平均 4tb 的磁盤(pán)空間。
我們的尾部延遲也大大改善了。例如,在 Cassandra 上獲取歷史消息的p99在40-125ms之間,ScyllaDB 的p99延遲在15ms之間,消息插入性能從 Cassandra 上的5-70ms p99上升到 ScyllaDB 上穩(wěn)定的5ms p99。
2022年底,全球觀眾都在收看世界杯。技術(shù)人員發(fā)現(xiàn),Discord 的監(jiān)控圖表可以展示決賽的進(jìn)球情況。這為技術(shù)團(tuán)隊(duì)提供了一個(gè)在會(huì)議期間觀看足球比賽的借口——不是“在會(huì)議期間看足球比賽”,而是“主動(dòng)監(jiān)控系統(tǒng)性能”。
以上的信息發(fā)送數(shù)量圖,描繪了世界杯決賽的精彩發(fā)展,圖中的每個(gè)峰值分別代表比賽中的重要節(jié)點(diǎn)。
1. 梅西罰進(jìn)點(diǎn)球,阿根廷1比0領(lǐng)先。
2. 阿根廷再次得分,以2比0領(lǐng)先。
3. 中場(chǎng)休息。當(dāng)用戶談?wù)摫荣悤r(shí),會(huì)持續(xù)15分鐘。
4. 姆巴佩為法國(guó)隊(duì)進(jìn)球,90秒后再次進(jìn)球?qū)⒈确职馄?
5. 規(guī)則結(jié)束了,這場(chǎng)重要的比賽將進(jìn)入加時(shí)賽。
6. 在加時(shí)賽的前半段沒(méi)有太多的事情發(fā)生,但我們到了中場(chǎng)休息,用戶開(kāi)始聊天。
7. 梅西再次得分,阿根廷隊(duì)領(lǐng)先!
8. 姆巴佩反擊將比分扳平!
9. 加時(shí)賽結(jié)束了,我們要進(jìn)點(diǎn)球了!
- 在整個(gè)點(diǎn)球大戰(zhàn)中,興奮和壓力不斷增加,直到法國(guó)隊(duì)失誤,而阿根廷隊(duì)沒(méi)有!阿根廷贏了!
每秒合并消息數(shù)
世界各地的人們都在觀看這場(chǎng)激動(dòng)人心的比賽,與此同時(shí),Discord 使用基于 rust 的數(shù)據(jù)服務(wù)和 ScyllaDB ,不費(fèi)吹灰之力便承擔(dān)了比賽產(chǎn)生的巨大流量,同時(shí)為用戶提供交流平臺(tái)。
參考資料
- HOW DISCORD STORES BILLIONS OF MESSAGEShttps://discord.com/blog/how-discord-stores-billions-of-messages
- HOW DISCORD STORES TRILLIONS OF MESSAGEShttps://discord.com/blog/how-discord-stores-trillions-of-messages
作者丨Stanislav Vishnevskiy & Bo Ingram
編譯丨onehunnit