网站首页 资讯 热点 行情 地区 推荐 民宿 酒店 家居 度假 滚动
首页 >  滚动 >  >  正文

Discord如何存储万亿级消息

2023-07-28 12:35:57来源:哔哩哔哩

作者:Discord 高级软件工程师 Bo Ingram


(资料图片仅供参考)

2017 年,我们撰写了一篇关于如何存储数十亿条消息的博文。 我们分享了我们如何开始使用 MongoDB,但后来将数据迁移到 Cassandra,因为我们正在寻找一个可扩展、容错且维护成本相对较低的数据库。 我们知道我们会成长,而且我们确实做到了!

我们想要一个与我们一起成长的数据库,但希望它的维护需求不会随着我们的存储需求而增长。 不幸的是,我们发现事实并非如此——我们的 Cassandra 集群出现了严重的性能问题,需要付出越来越多的努力来维护,而不是改进。

大约六年后,我们发生了很多变化,我们存储消息的方式也发生了变化。

我们的Cassandra 麻烦

我们将消息存储在名为 cassandra-messages 的数据库中。 顾名思义,它运行 Cassandra,并存储消息。 2017 年,我们运行了 12 个 Cassandra 节点,存储了数十亿条消息。

2022 年初,它有 177 个节点,拥有数万亿条消息。 令我们懊恼的是,这是一个高劳累的系统——我们的待命团队经常因数据库问题而被传呼,延迟是不可预测的,而且我们不得不减少维护操作,因为运行成本太高。

是什么导致了这些问题? 首先,我们来看一条消息。

上面的 CQL 语句是消息模型的最小版本。 我们使用的每个 ID 都是雪花,使其可以按时间顺序排序。 我们根据消息发送的频道以及存储桶(静态时间窗口)对消息进行分区。 这种分区意味着,在 Cassandra 中,给定频道和存储桶的所有消息将存储在一起并跨三个节点进行复制。(根据您设置的复制因子/ replica factor)

这种分区存在一个潜在的性能缺陷:只有一小群朋友的服务器发送的消息往往比拥有数十万人的服务器少几个数量级。

在 Cassandra 中,读取比写入更昂贵。 写入会附加到提交日志并写入称为内存表的内存结构,最终刷新到磁盘。 然而,读取需要查询 memtable 和可能的多个 SSTable(磁盘文件),这是一个更昂贵的操作。 用户与服务器交互时的大量并发读取可以使分区成为热点,我们想象中将其称为“热分区”。 当数据集的大小与这些访问模式相结合时,将导致我们的集群陷入困境。

当我们遇到热分区时,它经常会影响整个数据库集群的延迟。 一个频道于存储桶对 接收了大量流量,并且随着节点越来越努力地服务流量并且越来越落后,节点中的延迟将会增加。

由于该节点无法跟上请求,对该节点的其他查询也会受到影响。 由于我们以仲裁一致性 (quorum consistency) 级别执行读取和写入,因此对服务热分区的节点的所有查询都会遭受延迟增加,从而导致更广泛的最终用户影响。

集群维护任务也经常造成麻烦。 我们很容易在压缩方面落后,Cassandra 会压缩磁盘上的 SSTable 以提高读取性能。 不仅我们的读取成本更高,而且当节点试图压缩时,我们还会看到级联延迟。

我们经常执行一种称为“流言舞”的操作,其中我们将一个节点取出,使其在不占用流量的情况下进行压缩,然后将其放回以从 Cassandra 的提示切换中获取提示,然后重复直到积压的压缩任务完成。 我们还花费了大量时间调整 JVM 的垃圾收集器和堆设置,因为 GC 暂停会导致显着的延迟峰值。

改变我们的架构

我们的消息集群并不是唯一的 Cassandra 数据库。 我们还有其他几个集群,每个集群都表现出类似(尽管可能不那么严重)的故障。

在本文的上一次迭代中,我们提到对 ScyllaDB 很感兴趣,这是一个用 C++ 编写的与 Cassandra 兼容的数据库。 它承诺提供更好的性能、更快的修复、通过按核分片架构实现更强的工作负载隔离,以及无垃圾收集的生活,听起来相当吸引人。

尽管 ScyllaDB 绝对不是没有问题,但它没有垃圾收集器,因为它是用 C++ 而不是 Java 编写的。 从历史上看,我们的团队在 Cassandra 上的垃圾收集器方面遇到过许多问题,从影响延迟的 GC 暂停,一直到超长的连续 GC 暂停,情况非常糟糕,以至于操作员必须手动重新启动并将有问题的节点恢复到原来的状态。 这些问题是待命工作的一大梦魇,也是我们消息集群中许多稳定性问题的根源。

在探索 ScyllaDB 并观察测试中的性能提升后,我们决定迁移所有数据库。 虽然这一决定本身就可以写一篇博客文章,但简短的版本是,到 2020 年,我们已经将除一个数据库之外的所有数据库迁移到了 ScyllaDB。

最后一个? 我们的朋友,cassandra-message。

为什么我们还没有迁移它? 首先,它是一个大集群。 由于有数万亿条消息和近 200 个节点,任何迁移都将是一项复杂的工作。 此外,我们希望确保我们的新数据库在我们努力调整其性能的同时能够达到最佳状态。 我们还希望在生产中获得更多使用 ScyllaDB 的经验,狠狠地使用它并了解它的陷阱。

我们还致力于提高 ScyllaDB 用例的性能。 在我们的测试中,我们发现反向查询的性能不足以满足我们的需求。 当我们尝试以与表排序相反的顺序扫描数据库时,例如当我们以升序扫描消息时,我们会执行反向查询。 ScyllaDB 团队优先考虑改进并实现了高性能反向查询,消除了我们迁移计划中的最后一个数据库障碍。

我们怀疑在我们的系统上安装一个新数据库并不会让一切都神奇地变得更好。 热分区仍然是 ScyllaDB 中的一个问题,因此我们还希望投入精力改进数据库上游的系统,以帮助保护和促进更好的数据库性能。

数据服务服务数据

对于 Cassandra,我们在热分区方面遇到了困难。 给定分区的高流量会导致无限并发,从而导致级联延迟,其中后续查询的延迟将继续增长。 如果我们能够控制热分区的并发流量,我们就可以保护数据库不被淹没。

为了完成这项任务,我们编写了所谓的数据服务(data service) —— 位于 API和数据库集群之间的中间服务。 在编写数据服务时,我们选择了一种在 Discord 中越来越多地使用的语言:Rust! 我们之前在几个项目中使用过它,它也没有辜负我们的期许。 它为我们提供了与C/C++ 一样的速度,而无需牺牲安全性。

Rust 将无所畏惧的并发性视为其主要优点之一——该语言应该让编写安全、并发的代码变得容易。 它的库也非常适合我们想要实现的目标。 Tokio 生态系统是构建异步 I/O 系统的巨大基础,并且该语言为 Cassandra 和 ScyllaDB 提供驱动程序支持。

此外,我们发现在编译器的帮助、错误消息的清晰度、语言结构及其对安全性的强调下进行编码是一种乐趣。 我们非常喜欢它一旦编译后就可以正常工作的方式。 而且,最重要的是,它让我们可以说我们用 Rust 重写了它!(meme信誉非常重要, Rust yyds)。

我们的数据服务位于 API 和 ScyllaDB 集群之间。 它们每个数据库查询大约包含一个 gRPC 端点,并且故意不包含任何业务逻辑。 我们的数据服务提供的一大功能是请求合并。 如果多个用户同时请求同一行,我们只会查询数据库一次。 第一个发出请求的用户会触发worker启动工作任务。 后续请求将检查该任务是否存在并订阅它。 该工作任务将查询数据库并将该行返回给所有订阅者。

这就是 Rust 的强大之处:它让编写安全的并发代码变得容易。

让我们想象一下大型服务器上有一个重大公告,通知@everyone:用户将打开应用程序并阅读消息,向数据库发送大量流量。 以前,这可能会导致热分区,并且待命的工程师可能需要进行分页以帮助系统恢复。 借助我们的数据服务,我们能够显着减少数据库的流量峰值。

这里神奇的第二部分是我们数据服务的上游。 我们对数据服务实施了基于一致性哈希的路由,以实现更有效的合并。 对于对我们数据服务的每个请求,我们提供一个路由密钥。 对于消息,这是一个频道 ID,因此同一频道的所有请求都会发送到同一服务实例。 此路由进一步有助于减少数据库的负载。

这些改进有很大帮助,但并不能解决我们所有的问题。 我们仍然看到 Cassandra 集群上出现热分区和延迟增加,只是频率不那么高。 它为我们赢得了一些时间,以便我们可以准备新的最佳 ScyllaDB 集群并执行迁移。

一次非常大的迁徙

我们对迁移的要求非常简单:我们需要在不停机的情况下迁移数万亿条消息,并且需要快速完成,因为虽然 Cassandra 的情况有所改善,但我们经常需要救火。

第一步很简单:我们使用我们的超级磁盘存储拓扑配置一个新的 ScyllaDB 集群。 通过使用本地 SSD 提高速度并利用 RAID 将数据镜像到持久化磁盘,我们本地SSD的速度和以及永久磁盘的持久性。 集群启动后,我们可以开始将数据迁移到其中。

我们的迁移计划的初稿旨在迅速实现价值。我们将从一个切换时间开始,使用我们崭新的 ScyllaDB 集群处理较新的数据,然后在其后迁移历史数据。这增加了更多的复杂性,但是每一个大型项目需要的就是额外的复杂性,对吧?

我们开始将新数据双重写入 Cassandra 和 ScyllaDB,同时开始配置 ScyllaDB 的 Spark 迁移器。 它需要大量的调整,一旦完成设置,我们预计完成时间:三个月。

这个时间范围并没有让我们感到温暖更多地是混乱/迷惑,我们希望更快地获得价值。 我们作为一个团队坐下来,集思广益,讨论加快速度的方法,直到我们记得我们已经编写了一个可以扩展的快速且高性能的数据库lib。 我们选择从事一些 meme 驱动的工程并用 Rust 重写数据迁移器。

一个下午,我们扩展了我们的数据服务lib来进行大规模的数据迁移。 它从数据库读取令牌范围,通过 SQLite 在本地对它们设置检查点 (Checkpoint) ,然后将它们传输到 ScyllaDB。 我们连接了新的,改进的迁移器并得到了新的预估时间:九天! 如果我们能够如此快速地迁移数据,那么我们就可以忘记复杂的基于时间的方法,而是立即替换所有内容。

我们将其打开并保持运行,以高达每秒 320 万条的速度迁移消息。 几天后,我们聚在一起看着它达到 100%,然后我们意识到它已经完成了 %(不,是真的)。 我们的迁移器在读取最后几个令牌范围的数据时超时,因为它们包含从未在 Cassandra 中压缩的巨大墓碑范围。 我们压缩该令牌范围,几秒钟后,迁移完成了!

我们通过向两个数据库发送一小部分读取数据并比较结果来执行自动数据验证,一切看起来都很棒。 该集群在满生产流量的情况下运行良好,而 Cassandra 则遇到了越来越频繁的延迟问题。 我们在现场的团队聚集在一起,按下开关将 ScyllaDB 转换为主数据库,并吃了庆祝蛋糕!

几个月后……

我们于 2022 年 5 月切换了消息数据库,但此后它的表现如何?

这是一个安静、运行良好的数据库(因为我这周没有值班)。 我们不会进行横跨整个周末的救火,也不会在集群中调整节点以试图保持正常运行时间。 这是一个更高效的数据库——我们将从运行 177 个 Cassandra 节点减少到仅运行 72 个 ScyllaDB 节点。 每个 ScyllaDB 节点拥有 9 TB 磁盘空间,高于每个 Cassandra 节点平均 4 TB 的磁盘空间。

我们的尾部延迟也显着改善。 例如,在 Cassandra 上获取历史消息的 p99 为 40-125 毫秒,而 ScyllaDB 的延迟为 15 毫秒,消息插入性能从 Cassandra 上的 5-70 毫秒 p99 到 ScyllaDB 上稳定的 5 毫秒 p99。 由于上述性能改进,我们现在对我们的消息数据库充满信心,已经解锁了新的产品用例。

2022年底,全世界的人们都收看世界杯。 我们很快发现的一件事是进球数显示在我们的监控图表中。 这非常酷,因为不仅可以在系统中看到真实世界的事件,而且这给了我们的团队一个在会议期间观看足球比赛的借口。 我们不是“在会议期间看足球”,而是“主动监控系统的性能”。

我们实际上可以通过消息发送图来讲述世界杯决赛的故事。 这场比赛非常精彩。 莱昂内尔·梅西(Lionel Messi) 试图完成他职业生涯中的最后一项成就,巩固自己作为有史以来最伟大球员的地位,并带领阿根廷队夺得冠军,但才华横溢的基利安·姆巴佩(Kylian Mbappe)和法国队挡在了他的路上。

该图中的九个尖峰中的每一个都代表比赛中的一个事件。

梅西主罚命中,阿根廷1-0领先。

阿根廷再次进球,2-0领先。

现在是中场休息。 当用户谈论这场比赛时,会出现持续十五分钟的停滞状态。

这里最大的亮点是因为姆巴佩为法国队进球,并在 90 秒后再次进球将比分追平!

常规赛结束了,这场大型比赛将进入加时赛。

加时赛上半场没有发生太多事情,但我们到了中场休息,用户们正在聊天。

梅西再次进球,阿根廷取得领先!

姆巴佩反击扳平比分!

加时赛结束,我们进入点球大战!

点球大战中,兴奋和压力不断增加,直到法国队错失而阿根廷队错失! 阿根廷获胜!

全世界的人们都在观看这场令人难以置信的比赛,但与此同时,Discord 和消息数据库毫无波动。 我们在消息发送和处理方面已经取得了很大进展。 借助我们基于 Rust 的数据服务和 ScyllaDB,我们能够承担这样巨大的流量并为用户提供交流平台。

我们构建了一个可以处理数万亿条消息的系统,如果这项工作让您兴奋,请查看我们的career page。 我们正在招聘!

标签:

相关文章

[ 相关新闻 ]