Cloudflare 始终致力于构建并运营全球最快的网络。自 2021 年起,我们一直在 跟踪和报告网络性能表现:您可在此处查看最新更新。
构建最快的网络需要在多个领域持续投入。我们在硬件层面投入大量精力,以便拥有高效且快速机器;投资于对等互连安排,确保与互联网各节点的通信延迟降至最低。除此之外,我们还必须投资运行网络的软件,因为每新增一项网络服务,都可能引入额外的处理延迟。
无论请求消息的到达速度有多快,如果处理软件在思考如何处理和响应请求时耗时过长,就会形成性能瓶颈。今天,我们很高兴地宣布对软件进行了重大升级——根据第三方 CDN 性能测试结果显示,此次升级将我们的中位响应时间缩短了 10 毫秒,整体性能提升了 25%。
我们花了一年时间重建系统的主要组件,并大幅降低了数百万客户网络流量的延迟。与此同时,我们提升了系统安全性,并缩短了新产品的开发和发布时间。
每一个访问 Cloudflare 的请求,都会开启一段在我们网络中的旅程。这些请求可能来自浏览器加载网页、移动应用调用 API,或是来自其他服务的自动化流量。这些请求首先会在我们的 HTTP 和 TLS 层终止,然后进入我们称之为 FL 的系统,最后经过 Pingora,该组件会根据需要执行缓存查询或从源站获取数据。
FL 是 Cloudflare 的“大脑”。一旦请求到达 FL,我们就会在网络中运行各种安全和性能功能。它会应用每个客户的独特配置和设置,从执行 WAF 规则和 DDoS 防护,到将流量路由至开发人员平台和 R2。
FL 系统始建于 15 年前,一直是 Cloudflare 网络的核心。它让我们能够提供广泛的功能,但随着时间的推移,这种灵活性逐渐成为了一种挑战。随着我们不断推出更多产品,FL 变得越来越难以维护,处理请求的速度变慢,扩展起来也更加困难。每新增一项功能,都需要仔细检查现有逻辑,而且每次添加都会引入少许额外的延迟,这使得我们越来越难以维持期望的性能水平。
由此可见,FL 是我们系统的关键——我们经常称它为 Cloudflare 的“大脑”。它也是我们系统中最古老的部分之一:代码库的第一次提交是由我们的创始人之一 Lee Holloway 在我们首次发布之前做出的。本周我们将庆祝 15 岁生日——而这个系统早在我们成立的 9 个月前就已启动!
commit 39c72e5edc1f05ae4c04929eda4e4d125f86c5ce
Author: Lee Holloway <q@t60.(none)>
Date: Wed Jan 6 09:57:55 2010 -0800
nginx-fl initial configuration
正如提交记录所示,FL 的第一个版本是基于 NGINX 网络服务器实现的,其产品逻辑采用 PHP 实现。然而三年后,该系统变得过于复杂而难以有效管理,响应速度也过于缓慢,于是我们对正在运行的系统进行了几乎彻底的重写。这也催生了另一个重要的代码提交,而这次提交的作者正是我们现任的首席技术官 Dane Knecht。
commit bedf6e7080391683e46ab698aacdfa9b3126a75f
Author: Dane Knecht
Date: Thu Sep 19 19:31:15 2013 -0700
remove PHP.
从那时起,FL 的实现采用了 NGINX、OpenResty 框架以及 LuaJIT。这在很长一段时间内表现都很出色,但在过去几年里,它开始逐渐显露出局限性。我们不得不花费越来越多的时间去修复或规避 LuaJIT 中那些晦涩难懂的错误。我们当初为了能快速实现业务逻辑,采用了高度动态且结构松散的 Lua 代码编写方式,这种做法在当时是一种优势,但当需要集成大量复杂的产品逻辑时,却成了错误频发和进度延迟的源头。每当有新产品推出时,我们都必须逐一检查其他所有已有的产品,确认它们是否会受到新逻辑的影响。
很显然,我们需要重新思考架构方案。于是,在 2024 年 7 月,我们为这个全新且截然不同的实现提交了初始代码。为了节省时间来商定一个新名称,我们就直接称它为“FL2”,当然,原来的 FL 也就顺理成章地被称为“FL1”了。
commit a72698fc7404a353a09a3b20ab92797ab4744ea8
Author: Maciej Lechowski
Date: Wed Jul 10 15:19:28 2024 +0100
Create fl2 project
我们并不是从零开始的。我们此前曾发布过博客文章,介绍如何用 Pingora 替换掉我们另一套旧系统。Pingora 是用 Rust 编程语言开发的,并使用了 Tokio 运行时。我们还写过关于 Oxy 的博客,Oxy 是我们内部用于用 Rust 构建代理的框架。我们编写了大量 Rust 代码,并且在这方面已经积累了相当丰富的经验。
我们在 Oxy 上使用 Rust 构建了 FL2,并建立了一个严格的模块框架来组织 FL2 中的所有逻辑。
在启动 FL2 项目之初,我们就清楚地认识到:这不仅仅是在替换一个旧系统,而是在重建 Cloudflare 的整个技术根基。这意味着我们需要的不只是一个简单的代理,而是一个能够与我们共同演进、承载网络海量规模负载,并能让各团队在确保安全性和高性能的同时快速推进开发的框架体系。
Oxy 为我们提供了性能、安全性与灵活性的强大组合。它基于 Rust 构建,彻底消除了困扰我们基于 Nginx/LuaJIT 的 FL1 的一整类问题,比如内存安全问题和数据竞争,同时还能提供接近 C 语言级别的性能。在 Cloudflare 的规模下,这些保障不是可有可无的附加项,而是必不可少的核心要求。每个请求节省下来的微秒时间,都能切实提升用户体验;每一次避免的崩溃或边界情况,都有助于保持互联网的稳定运行。Rust 严格的编译时保障也与 FL2 的模块化架构完美契合,在该架构中,我们在产品模块及其输入和输出之间强制执行清晰的契约。
但做出这一选择并不仅仅是因为编程语言。Oxy 是我们多年来构建高性能代理的经验结晶。它已经为多个重要的 Cloudflare 服务提供支持,从我们的 Zero Trust Gateway 到 Apple 的 iCloud 私密中继服务,所以我们知道它能够应对 FL2 可能遇到的多样化流量模式和协议组合。它的可扩展性模型让我们能够拦截、分析和处理从第 3 层到第 7 层的流量,甚至还能对不同层的流量进行解封装和重新处理。这种灵活性是 FL2 设计的关键所在,因为这意味着我们可以以一致的方式处理从 HTTP 到原始 IP 流量的所有内容,并且能够在不重写核心基础组件的情况下,让平台不断演进以支持新的协议和功能。
Oxy 还内置了丰富的功能,这些功能以前需要大量定制代码才能实现。监控、软重载、动态配置加载和交换等功能都包含在框架中。这使得产品团队可以专注于其模块的独特业务逻辑,而不必每次都重新设计流程。这个坚实的底层基础让我们能够自信地进行变更,快速完成部署,并确保新功能上线后能如预期般稳定运行。
Oxy 带来的最重要改进之一,就是对重启过程的处理。任何处于持续开发与优化阶段的软件,最终都免不了要进行更新。对于桌面软件来说,这很容易:您只需关闭程序、安装更新,然后再重新打开即可。但在 Web 环境中,情况要复杂得多。我们的软件始终处于使用状态,无法简单地直接停止运行。一次 HTTP 请求的中断可能导致页面加载失败,而连接中断则可能让您从视频通话中被踢出。因此,可靠性绝非可有可无的选项。
在 FL1 中,升级意味着要重启代理进程。而重启代理实际上就是彻底终止该进程,这将立即中断所有活跃的连接。这对于 WebSocket、流媒体会话和实时 API 等长连接场景来说尤为痛苦。即使是计划内的升级也可能会导致用户可见的中断,而在发生故障期间意外触发的重启情况则可能更为严重。
Oxy 改变了这一局面。它内置了优雅重启机制,让我们能够尽可能地在不掉线的情况下推出新版本。当基于 Oxy 的服务的新实例启动时,旧实例会停止接受新连接,但会继续为现有连接提供服务,从而使这些会话能够不间断地持续运行,直到它们自然结束。
这意味着,当我们在部署新版本时,如果您有正在进行的 WebSocket 会话,该会话可以不受干扰地持续运行,直到它自然结束,而不会因为重启而被强制中断。在整个 Cloudflare 服务器集群中,部署操作会在数小时内协调进行,因此整体发布过程平稳顺畅,终端用户几乎无感知。
我们更进一步采用了 systemd 的套接字激活机制。不再让每个代理自行管理其套接字,而是由 systemd 来创建并拥有这些套接字。这样做将套接字的生命周期与 Oxy 应用程序本身的生命周期解耦。如果 Oxy 进程重启或崩溃,套接字仍然保持开启状态,随时可以接受新连接,一旦新的进程启动运行,这些连接就会被立即处理。这消除了在 FL1 中重启期间可能出现的“连接被拒绝”错误,并提高了升级过程中的整体可用性。
我们还基于 Rust 自主研发了协调机制,用自研的 shellflip 替代了 Go 语言的 tableflip 等库。该机制通过重启协调套接字实现配置校验、新实例孵化,并确保新版本健康运行后才关闭旧实例。这一改进优化了反馈闭环,使自动化工具能即时检测并响应故障,而无需依赖盲目的基于信号的重启机制。
为避免重蹈 FL1 的覆辙,我们希望设计一种能让所有产品逻辑之间的交互都清晰明确、易于理解的模式。
因此,在 Oxy 提供的基础之上,我们构建了一个平台,将所有产品逻辑分离成定义明确的模块。经过一些实验和研究,我们设计了一个模块系统,并强制执行一些严格的规则:
以下是一个模块阶段定义的示例:
Phase {
name: phases::SERVE_ERROR_PAGE,
request_types_enabled: PHASE_ENABLED_FOR_REQUEST_TYPE,
inputs: vec![
InputKind::IPInfo,
InputKind::ModuleValue(
MODULE_VALUE_CUSTOM_ERRORS_FETCH_WORKER_RESPONSE.as_str(),
),
InputKind::ModuleValue(MODULE_VALUE_ORIGINAL_SERVE_RESPONSE.as_str()),
InputKind::ModuleValue(MODULE_VALUE_RULESETS_CUSTOM_ERRORS_OUTPUT.as_str()),
InputKind::ModuleValue(MODULE_VALUE_RULESETS_UPSTREAM_ERROR_DETAILS.as_str()),
InputKind::RayId,
InputKind::StatusCode,
InputKind::Visitor,
],
outputs: vec![OutputValue::ServeResponse],
filters: vec![],
func: phase_serve_error_page::callback,
}
本阶段主要针对我们的自定义错误页面产品。该模块接收多项输入参数——包括访客 IP 信息、部分标头及其他 HTTP 信息,以及一些“模块值”。模块值是模块间传递信息的机制,也是保障模块系统严格属性得以实现的关键所在。以本模块为例,它需要依赖基于规则集的自定义错误产品所生成的特定信息(即输入参数中的“MODULE_VALUE_RULESETS_CUSTOM_ERRORS_OUTPUT”)。所有这些输入输出定义均在编译阶段强制执行。
虽然这些规则很严格,但我们发现,我们可以在这个框架内实现所有产品逻辑。这样做的好处是,我们可以立即发现哪些其他产品可能会相互影响。
构建框架是一回事,构建所有产品逻辑并使其正确无误,以便客户除了性能改进之外不会注意到任何其他东西,则是另一回事。
FL 代码库支撑了 Cloudflare 长达 15 年的产品迭代,且始终处于不断变化之中。我们无法暂停开发工作。因此,我们的首要任务之一就是寻找让迁移过程更轻松、更安全的方法。
第一步 - OpenResty 中的 Rust 模块
用 Rust 重写产品逻辑本身就已经会分散我们向客户交付产品的精力了。要是再要求所有团队同时维护两套产品逻辑版本,并且在完成迁移之前对每一处改动都要重新实现一遍,那实在是负担太重了。
为此,我们在基于 NGINX 和 OpenResty 的旧版 FL 系统中实现了一个兼容层,让新模块能够顺利运行。这样一来,各团队无需维护两套并行系统,可以直接用 Rust 编写业务逻辑,并逐步替换原有的 Lua 代码——无需等待整个旧系统被完全取代。
例如,下面是之前提到的自定义错误页面模块阶段的部分实现代码(我们删减了一些较为繁琐的细节,所以按此原文直接编译是无法通过的):
pub(crate) fn callback(_services: &mut Services, input: &Input<'_>) -> Output {
// Rulesets produced a response to serve - this can either come from a special
// Cloudflare worker for serving custom errors, or be directly embedded in the rule.
if let Some(rulesets_params) = input
.get_module_value(MODULE_VALUE_RULESETS_CUSTOM_ERRORS_OUTPUT)
.cloned()
{
// Select either the result from the special worker, or the parameters embedded
// in the rule.
let body = input
.get_module_value(MODULE_VALUE_CUSTOM_ERRORS_FETCH_WORKER_RESPONSE)
.and_then(|response| {
handle_custom_errors_fetch_response("rulesets", response.to_owned())
})
.or(rulesets_params.body);
// If we were able to load a body, serve it, otherwise let the next bit of logic
// handle the response
if let Some(body) = body {
let final_body = replace_custom_error_tokens(input, &body);
// Increment a metric recording number of custom error pages served
custom_pages::pages_served("rulesets").inc();
// Return a phase output with one final action, causing an HTTP response to be served.
return Output::from(TerminalAction::ServeResponse(ResponseAction::OriginError {
rulesets_params.status,
source: "rulesets http_custom_errors",
headers: rulesets_params.headers,
body: Some(Bytes::from(final_body)),
}));
}
}
}
每个模块的内部逻辑与数据处理实现了高度解耦,而 Rust 语言的设计特性也鼓励采用清晰、明确的错误处理机制。
我们许多开发最为活跃的模块都采用了这种方式进行处理,从而让各个团队在我们进行迁移的过程中,依然能够保持原有的开发迭代节奏。
要顺利完成如此大规模的迁移,一套强大可靠的测试框架必不可少。为此,我们内部开发了一套名为 Flamingo 的测试系统,能够同时对生产环境和预生产环境并发执行数千个完整的端到端测试请求。相同的测试用例会在 FL1 和 FL2 上同步运行,这样我们就能确信系统行为没有发生意外改变。
每当我们部署一个变更时,该变更会逐步在多个阶段中滚动发布,每个阶段处理的流量逐渐增加。每个阶段都会自动进行评估,只有当针对该阶段运行了全部测试用例并且全部通过,同时整体性能和资源使用指标也在可接受范围内时,该阶段才会通过。整个系统是完全自动化的,如果测试失败,系统会自动暂停或回滚变更。
好处是,我们能够在 48 小时内在 FL2 中构建并发布新的产品功能——而这在 FL1 中可能需要数周时间。事实上,本周至少有一项公告涉及这样的变化!
超过 100 名工程师参与了 FL2 的开发工作,我们已经构建了超过 130 个模块。不过,我们的工作尚未全部完成。我们仍在对系统进行最后的完善,以确保它能完全复现 FL1 的所有行为特性。
那么,我们如何在 FL2 尚不能处理所有请求的情况下将流量发送到 FL2 呢?如果 FL2 收到一个它不知道如何处理的请求,或者与该请求相关的一段配置,它就会放弃处理,并执行我们所说的回退——将整个请求转发给 FL1。这种回退是在网络层完成的,它只是简单地将原始数据字节转发给 FL1。
这一设计不仅让我们能够在 FL2 尚未完全就绪时就将流量导向它,还带来了另一个显著优势:当我们在 FL2 中实现了某项新功能,但又想仔细验证它的实际表现是否与 FL1 一致时,我们可以先在 FL2 上评估该功能,然后触发回退机制。通过这种方式,我们能够直接对比两个系统的行为表现,从而高度确信我们的实现是准确无误的。
我们在 2025 年初就开始将客户流量引导至 FL2,并在这一年中逐步增加了通过 FL2 处理的流量比例。本质上,我们一直在观察两张图表:一张显示路由至 FL2 的流量比例在不断上升,另一张则显示因 FL2 无法处理而回退到 FL1 的流量比例在持续下降。
我们首先将免费客户的流量导入新系统进行验证。通过这一过程,我们不仅验证了系统的正确性,还显著降低了主要模块的回退率。我们的 Cloudflare 社区中最活跃的核心用户 (MVP) 充当了早期预警系统,当他们怀疑某个新上报的问题可能与新平台有关时,就会进行冒烟测试并及时反馈。尤为关键的是,他们的支持使我们团队能够快速展开调查,实施针对性修复,或确认问题并非由迁移到 FL2 所导致。
随后,我们将系统推广至付费客户群体,并逐步增加使用该系统的客户数量。与此同时,我们还与希望借助 FL2 获得性能提升的部分大型客户展开了紧密合作,因此我们提前让他们接入了新系统,作为交换,他们也向我们提供了大量关于系统的反馈意见。
目前,我们的大多数客户已经在使用 FL2 了。虽然仍有一些功能尚未完成,暂时还不能让所有客户都迁移过来,但我们的目标是在未来几个月内停用 FL1。
正如我们在本文开头所描述的,FL2 的运行速度明显快于 FL1。造成这一结果的最主要原因其实很简单:FL2 执行的工作量更少。您可能已经注意到,在模块定义示例中有这样一行代码:
filters: vec![],
每个模块都能够提供一组过滤器,用于控制自身是否运行。这意味着我们无需为每个请求都执行所有产品的逻辑,而是可以非常轻松地选择仅需要的模块集。由此,我们开发每个新产品时所产生的额外开销也随之消除。
性能更佳的另一个重要原因是 FL2 是一个单一代码库,使用注重性能的语言实现。相比之下,FL1 基于 NGINX(用 C 语言编写),结合 LuaJIT(Lua 和 C 接口层),还包含大量 Rust 模块。在 FL1 中,我们花费了大量时间和内存将数据从一种语言所需的表示形式转换为另一种语言所需的表示形式。
结果表明,内部监测数据显示 FL2 所消耗的 CPU 资源不到 FL1 的一半,内存占用更是远低于 FL1 的一半。这是一个巨大的优势——我们可以将节省下来的 CPU 资源用于为我们的客户交付越来越多的功能!
我们使用自己的工具和独立基准测试(例如 CDNPerf)来衡量 FL2 在全网推广过程中的影响。结果显而易见:网站响应速度中位数加快了 10 毫秒,性能提升了 25%。
FL2 在设计上也比 FL1 更加安全。没有哪个软件系统是完美无缺的,但相比 LuaJIT,Rust 语言为我们带来了巨大的优势。Rust 拥有强大的编译时内存检查机制和类型系统,能够避免一大类常见错误。再加上我们严格的模块系统,我们在做出大多数改动时都能充满信心。
当然,如果使用不当,任何系统都谈不上安全。用 Rust 编写代码很容易导致内存损坏。为了降低风险,我们维护强大的编译时检查机制,并采用严格的编码标准、测试和审查流程。
长期以来,我们一直遵循一项原则:对于系统中出现的任何不明原因崩溃,都必须作为高优先级事项进行调查。我们不会放松这一原则,尽管到目前为止,FL2 中出现的新崩溃案例主要都是由硬件故障引起的。这类崩溃事件发生率的显著降低,将为我们留出充足的时间,更妥善地开展相关调查工作。
我们将在 2025 年剩余的时间里完成从 FL1 到 FL2 的迁移工作,并计划在 2026 年初停用 FL1。目前,我们已经在客户性能提升和开发速度方面看到了成效,我们也期待着让所有客户都能享受到这些好处。
我们还有一个最后的服务需要完全迁移。文章开头那张图里的“HTTP 与 TLS 终止”模块同样是一个基于 NGINX 的服务,目前我们正在进行用 Rust 重写的开发工作,且已进展到一半。这次迁移工作推进顺利,我们预计明年年初就能完成。
完成上述步骤后,当所有模块都实现了模块化、采用 Rust 编写、经过测试和扩展时,我们就可以真正开始进行优化了!我们将重新组织和简化各个模块之间的连接方式,扩展对 RPC 和流媒体等非 HTTP 流量的支持,以及更多其他优化举措。
如果您有兴趣参与这一征程,欢迎查看我们的招聘页面了解开放职位——我们一直在寻找优秀人才,携手共建更美好的互联网。