2025年Cloudflare故障回顾
2025年11月18日,互联网服务商Cloudflare(Cloudflare: 全球领先的互联网性能与安全服务提供商,提供CDN、DDoS防护等服务)出现长达5小时的故障。包括GPT、推特在内的大量头部互联网服务全面瘫痪。这也是继6月12日的Google Cloud故障和10月19日的AWS故障之后,半年来出现的第三起全球范围内的大型瘫痪事件。这提醒了我们,互联网这个强大的科技产物,比想象的还要脆弱。在三次事故中出场两次的Cloudflare,作为互联网的基建服务,也是其脆弱的根源之一。
故障的导火索:Bot Management模块崩溃
作为全球最大的CDN(Content Delivery Network: 内容分发网络,通过在全球部署节点加速内容传输)之一,Cloudflare的核心定位是流量的第一道关卡。当然,能做到这个市场规模,Cloudflare自然不能只贩卖基本的流量加速功能。防火墙、限流、监控、DEBUG之类的功能,也算是CDN服务的标配。而这其中,就有一个叫做Bot Management(BM: 机器人管理,用于检测并拦截恶意或非预期的自动化请求)的模块。顾名思义,这就是用来检测HTTP请求(Hypertext Transfer Protocol Request: 超文本传输协议请求,客户端向服务器发送的数据请求)是否来自爬虫之类的机器人。这个模块每隔几分钟,就会从数据库获取最新的特征数据,用来更新自己的检测模型。
在11:05,Cloudflare程序员对数据库的权限设置进行了更新。这个更新引发了连锁反应,导致返回的数据出现了一些问题。时间来到11:28,BM模块定时连接数据库,但这次它拿到了有问题的错误数据。对此没有任何防御的BM模块整个崩溃,所有HTTP请求都被报错打回。
工程师的误判与故障的定位
因为数据库补丁的发布是渐进式的,所以此时还有一些分片没有更新到错误。如果BM模块下一次定时更新时,刚好连上的是还没有问题的那台机器,它就会拿到干净的数据,检测就会恢复正常,故障就会消失。于是从11:30到13:30,请求的报错呈现过山车似的曲线,一会很多,一会很少,一会完全消失,一会又强度拉满。这就导致了在事发的前两个小时,Cloudflare的工程师一直都把这件事当做是黑客组织的DDoS(Distributed Denial of Service: 分布式拒绝服务攻击,通过大量请求使目标服务过载瘫痪)袭击来处理。因为刚好在前一天,微软就刚刚遭遇了每秒1.8TB的DDoS,仅次于Cloudflare自己在9月份遭遇的历史最强的2.7TB每秒的DDoS。
直到13:30,数据库补丁的滚动更新完成,现在所有分片都有了问题,报错曲线也变成了平稳的直线。工程师们这才意识到,故障可能出自于内部系统。他们快速定位到了BM模块,找到了有问题的错误数据,于是对数据进行了回滚。当然这是没有用的,因为又过了一会儿,BM模块就会重新从数据库获取到最新鲜的错误。直到14:24,工程师才找到问题的源头,掐断了BM模块的定时更新功能,并手动部署了一份干净的数据。6分钟后,全球的BM模块都同步了干净的数据,CDN服务才开始恢复。最后在17:06,所有被连累的服务陆续恢复,故障到此结束。整个事故持续了5个半小时,超过了6月份被Google Cloud连带着中招的那次,成为Cloudflare历史上最长的一次故障。
技术深挖:SQL代码与数据库差异
六年前,Cloudflare程序员因为写错一段正则表达式,引发了当时互联网历史上最严重的全球瘫痪事件。可能受此影响,在BM模块的设计里,他们没有用正则表达式对HTTP请求进行过滤,而是选择了传统机器学习打造的分类器模型。分类器是否准确,取决于特征(feature)的选择。一个HTTP请求,明面上可以作为feature的信息已经有很多了,比如原生的HTTP header就会包含30多种参数。但网络瞬息万变,敌人时刻都在调整,所以BM模块没有写死feature列表,而是从数据库中动态获取,这样就能确保所有分类器都可以在第一时间拿到最新最全的特征数据。
在数据库里储存这些feature的表格叫做http_requests_features,它的每一个column就是一个feature。因为某些历史遗留问题,BM模块要先通过一个叫做default的database,拿到这个表格的metadata,也就是每个column的名字、类型之类的信息。在BM模块的代码里,这个步骤由一个很简单的SQL代码完成。拿到这些metadata之后,BM模块就可以通过一个叫做r0的database,读取表格里相对应的数据。这样做其实有点脱裤子放屁,因为既然允许用户读取r0上表格的数据了,为何还要把它的metadata藏起来,要用户跑去另一个地方拿呢?
Cloudflare的程序员也意识到了这个问题,所以他们在11月18日早上11:05推送了这么一个更新:“给用户加上直接读取r0的metadata的权限。”这样用户就可以直接在r0上完成所有操作,速度更快,也更安全了。这个权限的改动本身没有什么问题,问题出在执行第一步的这段SQL代码上。你会发现这里只是写明了要查找的table名字,但没有说是哪个database里的table。如果他们用的是世界上最好的数据库PostgreSQL(PostgreSQL: 一种强大的开源关系型数据库管理系统),那么一切都不会有问题。因为PostgreSQL的database是互相隔离的,用户每次连接数据库,只会连上一个database,也只会看到这个database里的数据。你连上的是default,看到的就是default里的system.columns;连上的是r0,拿到的自然就是r0里的system.columns。
但是Cloudflare用的是ClickHouse(ClickHouse: 一种开源的列式数据库管理系统,以查询速度快著称)。ClickHouse是俄罗斯人发明的,用上了很多俄罗斯特色的数学黑科技,主打的就是一个快。要快就要有取舍,在读取权限的设置上,它采取的就是隐式的范围控制。也就是说,用户有多少数据的读取权限,它所发起的查询就会看到多少数据的信息。所以在权限改动的补丁发布后,用户同时拥有了default和r0的metadata的读取权限。那么这段SQL代码现在就会同时看到这两个database的system columns数据,它返回的,就是每个column都重复了一遍的feature列表。这一坨错误的数据,就是一切的源头,故障的导火索。
大家可以尽情吐槽这段SQL代码,可以批评它为何不在WHERE条件里面加上database等于default或者等于r0,为何不用SELECT DISTINCT进行过滤。但我认为,负责BM模块的整个团队都应该受批评,因为既然选择了ClickHouse,就应该很清楚它独特的权限逻辑。那么在做了一个关于权限的改动后,重点检查所有受影响的SQL代码是理所当然的事情。更何况受影响的总共就两步,第一步总共才4行代码,怎么能从头到尾都没有一个人发现问题呢?之前Google和AWS的故障报告都很老实地解释了开发组在测试阶段都犯了什么错误,导致bug泄露到生产环境,但Cloudflare的这份报告对此只字未提,所以我们就无从复盘了。
Rust代码中的“核武器”与意外之功
接下来这份错误的feature列表将会被BM模块的应用代码使用,开始错上加错。Cloudflare的主旨一直都是“更快、更快、更快”,所以他们在BM模块代码里使用了preallocate memory(预分配内存: 在程序运行前预留一块内存空间,以提高性能并避免运行时动态分配)的做法。也就是提前预定一节内存空间给feature列表,避免需要在运行时动态检查列表的长度,动态分配内存。而这个预留的空间是200个feature的长度。200这个数字没有任何特殊含义,纯粹就是Cloudflare程序员觉得这个数字够大,在可见的未来应该都不会有问题。毕竟一个HTTP请求就那么点东西,拼命凑,估计也就能凑出百来个feature。他们完全没有考虑过有一天会从数据库获取超过200个feature的可能性。所以在把feature列表并入数组的那行Rust(Rust: 一种注重性能、内存安全和并发性的系统编程语言)代码里,对于潜在的数组溢出错误,代码中使用了unwrap()(unwrap函数: Rust语言中用于处理可能失败操作的方法,如果操作失败会导致程序崩溃)来处理。
unwrap函数是Rust中的“核武器”,一旦遇到错误,它就会把整个程序给停掉(panic),这也直接导致了HTTP请求的报错。在官方故障报告发布的一个小时后,网上就出现了大量的视频和文章,说这次故障完全是Rust代码的锅,写unwrap()的人该死。我不确定这些人有多少是真的看懂了整篇报告,估计他们就是鼠标一拉到底,看到了最后这张Rust代码的截图,于是就围绕着它进行看图写作文了。
Rust强制要求程序员处理所有的错误,这是一个很好的做法。当时就有不少人在评论区里说:“Rust有unwrap()这种后门,哪里算强制要求了?”他们没搞懂的是,unwrap()本质上就是一种处理方式,只不过是一种非常简单粗暴的处理方式:“如果数据正常,就返回数据;如果不正常,那我就把整个程序给炸掉算了!”这个粗暴的做法,实际上并没有影响大局,你甚至可以说它立了大功。因为Cloudflare最近恰好在升级BM模块的引擎,引擎的迁移还没有完成,所以在系统内部,现在有两个版本的引擎在同时运行。新版引擎因为用了这段unwrap代码,导致HTTP请求报错。而那些还在用旧引擎的用户,没有出现报错。但因为分类器拿到的数据有错误,无法正常打分,只能统一返回0分。在正常情况下,0分只会赋予内部系统的请求,就是为了让BM模块不做检测,直接放行。而在错误的影响下,所有外部请求都会被安静地放行了,BM模块等于是形同虚设。所以,如果没有新版引擎里的unwrap代码“大闹天宫”,Cloudflare的工程师们甚至不一定能发现BM模块已经实质性停摆了,也不一定能发现它那么多低级的代码错误。
互联网的脆弱性与中心化趋势
在SQL代码里少写几个条件,在应用代码里定死数组的长度,这些低级的编程错误,都是程序员为了图方便而走的捷径。底层代码是这样,顶层的架构也半斤八两。可以说整个互联网现在就是在各种“图方便”的决策下,形成了一个脆弱的整体。从功能层面上看,DNS、CDN这些在发明之初就是为了方便数据交流的“翻译表”和“加速器”,逐渐成为大家最依赖的基础功能。随便一个出问题,整个“互联网”都会瞬间变成“不联网”。
而从资源层面上看,网络基建聚集在Cloudflare等几个企业,应用基建聚集在AWS、GCP等几个云服务商上。现在懂行的恐怖分子都知道,想要破坏人类社会,不需要多少枚核弹,往弗吉尼亚北部投一枚EMP(Electromagnetic Pulse: 电磁脉冲,由核爆炸或高能电磁武器产生,能摧毁电子设备)就够了。这也是有点讽刺的,毕竟互联网的鼻祖就是为了去中心化进行设计的、具有高度容错的军事系统。但随着互联网的发展,各种新服务的诞生,容错非但没有提高,反而是越来越低的。但不可否认的是,如果没有这些发展,互联网或许还能保留当初的理论优点,但肯定就不会像现在那样,成为几十亿人赖以生存的社会基建了。
📌 文中提及的人物和组织
公司/组织: Cloudflare, Google Cloud, AWS, Twitter, Microsoft
产品/模型: GPT, PostgreSQL, ClickHouse, Rust
媒体/书籍: 《让编程再次伟大#49》, 《让编程再次伟大#15》, 《让编程再次伟大#21》