
大家好,我是Tony Bai。
2025 年 11 月 18 日,世界标准时间(UTC) 11:20,支撑着全球大量互联网流量的 Cloudflare 网络开始出现严重故障。无数网站和应用的用户,开始频繁地看到那令人心悸的“Internal Server Error (500)”页面。一场席卷全球的互联网宕机事件,就此拉开序幕。
事后,Cloudflare 发布了一份极其详尽、坦诚的故障复盘报告。报告揭示了一个令人震惊、也极具讽刺意味的事实:这场灾难的最终扳机,竟然是新一代代理引擎FL2 中(这里仅针对文中提及的新引擎FL2,受影响的旧引擎FL文中并未提及具体原因),一段本应代表“内存安全”的 Rust 代码中的 unwrap() 调用。
这起事件,如同一颗投入平静湖面的巨石,激起了关于 Rust 安全模型、系统复杂性、以及“快速失败”哲学的层层涟漪。它迫使我们重新审视一个根本性问题:我们所追求的“内存安全”,真的能让我们高枕无忧吗?
故障的多米诺骨牌:从一个权限变更开始
Cloudflare 的报告清晰地描绘了一条如多米诺骨牌般精准倒下的故障链。令人惊叹的是,这一切的源头,并非黑客攻击,也不是硬件故障,而是一次看似无害的内部变更:
源头:ClickHouse 数据库权限变更 (11:05 UTC) 为了提升查询安全性和可靠性,Cloudflare 的工程师对 ClickHouse 数据库集群进行了一次权限变更。
第一个意外:重复的元数据 这次变更意外地导致了一个用于生成“特征文件”(feature file) 的元数据查询(
SELECT name, type FROM system.columns WHERE table = ...)开始返回重复的列名。因为该查询忘记了按数据库名进行过滤,而新的权限让它看到了底层r0数据库中的重复表结构。第二个意外:配置文件体积翻倍 这个“特征文件”是 Cloudflare 机器人管理 (Bot Management) 系统机器学习模型的核心输入。由于元数据查询返回了双倍的行数,最终生成的特征文件体积也翻了一倍,从约 60 个特征,激增到了超过 200 个。
第三个意外:触发预分配内存上限 为了极致的性能,Cloudflare 的核心代理服务(包括基于 Rust 的新一代引擎 FL2)会在启动时,为机器人管理模块预分配一块固定大小的内存,用于加载这个特征文件。这个预分配的上限被设置为 200 个特征。
最终扳机:Rust 代码中的
unwrap()恐慌 (Panic) 当那个体积翻倍的、包含超过 200 个特征的“毒丸”配置文件,被分发到全球的 FL2 服务器上时,灾难发生了。负责加载特征的 Rust 代码,在尝试将超过 200 个特征塞入预分配的 200 大小的缓冲区时,append_with_names方法返回了一个Err结果。然而,调用这段代码的地方,却简单粗暴地使用了unwrap()。// Cloudflare 报告中展示的 Rust 代码片段 let (feature_values, _) = features .append_with_names(&self.config.feature_names) .unwrap(); // <- BOOM!unwrap()的行为是:如果结果是Ok(value),则返回value;如果结果是Err(error),则立即让当前线程panic(恐慌)。雪崩:5xx 错误与全球宕机 工作线程的
panic,导致了一个未处理的错误。这个错误迅速向上传播,最终导致核心代理系统无法处理依赖于机器人管理模块的流量,并开始向上游返回大量的 HTTP 5xx 错误。多米诺骨牌全部倒下,全球大范围的互联网服务因此中断。
Rust 安全模型的反思:“内存安全”≠“永不崩溃”
这起事件,是对 Rust 安全模型的一次深刻、也是痛苦的“压力测试”。Rust 最引以为傲的“卖点”——内存安全——在这场灾难中,既是“英雄”,也是“恶棍”。
英雄之处:它精确地阻止了更坏的情况
Rust 在这里所做的一切,完全符合其设计哲学。append_with_names 方法正确地检测到了缓冲区溢出的风险,并通过返回一个 Err,阻止了一次潜在的内存损坏。如果这段代码是用 C++ 编写的,一个类似的错误可能会导致缓冲区溢出、数据损坏、甚至远程代码执行等更严重、更难以追踪的安全漏洞。
Rust 成功地将一个未定义的、危险的内存行为,转化为了一个已定义的、可预测的程序崩溃。
恶棍之处:“快速失败”的哲学真的普适吗?
然而,问题恰恰出在 unwrap() 这个“捷径”上。unwrap() 和它的兄弟 expect(),是 Rust “快速失败”(Fail-fast) 哲学的体现。它们背后的假设是:“我相信这种情况永远不会发生,如果发生了,那就是一个程序员无法恢复的、灾难性的逻辑错误,整个程序应该立刻死掉,而不是带着错误的状态继续运行。”
Cloudflare 的工程师们,显然也相信“特征文件永远不会超过 200 个”。
这次事件血淋淋地告诉我们:
在分布式系统中,你所做的“永不发生”的假设,几乎总会在某个时刻、以一种你意想不到的方式被打破。
unwrap()是一把极其锋利的双刃剑。它在原型开发、测试代码、或处理那些真正代表“程序不变量被破坏”的场景时非常有用。但将其用于处理任何可能由外部输入(即使是内部系统的“外部输入”)而失败的操作,都是在埋下一颗定时炸弹。Rust 的内存安全,并不能替代全面的错误处理和系统韧性设计。 它只能保证你的程序“死得干净”,而不能保证它“不死”。
更深层次的教训:超越语言的“系统性失败”
将锅完全甩给 Rust 或 unwrap() 是不公平的。这场宕机,是一次典型的、由多个层面小失误共同导致的系统性失败 。
数据库查询的脆弱性:那个元数据查询,为何如此脆弱,以至于一次权限变更就能使其输出加倍?它缺乏对数据库名的过滤,这是一个早已存在的隐患。
配置发布的“零校验”:一个体积异常的配置文件,为何能在没有任何校验和告警的情况下,被迅速分发到全球网络?配置发布管道缺乏基本的“理智检查”。
边界条件的“想当然”:为什么预分配的内存上限是 200?这个“魔法数字”背后的假设是什么?当假设被打破时,为什么没有一个优雅的降级方案(如拒绝加载新配置,继续使用旧配置),而是直接崩溃?
故障域的耦合:机器人管理模块的一次“错误”的特征文件生成,为何能导致核心代理的瘫痪,并进一步影响到 Workers KV 和 Access 等看似不相关的服务?这暴露了系统各组件之间过紧的故障耦合。
小结:废墟之上,我们学到了什么?
Cloudflare 的这次全球宕机,为整个软件行业都上了一堂极其昂贵的公开课。对于 Rust 社区而言,它提醒我们,Result<T, E> 和完善的 match 模式,才是处理可恢复错误的王道,而 unwrap() 应该像 unsafe 关键字一样,被审慎地、有意识地使用。
但更重要的是,它告诉我们,没有任何一门语言,无论其内存安全模型多么先进,能够将我们从系统性思考的责任中解救出来。构建可靠的、有韧性的分布式系统,是一项超越任何特定语言的、需要防御性编程、纵深防御、以及对“墨菲定律”抱有永恒敬畏的综合性工程挑战。
Cloudflare 在废墟之上,承诺将“加固配置文件的摄入”、“增加全局熔断开关”、“消除核心转储压垮资源的可能性”。这些,才是比争论“unwrap() 是否邪恶”更有价值的、真正能让我们从这次灾难中变得更强大的教训。
Cloudflare的故障复盘报告:https://blog.cloudflare.com/18-november-2025-outage/
如果本文对你有所帮助,请帮忙点赞、推荐和转发
!
点击下面标题,阅读更多干货!
- Rust 布道者Jon Gjengset深度访谈:在 AI 时代,我们该如何思考编程、职业与未来?
- SQLite 对 Go 和 Rust 说“不”:揭示“安全语言”光环下的工程现实
- Azure CTO深度解读:微软为何要用Rust“替换”C/C++,又将如何用AI加速代码迁移?
- 哲学家与工程师:为何Rust和Go的“官方之声”如此不同?
- Rust 2025深度解读:在十周年里程碑上,Niko Matsakis如何擘画下一个时代的灵魂与蓝图?
- Go vs. Rust再掀波澜:Grab真实案例复盘,Gopher如何看待这场“效率与代价”之争?
🔥 你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?
想写出更地道、更健壮的Go代码,却总在细节上踩坑?
渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
想打造生产级的Go服务,却在工程化实践中屡屡受挫?
继《Go语言第一课》后,我的 《Go语言进阶课》 终于在极客时间与大家见面了!
我的全新极客时间专栏 《Tony Bai·Go语言进阶课》 就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。
目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!

一次unwrap引发的全球宕机
523

被折叠的 条评论
为什么被折叠?



