聊聊Cloudflare:一个数据库权限变更,怎么搞瘫半个互联网?

Lee 1 次阅读 发布于 21 天前 最后更新于 21 天前 2780 字 预计阅读时间: 13 分钟


2025 年 11 月 18 日,Cloudflare 突然爆炸。

冷静下来刷到 Cloudflare 的官方复盘,才发现这事儿的来龙去脉特别有 “教学意义”:既不是复杂的黑客攻击,也不是硬件故障,而是一个 “想优化权限,结果玩脱了” 的技术小疏忽,叠加了几个系统设计的 “隐形坑”。

先说说故障发生时的 “现场”:用户和我们都看到了啥?

那天故障是从 UTC 时间 11:20 开始的(对应国内是晚上 7 点多,正是用户访问高峰),最直观的现象就是:凡是用了 Cloudflare CDN 的网站,打开后全是统一的 “Internal Server Error” 页面, error code 500—— 熟悉 HTTP 状态码的朋友都知道,5xx 代表 “服务器端出错”,而且这次是 Cloudflare 自己的网络出了问题,不是客户源站的锅。

我当时第一反应是 “不会是 DDoS 吧?” 毕竟 Cloudflare 天天抗大流量攻击,之前也有过类似案例。但很快发现不对劲:一是错误很统一,不像 DDoS 攻击时那种时好时坏的波动;二是 Cloudflare 自己的状态页居然也打不开了,报的是 “504 Gateway Timeout”(后来才知道这是巧合,状态页托管在 CloudFront 上,刚好那段时间也有点小问题)。

更诡异的是,故障不是 “一坏到底”:有时候刷新几次页面能打开,过几分钟又不行了。这种 “反复横跳” 的现象,一开始把 Cloudflare 的工程师也带偏了 —— 他们最初怀疑是 “超大规模 DDoS 攻击”,甚至在内部聊天里说 “担心是大 botnet 在示威”。直到后来才发现,这根本不是攻击,而是 “配置文件在搞鬼”。

核心故障原因:一个 “权限优化” 引发的连锁反应

等官方复盘出来,才明白这是一个典型的 “小改动引发大故障” 的案例,整个逻辑链其实不复杂,但每一步都踩在了 “没考虑到的细节” 上,咱们一步步拆:

第一步:ClickHouse 数据库的 “权限变更”—— 想做安全优化,结果漏了过滤条件

Cloudflare 的 Bot Management(机器人管理)系统,依赖一个 “特征配置文件”—— 简单说,这个文件里存了几百个 “特征”(比如请求的 IP 段、浏览器指纹、访问频率等),机器学习模型靠这些特征判断 “这个请求是真人还是机器人”。

这个特征文件不是固定的,每隔 5 分钟会自动更新一次:由一个运行在 ClickHouse 数据库集群上的查询生成,然后同步到 Cloudflare 全球所有节点。

ClickHouse 是个分布式数据库,Cloudflare 的集群架构是这样的:

  • 数据存在多个 “分片(shard)” 上,每个分片的实际数据存放在 “r0” 数据库里;
  • 平时查数据时,工程师用的是 “default” 数据库里的 “Distributed 表”(一种特殊表引擎,会自动去各个分片的 r0 数据库捞数据);
  • 之前为了简化权限管理,普通用户只能看到 “default” 数据库的表元数据(比如表结构、列名),看不到 “r0” 数据库的内容。

那天 UTC 11:05,工程师做了个 “权限优化”:为了实现更细粒度的权限控制(比如限制某个用户只能查特定分片的数据),把 “r0” 数据库的表元数据也 “显式暴露” 给用户了 —— 意思是现在查 “system.columns”(ClickHouse 的系统表,存所有表的列信息)时,会同时返回 default 和 r0 两个数据库里的列。

本来这是个正常的优化,但问题出在:生成 “特征配置文件” 的查询语句,没加 “数据库名过滤条件”

原来的查询是这样的(简化版):

SELECT name, type 
FROM system.columns 
WHERE table = 'http_requests_features' 
order by name;

它只过滤了 “表名是 http_requests_features”,没过滤 “数据库名”。权限变更后,这个查询会同时返回 “default.http_requests_features” 和 “r0.http_requests_features” 的列 —— 相当于原本 60 个特征,现在变成了 120 个,特征文件直接翻倍

第二步:特征文件膨胀,触发了内存预分配的 “死亡上限”

Cloudflare 的核心代理系统(新的叫 FL2,旧的叫 FL),在加载这个特征文件时,有个 “内存预分配限制”:为了性能优化,工程师提前设定了 “最多加载 200 个特征”(平时只用 60 个,留了很大冗余)。

但这次特征文件翻倍后,特征数超过了 200——FL2 代理的代码里有个 “unwrap ()” 调用(Rust 语言里的用法,用来处理 “可能出错的结果”),当发现特征数超限时,这个调用直接 “panic”(崩溃)了,导致整个代理服务无法处理请求,直接返回 500 错误。

这里又有个细节:旧的 FL 代理没报 500,但出了另一个问题 —— 它不会 panic,但会跳过超量的特征,结果所有请求的 “机器人分数” 都变成了 0。这就导致:如果客户配置了 “拦截低分数机器人” 的规则,会把所有真人请求都拦下来(误判);没开这个规则的客户,反而没感觉。

第三步:5 分钟一次的 “配置同步”,让故障反复横跳

前面提到故障时 “时好时坏”,原因也在这:ClickHouse 集群的权限变更不是 “一次性全更完”,而是逐步推到各个分片的。

所以每隔 5 分钟生成特征文件时,会出现两种情况:

  • 如果查询跑到 “还没改权限的分片” 上,生成的是正常文件(60 个特征),系统就恢复;
  • 如果查询跑到 “改了权限的分片” 上,生成的是翻倍文件(120 个特征),系统就崩溃。

直到后来所有分片都完成了权限变更,生成的全是坏文件,系统才稳定在 “崩溃状态”—— 这也解释了为什么一开始故障反复,后来彻底挂了。

受影响的服务:不止 CDN,连登录和邮件安全都躺枪

Cloudflare 的核心代理一挂,依赖它的服务全跟着出问题。我整理了几个关键的 “系统依赖链的脆弱性”:

服务 / 产品受影响的表现
核心 CDN + 安全服务直接返回 500 错误,用户打不开网站;同时延迟飙升(因为调试系统占了太多 CPU)
Turnstile人机验证组件加载失败 —— 很多网站登录页用了这个,导致用户登不进去
Workers KV键值存储服务错误率飙升(依赖核心代理),很多依赖 KV 的功能(比如动态页面)挂了
Cloudflare 控制台虽然能打开,但登录不了(Turnstile 挂了);配置更新要么失败,要么同步极慢
邮件安全垃圾邮件检测准确率下降(少了个 IP 信誉源),但邮件收发没断,影响不大
Access(身份验证)新登录全失败,已登录的会话不受影响(因为会话存在本地)

这里提下 “延迟飙升” 的原因:故障发生后,系统会自动记录 “未捕获错误” 的调试信息(比如日志、调用栈),结果大量的 500 错误导致调试系统占了太多 CPU,反而让正常请求的处理延迟变高 —— 这也是很多系统故障时的 “次生问题”,值得我们注意。

修复过程:从 “止血” 到 “根治”,用了 5 个多小时

Cloudflare 的修复节奏还算标准,遵循了 “先止血,再根治,最后清尾” 的故障处理原则,咱们看看时间线(UTC 时间):

  1. 11:28-13:05:初步排查,先给关键服务 “止血”一开始工程师以为是 Workers KV 出问题,尝试了 “限流”“账号隔离” 等办法,没用。直到 13:05,发现核心代理是关键,于是给 Workers KV 和 Access 做了 “绕过”—— 让它们用旧版本的代理,先降低错误率(虽然旧代理有 bot 分数问题,但至少能干活)。
  2. 13:37-14:24:定位核心原因,停掉坏文件生成这时终于查到是 “Bot Management 的特征文件” 有问题,于是:
    • 停掉自动生成坏文件的查询;
    • 找了一个 “已知正常” 的旧版本文件,测试加载(确认能恢复)。
  3. 14:30:全球推送正常文件,主故障解决把好文件推到全球所有节点,同时重启核心代理(FL2)。到 14:30 左右,大部分服务恢复正常,500 错误率骤降。
  4. 17:06:清尾,所有服务恢复剩下的工作就是重启那些 “进入坏状态” 的小服务(比如部分控制台功能、邮件安全的 IP 信誉源),直到 17:06,所有 5xx 错误率回到正常水平。

给我们的启示:从别人的坑,学自己的系统设计

Cloudflare 这次故障,号称是 “2019 年以来最严重的一次”—— 毕竟它承载了全球近 10% 的互联网流量,一挂就是 5 个多小时。但对我们来说,更有价值的是 “从这个坑中学到什么”,我总结了 4 个关键点:

1. 任何 “权限变更”“配置调整”,都要做 “灰度 + 回滚预案”

Cloudflare 的权限变更是 “逐步推到分片”,这本身是灰度,但问题在于 “没预判到查询会出问题”。其实可以加一步:先在一个测试分片上跑,观察特征文件是否正常,再全量推 —— 很多故障都是 “想当然觉得没问题,跳过了测试”。

2. 内部配置文件,也要像 “用户输入” 一样做校验

之前我们总觉得 “内部生成的配置文件不会有问题”,但这次教训是:哪怕是自己系统生成的文件,也要加校验(比如检查文件大小、特征数量是否在合理范围),超过阈值就拒绝加载,而不是直接 panic。

3. 故障排查时,要警惕 “巧合”,别被无关现象带偏

这次状态页挂了是巧合,但差点让工程师误以为是 “连环攻击”。下次排查时,可以先确认 “这个现象和核心故障是否真的有关联”—— 比如状态页挂了,先 ping 一下 CloudFront 的节点,看看是不是它自己的问题,再下结论。

4. 核心服务要有 “全局紧急开关”

Cloudflare 后来提到要 “加更多全局 kill 开关”—— 比如 Bot Management 出问题时,能一键关掉这个模块,让流量走 “无 bot 检测” 的降级路径,而不是直接返回 500。我们自己做核心服务时,也得设计 “降级方案”,不能 “要么全好,要么全挂”。

最后:坦诚复盘比 “不出错” 更重要

  • reward_image1
此作者没有提供个人介绍。
最后更新于 2025-11-20