一次数据库权限小改动,如何拖垮半个互联网?——Cloudflare 2025-11-18 大故障复盘

2025 年 11 月 18 日,Cloudflare 发生了 2019 年以来最严重的一次全球网络故障。大批依赖 Cloudflare 的网站和服务(包括 ChatGPT、X、游戏、各类 SaaS)在数小时内持续返回 5xx 错误,用户看到的就是 "Cloudflare 网络内部出错" 的报错页。

事后 Cloudflare 发布了详细的事故报告:这 不是黑客攻击 ,而是一连串由 ClickHouse 数据库权限变更 引发的连锁反应,最终导致核心代理程序崩溃。

下文我们按这样几个问题来讲:

  1. 这次事故表层上发生了什么?
  2. 技术上到底是怎样的一条 "多米诺骨牌链"?
  3. ClickHouse 权限变更 到底改了啥,为什么会出事?
  4. 我们能从中学到哪些通用的工程经验?

一、事故概览:从错误页到核心代理崩溃

1. 外在表现

  • 时间:

    • 11:20 UTC 左右:Cloudflare 网络开始大规模返回 5xx;
    • 14:30 左右:核心流量基本恢复;
    • 17:06:Cloudflare 宣布所有系统恢复正常。
  • 用户能感知到的症状:

    • 访问大量网站时,页面直接显示 Cloudflare 的错误页;
    • 许多登录、验证码、人机校验界面完全无法加载(Turnstile 挂了);
    • 一部分使用 Cloudflare Access / Zero Trust 的内部系统登录失效。

2. 高层技术原因(一句话版)

一次 ClickHouse 权限变更 → 查询 system.columns 行为变化 → 生成的 Bot Management 特征配置文件 "翻倍膨胀" → 超过代理模块设定的特征上限 → 新代理 FL2 里一段 Rust 代码 unwrap() 触发 panic → 核心代理程序崩溃,向全网返回 5xx。

注意:数据库本身没 "坏" ,ClickHouse 按文档规定的行为正确工作;问题在于上层特征生成逻辑对它做了错误假设。


二、ClickHouse 是什么?(给没用过的人一个 3 分钟版)

要理解事故里那条坑人的 SQL,我们先快速认识一下 ClickHouse。

你可以把 ClickHouse 理解成一个:

高性能的列式分析数据库,专门用来存储和查询海量日志、监控、埋点数据。

这里我们只需要关心它的三点特性:

1. 多个 database & 多种表类型

和 MySQL 一样,ClickHouse 有多个 database(库),每个库里有很多表。

但它还引入了 分布式表(Distributed) 的概念,用来统一查询分片数据。

在 Cloudflare 的架构里(根据官方描述):

  • default 数据库:放的是 Distributed 表 ,比如 default.http_requests_features,对用户来说是逻辑上的 "整张表";
  • r0 数据库:放的是实际分片上的 本地物理表 ,比如 r0.http_requests_features

也就是说:两边都有同名表,一个是聚合视图,一个是真实存储。

2. system.* 系统表:类似 information_schema

ClickHouse 有一套 system.* 表用来提供元数据,比如:

  • system.tables:有哪些表;
  • system.columns:所有表里有哪些列。

system.columns 的典型用法类似:

sql 复制代码
SELECT database, table, name, type
FROM system.columns
WHERE table = 'http_requests_features';

这有点像 MySQL 的 information_schema.columns

关键点: system.columns 只会显示 当前用户有权限看到的表的列信息 。如果你没权限访问某个库 / 表,那它在 system.columns 中就 "透明消失",根本不会出现在结果里。

3. RBAC 权限模型

ClickHouse 支持角色 / 权限(RBAC):

  • 可以针对:库、表、列、甚至行做 SELECT/INSERT 等权限控制;

  • 典型授权语法:

    vbnet 复制代码
    GRANT SELECT ON default.* TO some_role;
    GRANT SELECT ON r0.http_requests_features TO some_role;

也就是说,你给了什么权限,就会影响 system. 视图中用户能看到什么*。


三、那 Cloudflare 这次到底改了什么权限?

Cloudflare 在报告里描述,他们想做一件 "看起来很合理" 的事:

让运行分布式查询的不是单一的 "系统账号",而是发起请求的那个真实用户,这样限流、配额、权限控制都更细粒度。

为此,他们在 ClickHouse 上做了一个变更(简化理解,大致是这样):

之前:

vbnet 复制代码
GRANT SELECT ON default.* TO role_for_analytics;
-- 用户只看得到 default 库的表和列

之后:

vbnet 复制代码
GRANT SELECT ON default.* TO role_for_analytics;
GRANT SELECT ON r0.*       TO role_for_analytics;
-- 用户现在也能看到 r0 库里的底层物理表

这个操作导致的效果是:

同一个用户在查询 system.columns 时,原本只会看到 default 库的列,现在会同时看到 default + r0 两套数据

这本身是 ClickHouse 完全正常、符合文档的行为。

坑在上层: Cloudflare 的某段业务代码默认认为 "只会有 default 那一份"。


四、那条 "坑死人不偿命" 的 SQL:特征文件如何被 "翻倍膨胀"

Cloudflare 的 Bot Management 需要为自己的机器学习模型生成一个 "特征配置文件(feature file)" ,里面列出各种特征名称、类型等。这个配置文件下发给全网的代理,用来给每个请求打 bot score。

在生成这个文件时,有这样一条 SQL(官方报告里给出来的):

sql 复制代码
SELECT name, type
FROM system.columns
WHERE table = 'http_requests_features'
ORDER BY name;

我们注意三件事:

  1. 它只按 table 过滤,没有管 database
  2. 之前用户只看得到 default.http_requests_features
  3. 结果被直接用来拼装 "特征列表"。

1. 变更前:一切正常

权限变更之前,system.columns 里对这个查询返回的,大概是这样的(示意):

database table name type
default http_requests_features feature_a UInt8
default http_requests_features feature_b Float32
... ... ... ...

特征生成程序拿到这些行,生成一个有 N 个特征的文件,比如 60 个,完全在代理模块设定的 "最多 200 个特征" 上限之内。

2. 变更后:同名表 + 权限 = 数据翻倍

一旦给了 r0.* 的权限,这条 SQL 的结果就变成了:

database table name type
default http_requests_features feature_a UInt8
default http_requests_features feature_b Float32
... ... ... ...
r0 http_requests_features feature_a UInt8
r0 http_requests_features feature_b Float32
... ... ... ...

可以看到:同样的列在 defaultr0 下各出现了一遍

由于上层代码完全没看 database 字段,只拿 nametype 就用,等于是:

原有 N 条特征记录 → 现在变成了约 2N 条。

于是,Bot Management 生成的特征配置文件大小直接 翻倍膨胀

3. 代理模块中的上限 + unwrap() panic

Cloudflare 的代理模块(尤其是新一代 FL2)里,对特征数量设了一个硬上限,例如 200(报告里提到此前通常只用到 ~60 个)。这个模块大致做了两件事:

  1. 按上限预分配存储特征的内存;
  2. 在加载配置时,如果发现特征条数超出了预期,就会走一条错误路径。

问题就出在这条错误路径上:

  • 在 FL2 的 Rust 代码中,有地方对一个 Result 调用了 unwrap()
  • 在正常情况下这是 Ok(...),不会出事;
  • 但当特征条数超出上限时,它变成了 Err(...)unwrap() 直接 panic
  • panic 导致整个代理进程崩溃,无法处理请求,于是向外统一返回 5xx。

而这恰好发生在 Cloudflare 最核心的流量路径上,于是就出现了我们看到的 "半个互联网都挂了" 的现象。


五、这一链路里真正的几个 "坑点"

如果从架构和工程角度拆解,这条链路里至少有五个关键坑:

1. 把 "内部生成的配置" 当成了完全可信

特征文件是 Cloudflare 自己内部管道生成的,不是用户直接上传的。工程师通常会下意识觉得:

"这是我们自己系统生成的东西,应该是靠谱的。"

于是:

  • 没有对特征数量做严格校验或硬性截断;
  • 没有做好 "文件异常时的降级策略"(比如忽略多余特征 / 回退上一版本 / 关闭 Bot 模块)。

但现实是:只要链路稍微复杂一点,"内部配置" 就需要像不可信输入一样对待

2. SQL 查询对系统表行为做了隐式假设

那条 SQL 的隐含假设是:

"我查 system.columns 过滤某个 table 名,就只会返回 default 库那一份。"

而 ClickHouse 的行为定义是:

只要你对某个表有权限,它的列就会出现在 system.columns 中,哪怕有多个库里有同名表。

这其实是两套模型的思维冲突:

  • 业务侧:默认 "一个逻辑表名 → 一份元数据";
  • DB 侧:允许 "多个 database + 多个同名表全都合法存在"。

3. 权限(RBAC)变更没有反推依赖系统

ClickHouse 的 RBAC 是全局影响的:

  • 一旦你对 r0.* 授予 SELECT,用户就理所当然能看到 r0 下的表和 system.* 中相应条目;

  • 但这次权限变更的决策路径,很可能只关注了 "子查询用实际用户身份执行" 的需求,没有系统性地梳理:

    • 哪些上层流程依赖 system.columns
    • 这些流程是否对 database 做了区分?
    • 是否假设 "一张表名只有一份元数据"?

4. 核心路径里用了 "会崩整个进程" 的错误处理方式

在高可用系统里,一般会尽量避免在核心路径中:

  • 直接 panic
  • 在错误分支调用 unwrap()expect() 之类会终止进程的方法。

更理想的做法包括:

  • 对 "非核心功能" 模块的错误要能 fail-open / fail-closed 可配置
  • 更上层有一个 "熔断" 点,能把某些模块彻底摘除,而不是让它们把整个代理拉跨。

Cloudflare 在报告中也提到,今后会更严格地审查代理中各组件的 failure mode,并增加更多 "全局 kill switch"。

5. 缺少一眼就能看出 "feature 数量异常" 的监控

考虑到 Bot Management 是一个高度依赖配置的模块:

  • 如果在 "每次发布新特征文件" 时就有监控:

    • 统计特征数;
    • 与前一版本做对比(例如超过 2 倍就报警);
  • 那么这次问题可能在影响全网之前就被挡在实验 / 灰度阶段。


六、给不了解 ClickHouse 的读者的一点 "总结版教训"

你完全可以把这次事故当成一个通用的 "系统设计反例" ,而不是仅仅归咎于某个数据库或某行 Rust 代码:

  1. 所有配置都要当 "不可信输入" 处理

    • 不只是用户表单;
    • 包括内部管道生成的 JSON、特征文件、元数据快照......一旦链路足够长,它们随时可能和预期不一致。
  2. system. / information_schema 这类元数据查询要写得 "保守" *

    • 明确加上 database = 'xxx'
    • 或者在应用层做去重与限制;
    • 把 "只会有一份" 的假设写成显式条件,而不是隐含在 "现在刚好没有同名表" 的巧合里。
  3. 权限和元数据视图高度耦合时,变更要反推到所有依赖方

    • 给某个库加了 SELECT 权限,看似是 "给别人多了一点数据访问能力";
    • 实际上改变的是:系统表 / 视图 / 报表 / 配置生成 等一整条链路的输入空间。
  4. 在核心组件里,错误优先 degrade 而不是 crash

    • 例如:Bot 特征文件加载失败时,可以先把该模块暂时视为 "关闭",让流量以更弱的防护通过;
    • 真要 panic,也尽量把 panic 控制在某个 "安全边界" 之内,而不是当场把整个代理干掉。
  5. 监控不仅要看 "请求量、错误率",也要看 "配置和元数据的健康度"

    • 特征数、配置尺寸、字段数量、schema 变化,这些也是异常的重要信号。

七、结语

从 "改了一个数据库权限" 到 "全球大量网站 5xx",中间隔着的是:

  • 元数据行为的细微改变;
  • 业务代码对系统表的隐式假设;
  • 不够防御性的错误处理;
  • 对内部配置缺乏 "像不可信输入一样" 的敬畏。

如果你在做日志平台、风控系统、埋点分析,或者任何依赖 "配置 + 大规模分布式服务" 的架构,这次 Cloudflare 的事故值得你认真读一遍原始报告,然后对照自己的系统问一句:

"如果是我这套系统,只改一个权限或 schema,会不会也有类似的连锁反应?"

相关推荐
一 乐1 小时前
宠物猫店管理|宠物店管理|基于Java+vue的宠物猫店管理管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·后端·宠物管理
r***99821 小时前
在2023idea中如何创建SpringBoot
java·spring boot·后端
w***37512 小时前
【SQL技术】不同数据库引擎 SQL 优化方案剖析
数据库·sql
一 乐2 小时前
考公|考务考试|基于SprinBoot+vue的考公在线考试系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot·课程设计
--fancy2 小时前
如何使用Tushare构建自己的本地量化投研数据库
数据库·sql·数据分析
q***72192 小时前
Y20030018基于Java+Springboot+mysql+jsp+layui的家政服务系统的设计与实现 源代码 文档
android·前端·后端
i***39582 小时前
ShardingSphere-jdbc 5.5.0 + spring boot 基础配置 - 实战篇
java·spring boot·后端
AY呀2 小时前
DeepSeek:探索AI大模型与开发工具的全景指南
后端·机器学习
凡客丶2 小时前
SpringBoot整合Sentinel【详解】
spring boot·后端·sentinel