摆脱分叉陷阱:Meta 如何为50多个场景升级 WebRTC

  • 在 Meta,WebRTC 支撑多个平台的实时音视频。但在单一代码仓库中维护这样的大型开源项目分叉,会遇到特殊挑战:内部版本逐渐落后于上游,无法及时获得社区升级。
  • 本文介绍 Meta 团队如何摆脱“分叉陷阱”:构建双栈架构,在50多个使用场景中安全地进行 A/B 测试,并建立持续跟进上游的工作流。
  • 这种方式改善了性能、二进制体积和安全。原文发表时,Meta 团队仍用它,在推出每个上游新版之前进行 A/B 测试。

Meta 的实时通信服务涵盖全球 Messenger 与 Instagram 视频聊天、低延迟云游戏,以及 Meta Quest 的沉浸式 VR 投屏。为满足数十亿用户的性能需求,Meta 团队花费多年,为开源 WebRTC 库开发了专门优化的高性能变体。

永久分叉大型开源项目容易陷入行业常见困境。最初只是为了内部优化或快速修复,但随着上游演进、内部功能累积,合并外部提交所需资源可能变得难以承受。

原文发表时,Meta 团队已正式完成了一项持续多年的大规模迁移,打破了这一循环:把50多个使用场景从偏离上游的 WebRTC 分叉,迁移到基于最新上游版本的模块化架构。Meta 团队以上游作为骨架,在关键组件中注入自己的专有实现。

本文介绍如何解决“分叉陷阱”,在单一代码仓库中,把两个 WebRTC 版本同时构建进一个库以进行 A/B 测试,并持续升级正在测试的版本。

挑战:单一代码仓库与静态链接器

升级 WebRTC 这类库存在风险,尤其是在服务数十亿用户、回归难以回滚的情况下。设备与环境多样性意味着不能直接进行一次性升级,否则可能破坏部分用户体验。

Meta 团队因此优先构建 A/B 测试能力,让旧 WebRTC 与带干净补丁的新上游版本在同一应用中运行,同时应用自己的功能,并在运行时切换用户,以验证新版本。

由于应用构建图与体积限制,Meta 团队还优先寻找静态链接两个 WebRTC 版本的方式。但这违反了 C++ 的单一定义规则(ODR),引发数千个符号冲突。因此需要让同一库的两个版本在同一地址空间共存。

Meta 使用单一代码仓库,Meta 团队不希望反复经历同一过程。于是需要一种在这种环境下维护开源项目自定义补丁、持续拉取上游新版并反复应用补丁的方案。

由此聚焦于两个挑战:

  • 实现 A/B 测试能力。由于应用限制,需要在同一个库中构建两份 WebRTC。
  • 单一代码仓库没有功能分支时,如何跟踪补丁并变基?其他基于 libwebrtc 的开源项目,通常在每次升级时,把保存的补丁文件按顺序应用到干净仓库上。考虑到可扩展性,Meta 团队探索了更精细的方案。

方案一:适配层与双栈架构

为了支持 A/B 测试,Meta 团队在同一应用中构建两份 WebRTC。但将两份实现静态放入同一个上层通话编排库,会产生特殊挑战。Meta 团队在应用层与 WebRTC 之间建立 shim 适配层:这是位于应用代码和底层 WebRTC 实现之间的代理库。应用不再直接调用 WebRTC,而是调用 shim API。适配层提供统一、与版本无关的 API。

WebRTC 双栈架构:多个调用库经适配层选择旧版或最新版 WebRTC
适配层根据运行时配置,将调用分派到旧版或最新版 WebRTC。

适配层保存一个 flavor 配置,在运行时将每次调用分派到旧版或新版 WebRTC。把适配放在尽可能低的层次,避免了复制高层通话编排库所带来的显著体积增长。Meta 报告:复制高层库将增加约38MB未压缩体积,而该方案仅增加约5MB,减少约87%。

接下来看看双栈架构引入的障碍以及解决方式。

解决符号冲突

把两份 WebRTC 静态链接到同一二进制,会产生数千个重复符号错误。

通过 webrtc_legacy 和 webrtc_latest 命名空间区分两个 WebRTC 版本的符号
将两个版本的 webrtc 命名空间分别改为 webrtc_legacy 和 webrtc_latest,避免重复符号。

为确保每个版本的符号唯一,Meta 团队采用自动命名空间改写。脚本系统地改写指定 WebRTC 版本的所有 C++ 命名空间:最新上游副本中的 webrtc:: 变为 webrtc_latest::,旧版变为 webrtc_legacy::。库中所有外部命名空间都进行了这样的改名。

不过,并非所有代码都位于命名空间中。全局 C 函数、全局变量,以及有意或无意留在命名空间之外的类,同样会冲突。

对于这些代码,能移入命名空间的就移入;其余符号,例如全局 C 函数,则通过各版本专属标识符区分。

宏与预处理标志带来更隐蔽的问题。RTC_CHECK、RTC_LOG 等宏也可能用于 WebRTC 外部的包装库。若在同一翻译单元包含两个版本的头文件,就会触发重复定义。

Meta 团队结合多种策略处理:

  • 移除不必要的包含。
  • 重命名很少使用的宏。
  • 尽可能在两个版本间共享 rtc_base 等内部模块。这样还能减少二进制体积,以及需要适配的代码范围。
旧版 WebRTC 共享目标依赖复用最新版实现,以减少重复目标与重叠代码
webrtc/legacy 的共享目标依赖取自 webrtc/latest,以复用目标并减少重叠范围。

向后兼容

改写 WebRTC 的所有符号会破坏全部外部调用点。Meta 团队希望现有代码继续工作,不受干扰。一些调用点只构建固定 flavor,而不是双栈。

最初,Meta 团队前置声明新命名空间中使用的全部符号,再连接到旧命名空间。这样可行,却生成了大型且脆弱的头文件,维护成本很高。

经过迭代,Meta 团队采用了更好的方案:用 C++ using 声明批量导入命名空间,把某个 flavor 的完整命名空间导入熟悉的 webrtc::。由此得到简洁的声明头文件,新符号会自动处理。它们只是编译器指令,因此不增加二进制体积。外部工程师继续按原方式编写代码;连接工作同步进行,只迁移 Meta 团队关心的外部调用点。

Flavor:运行时分派版本

适配层包裹了两套版本之后,下一个问题是如何在运行时分派。每个 adapter 与 converter 都需要根据全局配置,实例化正确的底层对象:webrtc_legacy:: 或 webrtc_latest::。

Meta 团队使用基于模板的辅助库。占适配代码大部分的共享逻辑只写一次,版本特定行为通过 C++ 模板特化表达。这样避免重复代码,并在迁移阶段兼容单 flavor 构建。全局 flavor 枚举在应用启动早期设置,决定启用哪套实现。

定向 adapter 是中间对象,实现统一 API 并调用底层 WebRTC 对象,或反向转发;定向 converter 则是工具函数,在 shim 与 WebRTC 的类型系统之间转换结构体与枚举。

双向适配器:向外部暴露 WebRTC 内部类,以及向 WebRTC 注入自定义组件
左:向外部调用者暴露 WebRTC 内部类。右:向 WebRTC 注入自定义组件。

生成适配层

适配层需要 adapter 与 converter。数十套 API 涉及大量对象,每项都要有抽象 API 定义、适配和转换实现,以及单元测试,估算的人工工作量非常大。

Meta 团队转向自动化,通过 AST 解析构建代码生成系统,为类、结构体、枚举和常量生成适配代码基础。这些生成代码有完整单元测试,并易于扩展。Meta 报告,产能从每天一个适配提升到三四个,同时降低人为错误风险。若版本间 API 相同,简单适配几乎无需人工修改;对于 API 差异、工厂模式、静态方法、裸指针语义和所有权转移等复杂情况,工程师会继续完善生成代码。

连接并构建双栈应用

具备适配层后,Meta 团队开始将全部应用引用从直接 WebRTC 类型改为 shim 类型。例如 webrtc::Foo 变为 webrtc_shim::Foo。这带来所有权复杂性,以及空值处理和内存管理中的潜在隐蔽错误。Meta 团队用全面单元测试复现所有权转移与对象生命周期的问题场景,并对风险较高的改动补充端到端测试。

随后从小目标开始,逐步构建完整双栈应用。每轮迭代都暴露新问题:缺失适配、对象 flavor 错误、新宏或符号冲突。

一些从外部注入 WebRTC 的内部组件深度依赖 WebRTC 内部结构,尤其棘手。给它们做 shim,相当于让 WebRTC 代理自身。因此 Meta 团队使用 C++ 宏与 Buck 构建机制“复制”组件:构建时动态改变命名空间,复制高层构建目标,并通过单个头文件暴露两种 flavor 的符号。

完成后,内部应用和部分外部应用均能以双栈模式构建,并使用旧版或新版进行音视频通话。

项目新增一万多行适配代码,在数千个文件中修改了数十万行。尽管范围很大,仔细测试与审阅使项目没有发生重大问题。

使用这一方案,Meta 团队逐应用对旧版与最新版进行 A/B 测试,减轻回归、交付新版本,并删除旧代码。如今部分应用仍使用适配层,以持续吸收上游最新更新。

方案二:功能分支

由于所用的单一代码仓库并未广泛支持分支,Meta 团队需要持续跟踪补丁,并将它们不断变基到上游。明确要求是:每个补丁都有清晰的目标与负责团队。

有两种选择:把补丁文件提交到版本控制中,每次按正确顺序重新应用;或者在支持分支的独立仓库中跟踪补丁。

最终,Meta 团队选择在独立 Git 仓库中跟踪功能分支。其中一个原因是建立良好流水线,让功能分支与修复易于贡献给上游。

这些分支基于 libwebrtc Git 仓库,因此可以方便复用 Chromium 上游构建、测试与提交工具,包括 gn、gclient、git cl 等。

每个 Chromium 上游版本都创建一个基础分支,例如 M143 对应 Git 标签7499,Meta 团队建立 base/7499。每个补丁再基于基础提交建立分支,例如 debug-tools/7499。升级时,将各功能分支向前合并:debug-tools/7499 合并进 debug-tools/7559,hw-av1-fixes/7499 合并进 hw-av1-fixes/7599,依此类推。

全部功能都向前合并、解决冲突并通过构建与测试后,再依次合并全部功能分支,创建候选发布分支 r7559。

上游基础分支、内部功能分支和发布分支的持续合并流程
上游分支作为基础;内部功能分支在持续集成中反复验证,再合并到发布分支并导入单一代码仓库。图示中间版本为 7599,正文例子同时出现 7559 与 7599,均为原文编号。

这一方式的优点包括:分支较多时可以高度并行,自动保留完整 Git 历史与上下文,适合今后引入大模型自动解决合并冲突。此外,功能分支方便作为整体提交为开源上游贡献。

结果:持续升级

这种架构让 Meta 团队交付同时包含旧版和新版 WebRTC 的二进制。webrtc/latest 从 M120 启用,原文发表时已推进到 M145。截至原文发表时,Meta 团队不再落后多年,而是跟随最新稳定 Chromium,及时吸收上游升级。

关键工程收益

  • 性能:Meta 报告主要应用 CPU 使用最多下降10%,崩溃率最多改善3%。
  • 二进制体积:新版上游更高效,依据应用不同,压缩体积减少100到200KB。
  • 安全:移除 usrsctp 等弃用库,修复旧栈中的安全漏洞。
  • 上述变化使应用在现代技术栈上运行,同时带来可观测的用户参与度改善。

项目证明,即使单一代码仓库复杂且约束众多,也能在无需完全重写的情况下处理技术债。适配层加双栈方案,为希望摆脱分叉陷阱的组织提供了参考。

后续工作:AI 驱动的维护

迁移完成后,Meta 团队在原文中介绍了接下来的维护阶段。虽然已跟随最新版本,仍需在上游之上应用内部补丁。为提高效率,Meta 团队使用工具自动化工作流:

  • 构建健康:开发代理,自动修复 Git 分支中的构建错误。
  • 冲突解决:将补丁变基到 WebRTC 新版本时会遇到合并冲突。Meta 团队训练 AI 代理自动解决大部分冲突,只把最复杂的架构变更留给人工工程师。

致谢

这项工作由一支小型工程团队完成。他们认识到项目的战略价值,尽管复杂,仍全力投入,提出创意与方案,承担繁重工作,克服意外阻碍和独特挑战,最终完成项目:Dor Hen、Guy Hershenbaum、Jared Siskin、Liad Rubin、Tal Benesh、Yosef Twaik。


原文:Escaping the Fork: How Meta Modernized WebRTC Across 50+ Use Cases,作者 Boris Tsirkin、Joachim Reiersen,Meta Engineering,2026 年 4 月 9 日。中文译文。架构、实验与收益数字均来自 Meta 原文。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容