我们如何将Discord语音迁移到边缘

Lobsters Hottest 新闻

摘要

Discord将其超过80%的语音和视频流量迁移至Cloudflare覆盖300多个城市的边缘网络,显著降低了全球延迟和丢包率,例如法兰克福的ping值降低了34%。

<p><a href="https://lobste.rs/s/w6ptmr/how_we_moved_discord_voice_edge">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/11 13:40

# 我们如何将 Discord 语音迁移到边缘网络 来源:https://discord.com/blog/how-we-discord-voice-to-the-edge 四个朋友在 Discord 通话中的场景。 在 Discord,用户与最近语音服务器之间的距离至关重要。网络距离每增加一毫秒,每个数据包的延迟都会增加,超过某个临界点后,通话就不再感觉像朋友与你身处同一房间。 在 Discord 的大部分历史中,我们能为你提供的最接近的语音服务器位于全球约 30 个城市,这些城市是主要云提供商的数据中心所在地。如果你住在湾区或法兰克福,这没问题;但如果你住在雷克雅未克、奥克兰或其他超大规模云覆盖稀少的地区,体验就不那么好了。 去年,我们开始将 Discord 的语音和视频流量迁移到 Cloudflare 的边缘网络,该网络运行于超过 300 个城市。如今,超过 80% 的语音和视频流量运行在此,70% 的地区显示出同比质量提升。法兰克福领先,平均 ping 值下降 34%,丢包率比之前供应商降低 42%。 这篇文章讲述了我们如何走到这一步:为什么这样做,我们必须构建什么才能使其工作,以及我们今年早些时候如何调查欧洲的质量问题。 ## 不同的网络形态 大多数云基础设施围绕少量大型数据中心构建。Cloudflare 则另辟蹊径:在超过 300 个城市设立较小的存在点 (PoPs (https://www.cloudns.net/blog/what-is-a-point-of-presence-pop-and-why-does-it-matter/)),他们的主张是在所有这些 PoP 中运行 Discord 的语音和视频软件。 CDN (https://www.cloudflare.com/learning/cdn/what-is-a-cdn/) 旨在将静态内容缓存到用户附近,但 Discord 的语音和视频服务需实时在通话参与者之间路由低延迟的 UDP (https://www.cloudflare.com/learning/ddos/glossary/user-datagram-protocol-udp/) 数据包。Cloudflare 从 CDN 架构发展而来,并已构建了足够广泛的平台以支持多种流量,我们认为它也能适用于我们的工作负载。 这一改变为我们打开了地理上的可能性。传统上,冰岛用户的通话会经过数百公里外的鹿特丹进行路由。而 Cloudflare 在雷克雅未克设有 PoP。每个新位置仍需要我们进行部署,但地理距离首次不再是限制因素。同样适用于新西兰、夏威夷、拉各斯以及许多过去需要路由到最近超大规模云区域的地方。作为对比,其他超大规模云提供商全球仅运营约 30 到 40 个区域,覆盖不到某些国家,而 Cloudflare 的网络在各大城市拥有 300 多个 PoP。 ## 两个区域,两个意外 在开始任何工程工作之前,我们必须测试基本的地理前提是否成立。我们原本预期只需将 Cloudflare PoP 部署以替换之前供应商的区域,让流量自然迁移过去。前两次部署给我们上了一课:前提并不成立。 ### 阶段 0:冰岛 2025 年 2 月底,冰岛是我们的首次测试。我们之前的语音和视频供应商在那里没有存在点,因此冰岛用户一直通过数百公里外的鹿特丹路由。Cloudflare 在雷克雅未克设有 PoP,我们便在那里部署了一些语音服务器,观察结果。 对于仅限冰岛的通话,数据如预期般改善。Ping 下降 9%,丢包下降 11%。本地用户连接到了本地服务器。 但对于跨区域通话,数据却走向了反面。参与冰岛托管通话的非冰岛用户,ping 值飙升了 2.7 倍,丢包率上升了 9%。 这是由于 Discord 将通话分配到服务器的方式所致。我们选择一个 SFU (https://bloggeek.me/webrtcglossary/sfu/) 实例来托管整个通话,每个参与者在通话期间都连接到该实例(阅读关于 Discord 如何处理数百万并发语音通话的详细信息,请参阅 previous blog post (https://discord.com/blog/how-discord-handles-two-and-half-million-concurrent-voice-users-using-webrtc))。这意味着,如果一位冰岛用户与三位德国用户发起通话,通话可能落在雷克雅未克的服务器上,德国用户的数据包需发送到冰岛再返回。这个“本地”PoP 反而让所有非本地用户的情况更糟。 我们必须修订策略:事实证明,新的 PoP 仅当在其中托管的通话保持本地性时才有帮助,这意味着对于跨区域通话,主机放置位置比覆盖范围更重要。因此,我们不再填补覆盖空白,而是将重点转向逐一替换现有区域。像雷克雅未克这样的覆盖点被列入长期路线图,并结合更智能的通话放置逻辑。 ### 阶段 1:鹿特丹 阶段 1 始于 2025 年 4 月底,将鹿特丹的流量迁移到 Cloudflare 的阿姆斯特丹 PoP。该地区的大多数 ISP (https://www.thousandeyes.com/learning/glossary/isp-internet-service-provider) 看起来正常:ping 正常,质量指标稳定。 几小时内,通过法国最大 ISP 之一 Orange 连接的用户在高峰时段出现超过一秒的通话延迟。Orange 通话的语音质量在冻结率上退步了 30%。不幸的是,Orange 亮起了红灯。 我们开始深入调查。该地区的其他 ISP 健康,其他 Cloudflare PoP 健康,那么问题出在哪里?原来问题出在 Orange 到阿姆斯特丹的路径上。没有 ISP 会与其他每个网络直接互联。相反,流量通过中间传输提供商桥接。在这种情况下,Orange 到 Cloudflare 边缘的连接经过 Telia 的传输骨干网,而 Telia 到 Orange 的交接点已在高峰时段饱和。我们每向 Cloudflare 迁移一兆额外流量,情况就更糟。 两张折线图显示 Cloudflare 在鹿特丹高峰时段反复出现 ping 峰值,而之前供应商保持平稳。 大约十天后,我们回滚了。鹿特丹的 Cloudflare 实例被排空,我们依赖之前供应商的容量来吸收 Orange 方向的流量。Cloudflare 开始与 Orange 进行直接对等协商,并建议我们在其巴黎和伦敦的 PoP 部署 SFU,以便为 Orange 流量提供通往 Cloudflare 边缘的更短路径。这些 PoP 在接下来的几个月上线,所有相关方都从这些 PoP 变更中受益! 这也影响了我们的部署流程。在鹿特丹之前,我们根据容量准备情况来安排部署节奏。鹿特丹之后,我们根据对等分析来安排:在任何区域获得有意义的流量之前,团队先检查 Cloudflare 与该区域主要 ISP 的对等连接是否有余量。如果没有,该区域就等待。 ### 发生了什么变化 Cloudflare 的 PoP 确实比超大规模云区域覆盖更多地方,且来自更近 PoP 的流量延迟确实更低。因此,我们改变了迁移顺序的思考方式。冰岛表明,最近的 PoP 并不总是任何给定通话的最佳主机;鹿特丹则显示,即使理论上*应该*如此,网络路径仍然可能成为问题。 迁移速度放缓了。我们最初计划的节奏——几个月内完成大型区域,年底前全面迁移——让位于逐个区域的部署,并事先进行对等检查。虽然我们本应从一开始就这样做,但尝试其他部署方法仍然有助于我们确定正确的前进道路。 我们还意识到,Cloudflare 的操作机制与我们之前运行过的任何东西都不同。最初几个月的精力主要花在使其基本部署和主机生命周期管理在那里正常运行。 ## 发现:主机主动注册 在我们现有基础设施上的发现很简单:配置一台主机,然后在我们的发现服务中注册它。Cloudflare 则相反:Cloudflare 的调度程序会自行按时间线启动和回收机器以进行维护或转移流量负载,因此主机会在无预先通知的情况下出现,且主机必须自我介绍。 我们构建了一个发现服务来处理这个问题。语音主机在 Cloudflare 上启动后,主动调用注册,运行期间定期重新注册,并在关闭时取消注册。这些注册信息存储在 Valkey (https://docs.cloud.google.com/memorystore/docs/valkey/product-overview) 中(一种 GCP 内存存储服务),TTL 为十分钟,因此不主动检查的主机会自动退出。 我们较旧的服务发现几年前是建立在 etcd (https://etcd.io/) 上的,用于规模小得多的集群,而在 Cloudflare 的 25,000 个语音主机面前,它开始吃力了。发现延迟在上升,我们离一次大规模部署就会搞垮它只差一步。通过 Valkey 存储 Cloudflare 主机,使新增长不加重 etcd 负担;在迁移过程中,我们也移除了旧的 SFU 记录,在切换期间进行双重写入,最终淘汰了 etcd。 ## 主机作为 Spot 实例 Cloudflare 的部署平台针对短生命周期的 V8 Isolates (https://developers.cloudflare.com/workers/reference/how-workers-works/) 进行了优化,最初并非为语音流量设计。Discord 语音通话运行在一台主机上,该主机同时承载有状态的控制平面(谁在通话、谁静音、流如何传输)和无状态的媒体平面(用于音频和视频的 UDP 数据包)。UDP 传输在通话期间保持打开,因此在部署过程中回收该主机可能导致通话中断。 大多数云假设主机保持不变。主机启动时获得稳定名称,我们按自己的计划退役它。Cloudflare 不这样工作:它们的调度程序可以在运行时卸载正在运行的主机以回收资源(如果其他地方需要)。除了任何回收事件外,我们的容器每个月至少会因定期维护而重启一次。 ### 重建而非重启 在 Cloudflare 上,每次代码变更都是一次部署,会从新镜像重建容器。我们构建了一个部署器,请求 Cloudflare 重建任何使用错误(旧)镜像的主机,并轮询直到每个主机恢复。 跨 PoP 的协调重建可能会暂时将实例数量降至零,因此我们让容器的 supervisor 捕获关闭信号,并延迟实际退出五分钟。在此期间,替代实例启动,PoP 保持有主机,通话重新连接会落在同一区域的新主机上,而不是被弹走。 ### 两个 Worker,然后四个,然后八个 我们发送到 Cloudflare 的第一个组合语音/worker 容器每个主机运行四个 worker。这是在其他地方都适用的平均密度……但在这里不适用。 2025 年 5 月初,美国东部出现持续 1.5–2% 的丢包,而之前供应商的基线低于 0.5%,且在整个 Cloudflare 路径上均匀出现。我们最终追溯到 NIC (https://en.wikipedia.org/wiki/Network_interface_controller) 队列争用。在裸机上,每个 worker 有自己的 NIC 发送队列,但 Cloudflare 的容器运行时尚未向我们的容器暴露多队列 NIC,因此所有四个 worker 共享一个队列。内核在高峰时段开始在发送缓冲区层丢弃 UDP 数据包。 折线图显示 us-east 在 5 月 13 日出现丢包峰值,而所有其他区域保持平稳。 作为短期修复,我们将密度减半。以每个主机两个 worker 重新发布,丢包率回落到与之前供应商持平。虽然每台主机的可用性变差,但路径再次可行了。 Cloudflare 在接下来的几周内解决了底层缓冲区问题。到 2025 年 6 月初,他们修复了 UDP 缓冲区,我们在阿姆斯特丹以四个 worker 作为试点重新启用。到 9 月底,八个 worker 配置成为全球默认设置。 我们本期望从高密度开始并保持高密度。但实际情况是:我们以四个 worker 发布,缩小到两个进行数周调试,再回升到八个。 到 2026 年初,迁移机制基本完成。剩下两个问题,这两个问题让我们比迁移本身更深地探索了 Cloudflare 的基础设施。 ## 二十五秒(!)的挂起 第一个问题表现为并非*真正*故障的故障。某些 PoP 中的语音实例开始出现长达二十五秒以上的无响应,且日志中没有任何记录。Cloudflare 的洛杉矶 PoP 比其他 PoP 更频繁地出现此问题。 Cloudflare 从另一个客户那里也收到了类似信号,他们的内核团队独立发现了主机级别的 VM 级输入/输出停顿。问题出在新磁盘上的页面缓存累积。我们的写密集工作负载会填满缓存,然后内核会一次性刷新数 GB,导致块设备上的所有其他写入者停顿,直到刷新完成。使用较新硬件的 PoP 运行更多我们的工作负载,因此停顿更频繁。 一旦确认,Cloudflare 在自身一侧修复了问题。首先,通过调整脏页刷新频率;然后,对 VMM 的同步输入/输出路径进行了更复杂的更改。我们的贡献是报告症状并提供足够的遥测数据,使 Cloudflare 团队能够将其与主机指标相关联,从而将修复惠及 Cloudflare 的所有用户群。 ## 欧洲的 Ping 峰值 当我们于 2025 年底将布加勒斯特和斯德哥尔摩迁移到 Cloudflare 上基于 Rust 的新 SFU 时,这两个区域的指标开始下滑。Ping 在高峰时段攀升,语音质量在接下来的几周内恶化,到 2 月,客户端 ping 延迟飙升至半秒以上。甚至通过我们的支持渠道和 Reddit 收到了关于 ping 峰值和机器人声音的报告。 我们在调查期间将部分欧洲流量从 Cloudflare 转移出去。这花费了几周时间,并变成了一次比以往与 Cloudflare 更长的联合调试。早期的猜测是常见原因:路由异常、对等问题、针对我们端口的 DDoS。没有一项站得住脚。 ### 探测 我们的调查突破来自 Cloudflare 自身性能团队编写的一个被动探针。 我们的语音健康检查携带固定的有效载荷特征,在网络上可识别,无需检查连接状态。Cloudflare 构建了一个 eBPF (https://ebpf.io/) 程序,挂接到主机的网络栈中,并为经过的每个 Discord 健康检查打上时间戳。通过比较 NIC 上的到达和离开时间戳,他们得到了我们运行的每台主机上每个健康检查的金属层处理时间,且不触及我们的流量。 该探针进一步细分,发现物理 NIC 到虚拟 NIC 干净,虚拟 NIC 返回物理 NIC 也干净。时间消耗在虚拟机内部,意味着在我们的应用程序内部。 我们让 Cloudflare 团队直接访问受影响区域的一个容器。几分钟内,他们观察到我们进程内部的 futex 推迟高达 860 毫秒。随后几天的每线程 CPU 采样显示,在每个峰值窗口期间,一个线程在一个核心上占用 100%。同一窗口期间,套接字接收缓冲区也在增长。几天后,我们有了火焰图,端到端地展示了故障模式。 ### 我们应用程序中的写入饥饿 第一个根因在我们的代码中。我们的 SFU 运行八个 worker 线程,每个线程有自己的 UDP 套接字、单线程的 Tokio (https://tokio.rs/) 运行时和 select! 事件循环。这里重要的两件事是 recv future (https://doc.rust-lang.org/std/future/trait.Future.html)(从套接字拉取数据包)和刷新定时器(将传出数据包推送出去)。当接收缓冲区非空时,recv future 总是就绪,而总是就绪的 future 会被反复轮询。事件循环最终陷入

相似文章

引用 Luke Curley

Simon Willison's Blog

技术评论:Luke Curley探讨WebRTC的设计如何通过激进丢弃音频数据包来优先保障低延迟,这与LLM语音应用中提示词准确度比速度更重要的需求相矛盾。他讲述了在浏览器限制下在Discord实现重传所面临的挑战。

OpenAI 如何实现大规模低延迟语音 AI 部署

OpenAI Blog

OpenAI 详细介绍了其重新架构的 WebRTC 技术栈,旨在为超过 9 亿用户提供大规模低延迟语音 AI 服务。文章阐述了全新的 split-relay 和 transceiver 架构如何优化媒体路由与连接建立,以支持 ChatGPT 语音等实时交互场景。

Discord 事件

Hacker News Top

Discord 正在经历一次重大事件,API 错误增加,导致许多用户无法启动会话或发送消息。恢复操作正在进行中,系统正在逐步恢复。