我们如何(及为何)将生产环境的C++前端基础设施重写为Rust

Lobsters Hottest 新闻

摘要

NearlyFreeSpeech.NET 将其生产环境的C++前端基础设施(nfsncore)重写为Rust,该系统负责所有传入请求的路由、缓存和访问控制。迁移的动机是Rust的安全性保证、性能、生态系统优势以及老化的C++代码库的局限性。

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

缓存时间: 2026/04/20 14:44

# NearlyFreeSpeech.NET 博客 » 我们如何(以及为何)将生产级 C++ 前端基础设施重写为 Rust 来源:https://blog.nearlyfreespeech.net/2026/04/17/how-and-why-we-rewrote-our-production-c-frontend-infrastructure-in-rust “我们应该把 \_\_\_\_\_ 代码换成 Rust 吗?”这个问题经常出现。大多数时候,正确的答案都是相当坚决的“不”。所以我认为,通过一个实际案例来展示——当代码本身**极其**重要、**绝对不能**出错时,当我们提出这个问题时,我们最终回答“是”——可能会很有帮助(也很有趣)。下面就是这段代码是什么,我们如何得出那个答案,以及我们具体做了什么。 首先,当我们说“前端”时,我们指的不是大多数人(以及我们大多数会员)所认为的前端。我们的前端由部署在会员站点前面的服务器组成,负责缓存、代理、路由、访问控制和 TLS。 现在,当人们想到这些东西时——如果他们真的想过的话——通常会觉得这就是托管中的 Apache 部分。这个说法……正确但不完整。是的,前端服务器上确实运行着 Apache。但要让我们的服务正常运行,至少需要四个自定义 Apache 模块。其中一个模块将关于传入请求的所有决策权从 Apache 中剥离出来,传递给一个用 C++ 编写的自定义服务器进程(“nfsncore”)。当你在 UI 中添加自定义 IP 访问控制时,正是 nfsncore 将它们应用到传入请求上。当你设置一系列代理将请求路由到站点上的各种自定义守护进程时,也是 nfsncore 来判断传入请求应该使用哪一个。它处理重定向到正确的别名。它处理通配符别名(为那些我猜只有两个人还在用的功能)。它处理你的 Strict-Transport-Security。它处理维护模式。它处理离线站点。它捕获更新 TLS 证书所需的 ACME 请求。它还做一大堆检查来过滤掉愚蠢的、损坏的请求。 就我们的服务而言,nfsncore **事关重大**。无论你在我们这里托管什么,无论你的技术栈是什么,nfsncore 都会处理**每个**请求。我们服务的很多部分如果出问题可能影响部分用户,而这是如果出问题可能影响**所有用户**的部分。 但是,哎呀,我犯了个错误。我应该说:其中一个模块将关于传入请求的所有决策权从 Apache 中剥离出来,传递给一个用 **Rust** 编写的自定义服务器进程(“nfsncore”)。截至昨天,C++ 版本已经不再运行在任何服务器上了。 ## 是什么驱使你们这么做? 这可能是大多数人首先想到的问题。难道没听过“没坏就别修”吗?第一次重新发明轮子还不够糟糕吗?我们还要把那个重新发明的轮子再实现一遍?这些都是合理的问题。如果你打算对生产应用进行全面重写,你最好有充分的理由。对此我可以给出很多好的理由。Rust 是一种出色的语言,正为此类用途提供了顶级的安全性。Rust 速度非常快。Rust 生态系统极其强大。Cargo 让你很容易避免重新发明轮子。而 C++ 没有集中的生态系统。Boost 是最接近的,但它可能相当抽象,而且用起来不总是那么愉快。此外,我们的 C++ 代码已经非常老旧,有些做法使得添加新功能变得困难。但老实说,还有另一个因素。 当你在路由 URL 时,主机名是不区分大小写的。你需要将它们转换为小写。在 C++ 中是这样做的: `std::transform( _stHost.begin(), _stHost.end(), _stHost.begin(), [](unsigned char c) { return std::tolower(c); } );` 在 Rust 中是这样做的: `host = host.to_lowercase();` 如果你不懂 C++ 或 Rust,那么哪一种更容易让你猜出它的作用呢? 是的,C++ 的语法非常健壮、灵活,并且在某种意义上组合性很强。它就地转换,在内存效率上略微优于 Rust 的内置方法。但是,拜托,现在是 2026 年了,“将字符串转换为小写”这种操作对标准库来说还是太麻烦了吗?我必须逐个字符地做?使用两个模板和一个 lambda 函数? 这看起来可能有点抱怨得莫名其妙。因为它确实是个有点奇怪的抱怨。但其中包含的真相是:大量的 C++ 代码都是这样的。C++ 及其标准库采用了一种最低公共分母的方法。我们不能拥有好东西,因为我们还必须与仍在使用 EBCDIC 的大型机以及只出现在雷神公司制造的导弹中的 4 位嵌入式微控制器共享同一个游乐场。 归根结底,C++ 已经导致了不少情况,我们想做某事或添加一个功能,但结果就是……这是个好主意,但不值得为了对抗这门语言而付出艰辛努力。而且已经到了任何更改都带有不可预见后果风险的地步。 这段软件非常适合转换,因为我们一直以来都像对待 Rust 那样对待 C++:如果你使用 RAII 和智能指针,并对 `const` 非常挑剔,那么你就在通往内存安全的道路上前进了很远。但这只是可选的,而且你必须永不犯错。不过,多年来一直非常严格地坚持这一点意味着,比如说,著名的 Rust 借用检查器对我们来说并不是像对某些团队那样的障碍。 此外,代码库本身并不大。例如,它的大小还不到我们会员界面 PHP 代码库的 10%。这段代码的复杂性不在于其长度,而在于长期积累的知识:你必须**这样**做才能将请求代理到**那里**,否则它们就无法连接。如果你要将 http 重定向到 https,你必须完全按**那个顺序**做**这些**事情,否则浏览器会抱怨。 项目的目标是让 Rust 版本在转换时与 C++ 版本完美匹配,所以本文不会涉及任何酷炫的新功能。但是,嘿,现在有些束缚被解开了。敬请期待! ## 转换过程:偷走你所有家具,换成一模一样的东西 我们从一开始就知道,我们希望过渡过程尽可能无缝,同时也知道这将会很艰难。所以我们制定了一个相当详细的计划,而且实际上我们相当严格地执行了它。 1. **C++ 单元测试。** 我们为 C++ 代码添加了**数百**个单元测试。之前我们也有测试,但很多更像是集成测试。添加单元测试让我们能够定义软件各个层面各种情况下的行为。 2. **Rust 编码。** 我们以 C++ 代码为指导,编写了初始的 Rust 代码。每个 C++ 库(构成应用的共有七个)都有一个对应的 Rust crate。每一个 C++ 单元测试都有一个对应的 Rust 版本,此外还有我们为了获得全面覆盖率所需的任何额外单元测试。在此步骤中,我们还编写了一些与系统其他部分交互的 Rust 命令行小工具。比如一个小工具可以让我们在一个前端上查找别名并转储所有路由信息。这表明代码正在工作。而且它是一个很方便的小助手,因为替代方案是登录数据库,靠一个五行的 SELECT 来获取你需要的信息,全靠运气。 3. **互操作性测试。** Apache 通过我们其中一个自定义 Apache 模块使用 IPC 与 nfsncore 交互。该 IPC 有三种不同的实现方式。有一个使用 Apache 可移植运行时的 C 客户端实现,由该模块使用;在这次迁移中它没有(太多)改变。而 C++ 和 Rust 版本都有客户端和服务端实现。因此我们开发了另一套测试,重点确保任何一个客户端都能与另一个服务端配合工作。 4. **功能测试。** 我们有一个完整的测试框架,可以在 Apache 外部运行 nfsncore Apache 客户端模块,因此我们有一整套相关的测试,以确保 C++ 和 Rust 版本都能与它配合工作并产生相同的结果。 5. **Rust 模糊测试。** 模糊测试是我们在 C++ 中从未有过的功能,但在 Rust 中非常容易实现。它获取一些已知输入,然后开始尝试对这些输入进行随机变异,看看软件是否会崩溃。我们尝试了几亿次这样的变异。 此时,我们已经测试了所有内容,准备好投入生产了,对吧?哦不,亲爱的读者,我们才刚刚热身呢! 6. **回放测试。** 我们编写了一个客户端,它可以解析来自生产服务器的日志文件,将请求回放到 Rust 版本中,并使用真实数据确保得到相同的结果。(允许一些变化,比如一个站点最初可用但碰巧在我们运行测试之前被禁用了。)这是一个很好的测试,但并不完美,因为 nfsncore 的结果有时与 HTTP 状态码不同——nfsncore 的代码嵌入了额外的信息来告诉 Apache(例如)对于特定的 503 错误应该返回哪个错误页面:离线站点、维护模式还是服务中断。而且如果 nfsncore 说“成功,从会员站点获取内容”,会员站点可能仍然返回 404,而这正是最终记录在日志中的内容。所以结果经常与日志不匹配,但这并不表示实际存在问题。这使得这个测试的效果不如我们希望的那么好,将来我们可能不会再采用这种策略。 7. **代理测试。** 我们用 Rust 编写了一个代理,它从 Apache 模块接收输入,并实时并排运行 C++ 和 Rust 版本的 nfsncore,将所有传入请求同时发送给两者,将 C++ 的结果返回给 Apache,并报告任何差异。我们每天在一台服务器上部署这个代理,从保留给 beta 站点的服务器开始,直到覆盖了我们 50% 的前端服务器。正是在这里我们发现了一些有趣的 bug。不仅 Rust 版本有,原始 C++ 版本也有。我们忠实地重现了这些 bug。这些都是真正的边缘案例,大多与损坏的客户端有关。所以我们在两个版本中都修复了它们,然后重新测试。一旦我们能够在三天内没有任何差异——除了极少数极窄的时间窗口,比如某个会员恰好在其站点状态在两个版本查询之间的微秒窗口内启用/禁用了站点——我们就准备好继续了。 8. **统计分析。** 由于我们的负载均衡,请求在给定位置的不同服务器之间相当均匀地分布。因此,尽管通过特定服务器的单个访问是独特的,但总体上它们是相当相似的。我们从运行代理代码的 50% 服务器中随机抽取了 1.5 亿个请求,并从运行完全未修改的原始 C++ 代码的另外 50% 服务器中抽取了另外 1.5 亿个请求。我们分析了两个样本中请求延迟和 HTTP 状态码的分布。我们这样做了七次,连续一周每天一次。延迟在功能上是相同的(没有桶差异 >0.1%)。状态码在大多数情况下也相差 <0.1%。在存在差异的地方(最高达 0.5%),我们进行了调查,并确认这与我们在代理测试中修复的边缘案例有关。 9. **分阶段部署。** 此时,我们觉得 Rust 版本已经准备好了。但你不能过于自信。所以我们继续每天在一台服务器上推出代理-C++-Rust 三重组合,直到达到 100% 部署。然后我们每天处理一台服务器并反转角色,使 Rust 版本成为权威版本,而不是 C++。一旦达到 100% 部署,我们每天处理一台服务器,移除代理和 C++ 版本,让 Rust 版本独自在生产环境中全速运行。昨天,我们达到了 Rust 版本的 100% 生产部署。 Rust 的性能大致与 C++ 版本相当。它慢了大约几个百分点,但两个版本都可以在大约 10% 的利用率下处理满载生产,所以有足够的余量。 Rust 版本确实有一些改进,但可惜这些改进是为我们自己,而非为你们。它直接将统计数据报告到我们的主遥测系统中,而不是写入 stderr。它通过与我们会员界面相同的管道报告带有堆栈跟踪的错误,这样我们就不必完全依赖远程监控来告知我们前端服务器出现故障。在 C++ 中实现这些东西是永远不会发生的:它们不值得付出努力,也不值得冒风险。现在这个账怎么算都不同了。 我们对此更改感到非常满意。最成功的 IT 项目通常都是那些没人注意到的项目。*(当然,除非有人写了一篇长篇博文来讨论它。)* 我并不是说过去几个月没有遇到过任何减速带。就在最近,我们不得不在相当短的时间内 YOLO 式地推送了一些内核补丁,这总是有点破坏性的。但如果你在过去几个月里确实遇到了什么奇怪的事情,那肯定不是因为这个。不过,我希望这个项目能向我们自己和我们的会员证明,尽管我们通常的行为方式如此,但在关键时刻,我们是能够认真行事的。** 既然我们声称这件事值得做的一部分原因是 Rust 提供了 C++ 所没有的未来增强潜力,那么我们现在要做的就是兑现这一点。没问题!(https://www.youtube.com/shorts/g27eiU_t_n4)让我先…… 503 Service Unavailable 请稍后再试。 * OK,严格来说,至少**有一个人注意到了**(https://members.nearlyfreespeech.net/forums/viewtopic?t=12241),因为我们曾短暂犯了个错误,让一段不像我们想象中那么 beta 的代码溜到了一台服务器上。 ** 只要我们都明白这**是**一场表演就好。 本文章的评论 RSS 订阅。(https://blog.nearlyfreespeech.net/2026/04/17/how-and-why-we-rewrote-our-production-c-frontend-infrastructure-in-rust/feed/) TrackBack URI (https://blog.nearlyfreespeech.net/2026/04/17/how-and-why-we-rewrote-our-production-c-frontend-infrastructure-in-rust/trackback/)

相似文章

12万行Rust代码:深入Nosdesk后端

Hacker News Top

深入技术解析Nosdesk的Rust后端,涵盖架构决策如流式管道、Postgres同步以及贯穿12万行代码的类型安全设计模式。

从Go迁移到Rust

Hacker News Top

一份为Go开发者迁移到Rust编写的全面指南,专注于后端服务,对比正确性、运行时和人体工程学方面的权衡,并提供关于渐进式迁移的实用建议。

Rust语言的性能

Lobsters Hottest

本次演讲分析了Rust相较于C++的性能优势与劣势,提供了基准测试和最佳实践。附有幻灯片和阅读材料。

从Rust到Ruby

Hacker News Top

开发人员描述使用LLM将一个15,000行的Rust Web应用转换为Ruby on Rails,发现Ruby版本明显更短,并评估了开发速度、安全性和可测试性方面的权衡。