频道聊天顶流 Discord,如何实现亿级消息架构迁移

image.png

2016 年 7 月,Discord 宣布日消息量达到 4000 万一天,12 月达到一亿条

为了应对如此巨量的数据,技术团队在 2015 年 11 月时达到 100 万条消息时,一改年初的单副本 MongoDB 架构,最终迁移到 Cassandra 数据库。

当前的问题

采用单副本的 MongoDB 满足 CAP 理论的 CA,即保证一致性和可用性,但无法容忍网络分区。当出现网络分区问题时,服务将不可用。

随着数据量增长,内存不足以支撑对应量级的索引和数据,出现无法预测的延时。

业务情况

  1. Discord 随机读多,重度写,读写比例接近 1:1。
  2. 不同频道数据倾斜明显,技术团队构建 MongoDB 索引时用的是 channel_id + create_at(渠道ID+创建时间)。

其中语音频道每日消息个位数,私有频道每年消息在万级别,而公有频道则读写均很多,主要读近期数据,每年消息量超过百万。

MongoDB 能否适应这些问题

MongoDB 将活跃的数据和索引尽可能加载到内存中,以提高读写性能。

当访问语音频道或私人频道,大概率数据不在缓存中,需要从磁盘获取,产生磁盘 IO 延迟增大。

同时驱逐了其他公有频道的缓存,造成其他频道的访问延迟也增大

而大量写入操作需要更新数据和索引,同样要先读数据,因 MongoDB 强调一致性,还会碰到写竞争问题,进一步拖慢性能。

不迁移架构,最简单的是服务器升配,无限制增加单机磁盘和内存是不可能的。

从单副本到分片是很自然的选择,Discord 却放弃了,为什么?

分片复杂且不够稳定

  1. 分片迁移复杂性

从单副本到多分片,涉及对数据的一致性、可用性以及性能的管理,可能会引发大量写入导致服务不可用

  1. 全局一致性难以保证

MongoDB 默认为 CA 保证一致性,涉及多个用户同时修改、查询时,会有写入竞争和锁定。

  1. 再分片复杂性

不同数据类别(文字、语音)和不同公开程度(私有、公开)的频道存在明显的数据倾斜问题。

此时可能需要重新划分分片,又会出现分片迁移类似的问题。

新数据库满足什么

从上面的分析看,数据库需支持重度写、同时易于分片(扩展)。

技术团队还提出这些要求,最终 Cassandra 胜出了。

  1. 线性扩展性:不需要在数据增长时重新分片或手动管理节点。
  2. 自动故障切换:确保高可用性,降低运维成本。
  3. 低维护成本:一旦部署,只需随着数据增长添加新节点。
  4. 性能可预测:能满足重度写和随机读。不需要额外引入缓存层。
  5. 开源:Discord 希望自己控制,不依赖第三方公司。

迁移-数据建模优化

数据库选型要考虑数据迁移和新数据的写入。

Cassandra 的数据是自动分片,集群是线性可扩展的。这一切来自于 KKV(Key-Key-Value)的设计,比我们熟知的 K-V 多了一个 K,第一个 K 是分区键(Partition Key),定义数据的逻辑分区,是实现水平扩展和负载均衡的关键。第二个键是找到对应记录的 Clustering Key,用于分区内部对数据排序。

分区键需要唯一定位到节点,Clustering 键需要单调递增。

image.png

创建时间不能作为分区键,因为无法唯一定位到节点,无效查询多个节点。

Discord 选择用 ChannelID - MessageID - Mesaage 作为 KKV,MessageID 为可排序的雪花算法生成的ID,单调递增。

再分区-解决数据倾斜

但数据倾斜问题还是存在,大分区情况下,Cassandra 会在压缩时引发 GC。擒贼先擒王,技术团队分析了最大的频道的数据分布,发现 10 天作为切割可以将分区控制在 100M 以内。


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)

于是新的 KKV 变成 ((channel_id, bucket), message_id),对于某个大频道 Pin 的数据,要回到当时的聊天时间点,只需根据消息 ID 计算对应的 Bucket 范围,就可以直接定位到节点,无需盲扫。

对于大频道,大概率只需要扫描最近的一个桶就可以满足一次数据拉取的量。

缺点是对于不活跃的频道、或语音频道等,这类时间分布稀疏的,查看数据必须跨多个 Bucket 才可以收集足够的数据范围。

你可能会问,那要是频道的数据停在几年前,岂不是全表扫描了。

答案是一般以月或年为单位,限制一次请求最多可以跨的桶数,因此这个性能是可预见的

最终一致性

MongoDB 的一致性是 Read Before Write,写之前必须查同时要加锁。

Cassandra 是一个 AP 数据库,是最终一致性,以最后一次写入为准,不存在写入竞争。在写入性能上天然比强一致性的数据库有优势。

编辑、删除竞争问题

创建消息时,Discord 严格限制了必须有作者等额外信息字段,但却出现了作者信息为空的数据!

Discord 允许同时多个用户同时修改一条数据,当有用户编辑数据时,另一个用户可以删除。

创建虽然要求必须有作者等额外的字段,但编辑却没有,如编辑请求晚于删除,就会存在空字段

两个方案,直接以全字段上报编辑请求,另一个是当检测到字段不全时标记为删除,Discord 选择了后者。

标记删除的墓碑问题

Cassandra 的删除也是一种 upsert 操作,数据像墓碑一样标标记出来,当读到该块数据时,这些墓碑数据就会被跳过,过了一段时间才被数据库压缩删除。

在迁移后的半年,Discord 碰到了一个诡异的情况:

访问一个叫 PuzzleAndDragon 频道,里面只有一条数据,却会触发 10s 的 GC 且加载要花费长达 20s 的时间。

原来这个频道的消息被大量删除了,有别于不活跃频道大部分时间是在读空桶,这里的数据虽然被标记删除,但进入这个频道依旧需要扫描上百万的消息。

最终,技术团队找到了两个点:

  1. 降低删除数据的生命周期,从 10 天改到 2 天
  2. 跟踪 (channel, bucket) 中无任何数据的 Bucket 分片,查询时跳过这些分片,不读取。

结论

数据存储架构的迁移不是对新技术的盲目追求,是否到了非改不可的地步,需要给出明确的原因。

迁移的选型离不开对业务的读写模式、数据分布做分析,同时对扩展性留有余地

例如本文中涉及了对分区键的扩展修改,如果选择 ClickHouse(截止到 2024)则又涉及到对数据表的重建。

如同 CAP 是无法达成的完美三角,选型也需要我们根据业务做出合理取舍,技术就是这样。

参考文章

  1. How Discord Stores Billions of Messages (https://discord.com/blog/how-discord-stores-billions-of-messages)
  2. Cassandra Basics (https://cassandra.apache.org/_/cassandra-basics.html)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容