Cloudflare Nov. 18th 严重全球故障

官方事故报告:https://blog.cloudflare.com/zh-cn/18-november-2025-outage/
原文采用 UTC 世界协调时,本文采用北京时间

2025 年 11 月 18 日 19:20,Cloudflare 网络开始出现严重故障,无法正常传输核心网络流量。尝试访问会看到一个错误页面,提示 Cloudflare 网络出现故障。

Cloudflare 故障图

22:30 时,核心流量基本恢复正常。在接下来的几小时内,随着流量逐步恢复在线,工程师努力减轻网络各部分增加的负载。截至 01:06 ,Cloudflare 所有系统均已恢复正常运行。

0. 事件概述

数据库权限变更

Cloudflare 的工程师在 19:05 对 ClickHouse 数据库集群进行了权限更新。目的是为了改进安全性,让分布式查询在初始用户账号下运行,而不是共享系统账号。这个变更产生了一个副作用:用户现在可以显式地看到底层表(r0 数据库)的元数据,而不仅仅是默认表(default 数据库)。

Bot Management 系统生成了错误文件

Cloudflare 的“Bot Management”(机器人管理)模块会每几分钟运行一个 SQL 查询来生成“特征文件”,用于机器学习模型判断流量是否为机器人。该查询原本假设只返回 default 数据库的列。但在权限变更后,查询结果包含了 default 和 r0 的数据,导致返回的列重复了。结果,生成的“特征文件”大小翻倍,包含的特征数量超过了预期。

Rust 代码的 unwrap() Panic

这个错误的特征文件被分发到了 Cloudflare 全球网络的服务器上。负责路由流量的核心代理软件(FL2,使用 Rust 编写)读取该文件。代码中设置了一个硬性限制:特征数量不能超过 200(为了内存预分配的性能优化)。由于文件大小翻倍,特征数超过了这个限制。关键错误点:代码在处理这个超限错误时,使用了 Result::unwrap()。当遇到错误(Err)时,unwrap() 会直接导致线程 Panic(崩溃),而不是优雅地处理错误。报错信息:thread fl2_worker_thread panicked: called Result::unwrap() on an Err value。这导致代理服务不断重启、崩溃,无法处理流量。

问题找到

1. 分析原因

Cloudflare 处理请求机制

Cloudflare 处理流程
当请求经过核心代理时,Cloudflare 会运行网络中可用的各种安全和性能产品。其中一个模块,也就是“机器人管理”,是导致服务中断的根本原因。

机器人管理模块

Cloudflare 机器人管理模块中的机器学习模型将用于预测请求是否为自动化请求的特征配置文件作为输入。此特征文件每隔几分钟会更新一次,并发布到整个 Cloudflare 网络。

ClickHouse 集群

一个 ClickHouse 集群由多个分片组成。为了查询所有分片中的数据,Cloudflare 在名为 default 的数据库中使用了所谓的分布式表(由表引擎 Distributed 提供支持)。Distributed 引擎会查询数据库 r0 中的基础表。基础表用于存储 ClickHouse 集群中每个分片上的数据。

分布式表的查询通过共享系统账户运行。为了提高分布式查询的安全性和可靠性,我们努力让查询以初始用户账户身份运行。

当 ClickHouse 用户从 ClickHouse 系统表中查询表元数据(例如 system.tables 或 system.columns 时,只能看到 default 数据库中的表

工程师的失误

由于用户已经拥有对 r0 中基础表的隐式访问权限,工程师在 19:05 进行了一项更改,使这种访问权限显化,以便用户也可以查看这些表的元数据。通过确保所有分布式子查询都能够以初始用户身份运行,Cloudflare 可以更精细化地评估查询限制和访问权限授权,从而避免因某个用户的错误子查询而影响其他用户。

上述更改使所有用户都能够访问其有权访问的表中的准确元数据。但是,过去工程师做出了一些假设,认为此类查询返回的列的列表仅包含 default 数据库:

1
2
3
4
5
6
7
SELECT
name,
type
FROM system.columns
WHERE
table = 'http_requests_features'
order by name;

注意,该查询并未按数据库名称进行筛选。随着 Cloudflare 逐步向特定 ClickHouse 集群的用户授予显式权限,在 19:05 进行更改之后,上述查询开始返回“重复”列,因为这些列对应存储在 r0 数据库中的基础表。

上述查询会返回一个包含列的表,类似于下表(简化版示例):
查询表示例图

然后,由于用户获得了额外的权限,响应现在包含 r0 架构的所有元数据,导致响应中的行数增加了一倍以上,最后影响最终文件输出中的行数(即:特征数量)。

Rust 的歇菜

Cloudflare 代理服务上运行的每个模块都设置了一些限制,以避免无限制的内存消耗,并通过预分配内存来优化性能。在本实例中,机器人管理系统限制了运行时可用的机器学习特征数量。当前,该限制设置为 200,远高于目前使用的大约 60 个特征。同样,设置此限制是出于性能考虑,Cloudflare 为这些特征预分配了内存。

当包含 200 多个特征的错误文件传输到 Cloudflare 服务器时,达到了此项限制,导致系统崩溃。以下显示了执行检查并导致未处理错误的 FL2 Rust 代码:

FL2 Rust 代码图

这导致了以下线程崩溃,进而引发了 5xx 错误:

1
thread fl2_worker_thread panicked: called Result::unwrap() on an Err value

2. 影响范围

官方数据

服务/产品 影响描述
核心 CDN 与安全服务 HTTP 5xx 状态代码。本文顶部的屏幕截图显示了最终用户看到的典型错误页面。
Turnstile Turnstile 无法加载。
Workers KV 由于核心代理出现故障,导致对 KV”前端”网关发出的请求失败,进而导致 Workers KV 返回的 HTTP 5xx 错误数量显著增加。
仪表板 虽然仪表板基本维持正常运行,但由于登录页面中的 Turnstile 不可用,导致大多数用户无法登录。
电子邮件安全 虽然电子邮件处理和递送未受影响,但我们观察到对某个 IP 信誉源的访问暂时中断,这降低了垃圾邮件检测的准确性,阻止了某些新域期限检测的触发,不过未对客户造成严重影响。此外,我们还发现了某些自动移动操作出现故障;所有受影响的邮件均已审核并修复。
Access 从事件发生之初一直持续到 13:05 启动回滚为止,大多数用户普遍遭遇了身份验证失败。所有现有 Access 会话均未受到影响。所有身份验证失败的尝试均导致显示错误页面,也就是说,在身份验证失败的情况下,没有任何用户能够访问目标应用。在此期间,所有成功的登录均已正确记录。当时尝试的任何 Access 配置更新,要么直接失败,要么传播速度非常缓慢。现在,所有配置更新均已恢复。

用户反馈

据全球用户反馈,打不开的大型网站如下:
打不开的大型网站

3. 启示

未完待续…

一句话总结:你怎么能在生产环境里直接 unwrap() 啊?!Rust 不是这样用的!你应该先认真设计一个靠谱的错误类型,用 thiserror 或 anyhow 包装好上下文信息,然后在每一层调用链里用 ? 把错误优雅地向上传递。遇到可能出现网络抖动、I/O 超时、序列化失败这种情况,你要先写好健壮的重试逻辑、退避策略和熔断机制,并且在日志里带上 trace id,这样 SRE 才能在凌晨三点定位问题。然后你要写单元测试,把所有可能失败的路径都测一遍;集成测试里还要模拟网络异常和依赖服务挂掉的情况,确保你的代码不会一言不合就 panic。接着你要跑一下 clippy,把所有 “consider handling the Result instead of unwrapping” 的警告都修干净;还要跑 rustfmt,让代码风格保持一致。之后你才可以 commit 然后 push。你 push 上去之后,CI 会跑 cargo test、cargo check、cargo clippy、cargo fmt -check,还有压力测试确保你的服务在压力下不会因为一个 unwrap() 就直接把整个服务集群带走。等 PR 至少经过两位 reviewer、三个 LGTM,并且 SRE 点头同意这个改动不会再次导致全球范围的 5xx 风暴之后,我才会考虑把你的分支 merge 进去。你怎么上来就直接在关键路径 unwrap()?!Rust 根本不是这样写的!我拒绝合并!