【系统架构设计】Discord 架构:从 Go 到 Rust 的性能进化

💡 原文中文,约28500字,阅读约需68分钟。
📝

内容提要

Discord 后端采用多语言架构,Elixir 处理实时连接,Rust 优化热路径,存储从 MongoDB 迁移至 ScyllaDB。语音系统迁移至边缘节点,但遭遇进程邮箱瓶颈和故障。通过事故复盘,团队强化了架构韧性,并利用 Elasticsearch 实现万亿级消息索引。

🔎

延伸解读

语言选型:按热路径而非流行度

Discord 并非全栈 Rust,而是按服务特性选择语言:Elixir 处理百万级有状态连接,Rust 优化热路径,Go 在 Read States 上因 GC 与巨型 LRU 冲突而败北。这提醒我们,语言选型应基于具体负载特征,而非盲目追随技术潮流。

存储演进:从 MongoDB 到 ScyllaDB

消息存储从 MongoDB 迁移至 Cassandra 再到 ScyllaDB,解决了索引无法常驻内存、JVM GC 尖刺等问题。迁移过程中,Rust 数据服务通过请求合并缓解了热分区压力,最终将节点数从 177 台降至 72 台,p99 延迟大幅改善。

边缘迁移的代价:新瓶颈浮现

将语音迁移至 Cloudflare 边缘虽降低了延迟,却引入了 recv starvation、NIC 队列争用等问题。冰岛实验表明,地理最近并非最优,混合区域需更智能的 call placement。边缘计算并非万能,需权衡覆盖与性能。

事故复盘:共享基础设施的隐患

2026 年语音故障源于 K8s 缩容与 Elixir 邮箱瓶颈,Holster.Pool 与 etcd 耦合导致级联失败。教训是共享连接池和 supervisor 不应服务多条关键路径,需通过 PartitionSupervisor 和 drain webhook 增强韧性。

Q&A

Discord 为什么从 Go 迁移到 Rust?

Discord 的 Read States 服务原本用 Go 编写,但遇到 GC 与巨型 LRU 缓存的结构性冲突:Go 的垃圾回收器每约 2 分钟强制扫描整个 LRU,导致延迟尖刺。Rust 无 GC,驱逐时立即释放内存,消除了尖刺,并允许将 LRU 容量提升至 800 万条。

Discord 的消息存储经历了怎样的演进?

Discord 最初使用 MongoDB,后因索引无法常驻内存而迁移到 Cassandra,最终迁移到 ScyllaDB。迁移后,节点数从 177 台 Cassandra 降至 72 台 ScyllaDB,p99 延迟显著下降(如拉取历史消息从 40-125ms 降至 15ms)。

Discord 的语音系统是如何设计的?

Discord 语音系统采用 SFU(选择性转发单元)架构,客户端只上传一路媒体流,由 SFU 转发给其他参与者。后端由 Elixir 处理信令(Gateway、Guilds、Voice Syncers),C++/Rust 实现媒体面。语音服务器通过 etcd 服务发现,Guilds 选择负载最低的服务器。

2026 年 Discord 语音故障的根本原因是什么?

2026 年 3 月 25 日的语音故障源于 Kubernetes 缩容时,Sessions 服务的 pod 终止未完成安全等待,导致 17% 的 session 非优雅退出,引发 :DOWN 消息风暴,进而导致 Gateway 重连风暴和 Voice Syncers 的邮箱积压,最终使 SFU RPC 失败。

Discord 如何实现万亿级消息的搜索?

Discord 使用 Elasticsearch 的 cell 架构,将索引拆分为多个小型 ES 集群,通过 PubSub 队列和 Rust MessageRouter 进行消息路由和批量索引。针对大型服务器(BFG)使用专用 cell,并支持在线 dual-index 切换。索引吞吐量是旧系统的 2 倍,查询中位数从 500ms 降至 <100ms。

Discord 为什么选择 Elixir 处理实时连接?

Discord 选择 Elixir 是因为其 Actor 模型和进程隔离适合管理百万级 WebSocket 连接,支持热代码升级和进程级故障隔离。Elixir 的轻量进程和 Supervisor 树使得实时系统更易运维。

Discord 在语音迁移到边缘节点后遇到了哪些问题?

迁移到 Cloudflare 边缘后,Discord 遇到了 recv starvation(接收饥饿)问题,即事件循环中 recv 始终就绪导致 flush 被饿死,造成延迟尖峰。此外还有 NIC 队列争用、noisy neighbor 等问题。通过调整 worker 数量、CPU affinity 和发送预算修复。

Discord 如何应对数据库的 hot partition 问题?

Discord 通过 Rust Data Services 实现请求合并(request coalescing),将同一 channel 的并发读合并为单次查询,减轻数据库压力。此外,使用一致性哈希按 channel_id 路由到同一实例,提高合并率。但 hot partition 并未完全消除,只是被缓解。

🏷️

标签

➡️

继续阅读