p99 延迟从 9.5 毫秒降到 18 微秒:Cloudflare 机器学习基础设施重构全记录
2026/5/6 16:20:29 网站建设 项目流程

有一类工程问题,不是因为代码写错了,而是因为系统架构在规模增长之后触碰到了物理极限。

Cloudflare 的机器学习团队经历的正是这种情况。他们的 Bot 检测系统需要对每一个 HTTP 请求做实时机器学习推理,而 Cloudflare 的流量峰值是每秒 6300 万次请求。在这个规模下,任何看起来"还好"的延迟,乘以请求量之后都会变成一个天文数字。

这篇文章记录了他们如何把机器学习特征提取的 p99 延迟从9.5 毫秒降到18 微秒——降幅 99.8%,速度提升 528 倍。

原文链接:https://blog.cloudflare.com/scalable-machine-learning-at-cloudflare/

mmap-sync 开源地址:https://github.com/cloudflare/mmap-sync


先理解这套系统在做什么

Cloudflare 的 Bot Management 系统需要对每一个 HTTP 请求打分,判断它是真实用户还是机器人流量。这个评分在所有检测机制中覆盖面最广,为超过72%的 HTTP 请求提供最终的 Bot Score 决策。

核心推理引擎是CatBoost,一个以低延迟推理著称的梯度提升框架。但推理本身只是延迟方程的一部分,另一部分是特征提取和准备

这两件事合在一起,才决定了每个请求实际感受到的延迟。

特征的类型也在演进:早期只用"单请求特征",比如某个 HTTP 头是否存在、值是什么。但这类特征太容易伪造,攻击者稍作修改就能绕过检测。后来引入了跨请求聚合特征,比如某个 IP 在过去一段时间内关联过多少个不同的 User-Agent,这类特征更难伪造,但也更难实时提取。


旧系统:Gagarin 的能与不能

负责提供跨请求聚合特征的系统叫Gagarin,用 Go 实现,本质上是一个特征服务平台。

工作流程大致如下:

  1. HTTP 请求到达边缘节点,从请求属性中提取维度键
  2. 先查多层 Lua 缓存,命中则直接用
  3. 缓存未命中时,通过 Unix Domain Socket 发 memcached 请求给 Gagarin
  4. Gagarin 返回对应的特征向量
  5. 特征向量送入 CatBoost 模型,产出 Bot Score

早期这套系统运转良好,p50 延迟约200 微秒。但随着特征数量增加、流量持续增长,缓存命中率开始下降,延迟随之恶化:

  • p50:500 微秒
  • p99 峰值:10 毫秒

团队对 Gagarin 做了大量的低层次调优,最终触碰到了一个无法通过调参解决的边界。


问题的根源:Unix Socket 不够快

Cloudflare 团队用第一性原理的方式重新审视了这个问题:操作系统提供的最高效进程间通信方式是什么?

他们用 ipc-bench 这个开源工具测量了 Linux 上各种 IPC 机制的延迟(条件:百万次 1024 字节消息的双向通信):

IPC 方式平均延迟 (μs)平均吞吐 (msg/s)
TCP socket8.74114,143
Unix Domain Socket5.61177,573
管道(Pipe)4.73210,369
消息队列4.40226,421
Unix 信号2.45404,844
共享内存0.5981,616,014
内存映射文件0.5031,908,613

结论一目了然:Unix socket 已经是相对高效的选项,但和共享内存、内存映射文件相比,还差了整整一个数量级。

旧系统的延迟问题,根源之一就在于必须经过 Unix socket 这条"慢路"。


评估了六种方案,最终选了内存映射文件

在确定方向之前,团队系统性地排除了其他选项:

继续优化 Gagarin:Go 的 GC 暂停、hashmap 查找性能、Unix socket 同步开销,这些是语言和架构层面的固有限制,调参无法根治。

迁移到 Quicksilver(Cloudflare 内部的分布式 KV):特征的更新频率太高,会对 Quicksilver 的其他用途产生负面影响,且底层仍然是 Unix socket。

扩大多层缓存:把几千万个维度键连同特征向量全部缓存在内存里,会导致每个 worker 线程各维护一份副本,内存消耗不可接受。

对 Unix socket 做分片:能部分缓解争用,但引入了额外复杂度,且治标不治本。

换用 RPC:RPC 仍然需要某种通信总线(TCP/UDP/UDS),性能不会有本质改变。

最终选定:内存映射文件(mmap)


三个关键技术决策

选定内存映射文件之后,还需要解决三个核心问题:如何在文件里高效查找特征?如何在高并发下安全更新?如何消除反序列化开销?

无等待同步(Wait-free)

普通的加锁(mutex/spinlock)在高并发下会引入争用,正是 Unix socket 方案的痛点之一。

Cloudflare 的设计受 Linux 内核RCU(Read-Copy-Update)模式Left-Right 并发控制技术启发,采用了无等待同步:

  • 维护两份数据副本,存储在两个独立的内存映射文件中
  • 单个 writer管理写入,写入时更新非活跃副本,完成后原子切换版本号
  • 多个 reader 可以并发读取活跃副本,无需任何锁
  • 第三个文件专门存储同步状态(当前活跃文件索引、数据大小、校验和、各副本活跃读者计数)

这套机制保证了读操作永远不会被阻塞,且在有限步骤内必然完成——这是比 lock-free 更强的保证。

零拷贝反序列化(rkyv)

从文件里读数据,通常需要把字节流反序列化成内存中的数据结构,这个过程有拷贝和计算的开销。

零拷贝反序列化的思路是:让序列化后的字节布局和内存布局完全一致,这样可以直接把文件内存映射后的字节当作 Rust 结构体来用,不需要任何拷贝或转换。

Cloudflare 选用了rkyv框架,它是少数几个能对HashMap做零拷贝访问的 Rust 序列化库之一。读取特征时,数据不会被复制,也不会有额外的解析计算,直接引用映射内存中的字节。


mmap-sync:把三个技术打包成一个 Rust crate

Cloudflare 把内存映射文件 + 无等待同步 + 零拷贝反序列化三者结合,封装成了一个 Rust crate,命名为mmap-sync,并开源发布。

核心是一个叫Synchronizer的结构体,接口极其简洁:

implSynchronizer{// 写入任意可序列化的 Rust 结构体pubfnwrite<T>(&mutself,entity:&T,grace_duration:Duration)->Result<(usize,bool),SynchronizerError>{...}// 零拷贝读取,返回带生命周期保护的引用pubfnread<T>(&mutself)->Result<ReadResult<T>,SynchronizerError>{...}}

读操作返回一个 RAII guard,持有期间自动增加活跃读者计数,出作用域后自动减少——writer 会等到所有读者离开之后才回收旧副本,整个过程对调用方完全透明。


BLISS:新系统的完整架构

基于 mmap-sync,Cloudflare 重新设计了整个 Bot 检测的基础设施,命名为BLISS(Bots Liquidation Intelligent Security System),由两个组件构成:

bliss service

用 Rust 实现的多线程 sidecar daemon,负责数据的写入侧

  • 周期性地从上游拉取最新的机器学习特征和维度数据
  • 解析后通过 mmap-sync 写入内存映射文件
  • 基于 Tokio 异步运行时,擅长大批量数据处理和 I/O 密集操作

bliss library

用 Rust 实现的单线程动态库,负责读取和推理侧

  • 通过 FFI 嵌入每个 worker 线程,用 Lua 模块调用
  • 从内存映射文件中零拷贝读取特征
  • 调用 CatBoost 模型完成推理,产出 Bot Score
  • 零堆分配:所有数据结构预先分配,运行时只用栈内存;用 dhat 堆分析器在集成测试中强制验证这一点
  • SIMD 优化:对某些请求属性的 hex 解码使用 AVX2/SSE4 指令集,速度提升 10 倍

编译配置也做了深度调优:

[profile.release] codegen-units = 1 # 全程序优化,不分片 lto = "fat" # 跨 crate 链接时优化 opt-level = 3 # 最高优化级别 debug = true # 保留调试符号,不影响性能

上线数据:延迟改善了多少个数量级

延迟指标改造前 (μs)改造后 (μs)变化
p505329降低 98.3%,快 59 倍
p999,51018降低 99.8%,快 528 倍
p99916,00029降低 99.8%,快 551 倍

在整体 HTTP 请求处理层面:

  • 整体平均处理延迟降低12.5%
  • Bot Management 模块延迟降低55.93%

团队用了一个很直观的换算来说明这个改善的量级:在 Cloudflare 每秒 4600 万请求的规模下,每个请求节省 523 微秒,等价于每天节省超过 65 年的处理时间

除延迟之外,其他指标同样改善显著:

  • 特征可用性从有损达到 100%:消除了 Unix socket 超时,误报和漏报率下降
  • 释放了等价于数千个 CPU 核心和数百 GB 内存的资源,提升了整体服务器利用率
  • 清除了数千行 Lua 和 Go 代码,降低了技术债务
  • 机器学习能力大幅扩展:可以承载数百个特征、数十个维度和多个并行模型

这套方案的本质

这次重构表面上是一次延迟优化,但背后的工程逻辑值得单独提炼。

第一,从性能测量开始,而不是从直觉开始。用 ipc-bench 系统性地测量各种 IPC 方式的延迟,数据驱动选型,不靠猜测。

第二,找到真正的瓶颈,而不是在边际上优化。Gagarin 经过大量调优之后,p99 依然是 10ms。因为问题不在代码质量,在于 Unix socket 这条通信路径本身的物理限制。换掉通信路径,才能突破上限。

第三,把解决方案抽象成可复用的基础设施。mmap-sync 不只是 BLISS 的内部实现细节,而是一个可以独立使用的 Rust crate,开源之后整个社区都可以用同一套机制解决类似问题。

在 Cloudflare 这个规模上,每一微秒的节省都在被放大,但解决问题的思路——测量、找根因、换架构、抽象复用——在任何规模的系统里都适用。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询