首页
Preview

为什么Discord要从Go转向Rust

我们的博客已经迁移到我们的主网站上了!请访问discord.com/blog以获取有关未来文章的信息。

Rust 正在成为多个领域的一流语言。在 Discord,我们已经在客户端和服务器端使用 Rust 取得了成功。例如,我们在客户端为 Go Live 的视频编码管道使用它,在服务器端则用于 Elixir NIFs。最近,我们通过将一个服务的实现从 Go 切换到 Rust 大大提高了其性能。本文将解释为什么重新实现该服务对我们是有意义的,如何实现以及产生的性能改进。

Read States 服务

Discord 是一家以产品为中心的公司,因此我们将从一些产品背景开始。我们从 Go 切换到 Rust 的服务是“Read States”服务。它的唯一目的是跟踪你已经阅读过的哪些频道和消息。每次连接到 Discord,每次发送消息和每次阅读消息都会访问 Read States。简而言之,Read States 处于热点路径上。我们希望确保 Discord 始终感觉超级快速,因此我们需要确保 Read States 快速。

使用 Go 实现时,Read States 服务未满足其产品要求。大部分时间它很快,但每隔几分钟我们会看到大量的延迟峰值,这对用户体验很不好。经过调查,我们确定这些峰值是由于核心 Go 功能引起的:其内存模型和垃圾回收器(GC)。

为什么 Go 没有达到我们的性能目标

为了解释为什么 Go 没有达到我们的性能目标,我们首先需要讨论服务的数据结构、规模、访问模式和架构。

我们用于存储阅读状态信息的数据结构方便地称为“Read State”。Discord 有数十亿个 Read States。每个用户每个频道有一个 Read State。每个 Read State 都有多个需要原子更新和经常重置为 0 的计数器。例如,其中一个计数器是你在一个频道中有多少个 @mentions。

为了快速进行原子计数器更新,每个 Read States 服务器都有一个最近最少使用(LRU)缓存。每个缓存中有数百万个用户。每个缓存中有数千万个 Read States。每秒有数十万个缓存更新。

为了持久化,我们使用 Cassandra 数据库集群来支持缓存。在缓存键被驱逐时,我们将你的 Read States 提交到数据库。每当更新 Read State 时,我们还会安排在 30 秒后进行一次数据库提交。每秒有数万个数据库写入。

在下面的图片中,你可以看到 Go 服务的一个高峰样本时间段的响应时间和系统 CPU。¹ 可以看到,大约每 2 分钟就会出现延迟和 CPU 峰值。

那么为什么会有 2 分钟的峰值?

在 Go 中,在缓存键被驱逐时,内存不会立即被释放。相反,垃圾回收器定期运行以查找任何没有引用的内存,然后释放它。换句话说,内存在不再使用后不会立即释放,而是在垃圾回收器确定它是否真的不再使用之前会暂时存在。在垃圾回收期间,Go 必须做很多工作来确定哪些内存是空闲的,这可能会使程序变慢。

这些延迟峰值绝对有垃圾回收性能影响的问题,但我们已经非常高效地编写了 Go 代码,并且几乎没有分配垃圾。我们没有创建很多垃圾。

经过查看 Go 源代码,我们了解到 Go 每2 分钟强制运行一次垃圾回收。换句话说,如果垃圾回收 2 分钟没有运行,无论堆增长如何,go 都会强制运行垃圾回收。

我们认为我们可以调整垃圾回收器以更频繁地发生以防止大的峰值,因此我们在服务端实现了一个端点来更改垃圾回收器的GC 百分比。不幸的是,无论我们如何配置 GC 百分比,都没有改变。为什么?原来是因为我们没有足够快地分配内存,以使其强制垃圾回收更频繁。

我们继续挖掘并了解到,这些峰值之所以很大,并不是因为有大量的待释放内存,而是因为垃圾回收器需要扫描整个 LRU 缓存以确定内存是否真正从引用中释放。因此,我们认为较小的 LRU 缓存会更快,因为垃圾回收器需要扫描的内容更少。因此,我们为服务添加了另一个设置,以更改每个服务器的分区 LRU 缓存的大小和架构。

我们是正确的。缩小 LRU 缓存后,垃圾回收导致的峰值更小。

不幸的是,缩小 LRU 缓存的折衷方案导致了更高的 99th 延迟时间。这是因为如果缓存较小,则用户的 Read State 更不可能在缓存中。如果它不在缓存中,则我们必须进行数据库加载。

经过大量的负载测试不同的缓存容量后,我们找到了一个看起来不错的设置。虽然不完全满意,但满意度足够高,而且有更重要的事情要做,因此我们将服务像这样运行了一段时间。

在那段时间里,我们在 Discord 的其他部分看到了越来越多的 Rust 成功案例,我们共同决定创建所需的框架和库,以完全使用 Rust 构建新服务。由于这个服务规模较小且自包含,因此它是一个很好的 Rust 移植候选对象,但我们还希望 Rust 可以解决这些延迟峰值问题。因此,我们承担了将 Read States 移植到 Rust 的任务,希望证明 Rust 可以作为服务语言,并改善用户体验。²

Rust 中的内存管理

Rust 是极快和内存高效的: 没有运行时或垃圾回收器,它可以为性能关键的服务提供动力,可以运行在嵌入式设备上,并且可以轻松地与其他语言集成。³

Rust没有垃圾回收机制,因此我们认为它不会像Go那样出现延迟峰值。

Rust采用了一种相对独特的内存管理方法,它融合了“内存所有权”的思想。基本上,Rust会跟踪哪些程序可以读写内存。它知道程序何时在使用内存,并在内存不再需要时立即释放内存。它在编译时强制执行内存规则,使得几乎不可能出现运行时内存错误⁴。你不需要手动跟踪内存,编译器会替你完成。

因此,在Rust版本的Read States服务中,当用户的Read State从LRU缓存中清除时,它会立即从内存中释放。Read State内存不会等待垃圾回收器收集它。Rust知道它不再使用,因此立即释放它。不需要运行时进程来确定是否应该释放它。

异步Rust

但是,Rust生态系统存在一个问题。在重新实现该服务的时候,Rust stable版本没有很好地支持异步Rust。对于网络服务来说,异步编程是必需的。虽然有一些社区库可以实现异步Rust,但它们需要很多的仪式,并且错误消息非常晦涩。

幸运的是,Rust团队正在努力使异步编程变得容易,并且它已经可以在Rust的不稳定夜间频道中使用了。

Discord从来没有害怕接受看起来有前途的新技术。例如,我们是Elixir、React、React Native和Scylla的早期采用者。如果一项技术有前途并给我们带来优势,我们不介意应对先进技术的固有困难和不稳定性。这是我们用不到50个工程师就迅速达到2.5亿用户的方法之一。

接受Rust夜间版中的新异步特性是我们愿意接受新的有前途技术的又一例子。作为一个工程团队,我们决定使用夜间版的Rust,并承诺一直使用夜间版,直到异步在stable版本中完全支持。我们一起处理了任何出现的问题,现在Rust stable支持异步Rust⁵。这个赌注成功了。

实现、负载测试和发布

重写实际上相当简单。它从粗略的翻译开始,然后在有意义的地方进行了精简。例如,Rust拥有出色的类型系统和广泛的泛型支持,因此我们可以丢弃由于缺乏泛型而存在的Go代码。此外,Rust的内存模型可以跨线程推理内存安全性,因此我们能够丢弃一些在Go中需要的手动跨协程内存保护。

当我们开始负载测试时,我们立即对结果感到满意。Rust版本的延迟与Go版本相同,并且没有延迟峰值!

值得注意的是,在写Rust版本时,我们只是基本地考虑了优化问题。 即使只有基本的优化,Rust也能够超越经过高度调优的Go版本。 这充分证明了使用Rust编写高效程序的易用性,相对于我们在Go中进行的深度探索。

但是,我们并不满足于仅仅匹配Go的性能。经过一些分析和性能优化,我们能够在每个性能指标上击败Go。延迟、CPU和内存在Rust版本中都表现得更好。

Rust性能优化包括:

  • 在LRU缓存中使用BTreeMap代替HashMap,以优化内存使用。
  • 将初始度量库替换为使用现代Rust并发性的库。
  • 减少我们进行的内存复制次数。

满意后,我们决定推出该服务。

由于我们进行了负载测试,因此发布过程非常顺利。我们将其放在单个金丝雀节点上,找到了一些缺失的边缘情况,并加以修复。之后不久,我们将其推向了整个机群。

下面是结果。

Go为紫色,Rust为蓝色。

提高缓存容量

服务成功运行几天后,我们决定提高LRU缓存容量。在上述提到的Go版本中,提高LRU缓存的容量会导致更长的垃圾回收时间。我们不再需要处理垃圾回收,因此我们认为可以提高缓存的容量并获得更好的性能。我们增加了盒子的内存容量,优化了数据结构以使用更少的内存(为了好玩),并将缓存容量增加到了800万个Read States。

下面的结果说明了一切。请注意,平均时间现在以微秒为单位,max @mention以毫秒为单位。

不断发展的生态系统

最后,Rust的另一个伟大之处是它的生态系统在快速发展。最近,tokio(我们使用的异步运行时)发布了0.2版。我们升级后,它为我们提供了免费的CPU优势。下面你可以看到从第16个开始,CPU始终更低。

结语

此时,Discord在其软件堆栈的许多地方都使用了Rust。我们将其用于游戏SDK、Go Live的视频捕获和编码、Elixir NIF和几个后端服务等等。

当开始一个新项目或软件组件时,我们会考虑使用Rust。当然,我们只会在有意义的地方使用它。

除了性能之外,Rust对于工程团队还有许多优势。例如,它的类型安全性和借用检查器使得在产品要求发生变化或发现了关于语言的新知识时很容易重构代码。此外,生态系统和工具非常优秀,并且具有相当大的动量。

如果你看到这里,你可能是新近对Rust感到兴奋,或者已经很久以前就对它感到兴奋了。如果你想在专业领域使用Rust解决有趣的问题,你应该考虑在Discord工作。

还有一个有趣的事实:Rust团队使用Discord进行协调。甚至有一个非常有用的Rust社区服务器,你可以在里面找到我们聊天。点击这里查看。

[1] Go语言版本为1.9.2。编辑:图表来自1.9.2。我们尝试了1.8、1.9和1.10版本,但没有任何改进。从Go到Rust的最初端口完成于2019年5月。

[2] 明确一点,我们并不认为你应该把所有东西都重写成Rust。

[3] 引用自https://www.rust-lang.org/

[4] 当然,除非你使用unsafe

[5] https://areweasyncyet.rs/

译自:https://medium.com/discord-engineering/why-discord-is-switching-from-go-to-rust-a190bbca2b1f

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
菜鸟一只
你就是个黄焖鸡,又黄又闷又垃圾。

评论(0)

添加评论