Cloudflare 2025年11月18日全球网络出现故障事后分析
2025年11月18日协调世界时11:20(本博客所有时间均为协调世界时),Cloudflare网络开始出现核心网络流量传输严重故障。试图访问我们客户网站的互联网用户会看到错误页面,提示Cloudflare网络内部发生故障。

该问题并非由任何形式的网络攻击或恶意活动直接或间接引发。实际诱因是某数据库系统权限变更导致其向机器人管理系统的“特征文件”写入大量冗余条目,致使该文件容量翻倍。超出预期的特征文件随后传播至网络中所有节点设备。
运行于这些机器上的网络流量路由软件会读取该特征文件,以使机器人管理系统实时应对不断变化的威胁。该软件对特征文件的大小设定了限制,而该限制值低于文件翻倍后的实际大小,导致软件运行失败。
我们最初误判症状源于超大规模DDoS攻击,后准确定位核心问题,成功阻止超大特征文件的传播并替换为早期版本。截至14:30,核心流量基本恢复正常。随后数小时内,我们持续缓解网络各节点因流量激增造成的负载压力。截至17:06,Cloudflare所有系统均已恢复正常运行。
我们对此次事件给客户及整个互联网生态造成的影响深表歉意。鉴于Cloudflare在互联网生态中的重要性,任何系统中断都是不可接受的。网络一度无法路由流量的状况令团队每位成员都深感痛心。我们深知今日未能履行承诺。
本文将详细阐述事件经过及失效系统与流程。这只是我们防止此类故障重演计划的开端,而非终点。
故障始末
下图展示了Cloudflare网络处理的HTTP 5xx错误状态码请求量。正常情况下该数值应极低,且在故障发生前确实如此。

11:20前的数据量是我们网络中观察到的预期5xx错误基线。突增及后续波动表明系统因加载错误功能文件而故障。值得注意的是,系统随后会恢复运行一段时间——这种行为对于内部错误而言极为异常。
究其原因,该文件由ClickHouse数据库集群上的查询每五分钟生成一次,而该集群正逐步更新以优化权限管理。只有当查询运行在已更新的集群部分时才会生成错误数据。因此每五分钟系统就有可能生成有效或无效的配置文件集,并迅速传播至整个网络。
这种波动导致系统状态难以判断——由于网络中时而分发正常配置文件时而分发错误配置文件,整个系统会反复恢复又重新故障。最初我们甚至怀疑是遭受攻击所致。最终所有ClickHouse节点都开始生成错误配置文件,系统波动趋于稳定在故障状态。
故障持续至14:30才被定位并解决。我们通过停止错误特征文件的生成与传播,手动将已知有效文件插入特征文件分发队列,并强制重启核心代理服务器来解决问题。
上图中残留的长尾部分,是团队重启其他已进入异常状态的服务所致,5xx错误码数量于17:06恢复正常。
以下服务受到影响:
| 服务/产品 | 影响描述 |
|---|---|
| 核心CDN和安全服务 | 返回HTTP 5xx状态码。本文开头的截图展示了向终端用户返回的典型错误页面。 |
| Turnstile | Turnstile加载失败。 |
| Workers KV | 因核心代理故障导致对KV“前端”网关的请求失败,Workers KV返回的HTTP 5xx错误显著增加。 |
| 控制面板 | 控制面板基本正常运行,但登录页面因Turnstile不可用导致多数用户无法登录。 |
| 邮件安全 | 邮件处理与投递未受影响,但我们观察到IP信誉源的临时访问中断,导致垃圾邮件检测准确率下降,部分新域名年龄检测未能触发,未造成重大客户影响。同时部分自动移动操作出现故障,所有受影响邮件均已审核并修复。 |
| 访问服务 | 自事件发生起至13:05回滚操作启动期间,多数用户普遍遭遇身份验证失败。现有访问会话不受影响。所有验证失败均跳转至错误页面,意味着用户在验证失败期间从未进入目标应用程序。该时段内成功登录记录均被正常保存。当时尝试的任何访问配置更新要么直接失败,要么传播速度极慢。目前所有配置更新均已恢复正常。 |
除返回HTTP 5xx错误外,我们还观察到影响期间CDN响应延迟显著增加。这是由于调试和可观测系统消耗了大量CPU资源——这些系统会自动为未捕获的错误添加额外调试信息。
Cloudflare的请求处理机制及今日故障成因
每个发往Cloudflare的请求都会在我们的网络中遵循明确路径。无论是浏览器加载网页、移动应用调用API,还是其他服务的自动化流量,这些请求首先终止于我们的HTTP和TLS层,随后流入核心代理系统(我们称之为FL,即“前线”),最终通过Pingora进行缓存查询或从源站获取数据。
我们曾在此处详细介绍核心代理的工作原理链接。

当请求通过核心代理时,我们会运行网络中可用的各类安全与性能产品。代理会应用每个客户的独特配置和设置,从强制执行WAF规则和DDoS防护,到将流量路由至开发者平台和R2。它通过一组特定领域的模块实现这一目标,这些模块将配置和策略规则应用于通过我们代理的流量。
其中“机器人管理”模块正是本次故障的根源。
Cloudflare的机器人管理系统包含多项技术,其中机器学习模型会为每条穿越网络的请求生成机器人评分。客户通过机器人评分控制允许哪些机器人访问其网站。
该模型以“特征”配置文件作为输入。此处的特征指机器学习模型用于判断请求是否为自动化的单一属性。特征配置文件即各类特征的集合。
该特征文件每隔数分钟刷新一次并发布至整个网络,使我们能够应对互联网流量流向的变化,及时应对新型机器人及攻击手段。鉴于恶意行为者战术变化迅速,频繁快速的部署至关重要。
底层ClickHouse查询行为(详见下文)的变更导致该文件生成大量重复的“特征”行,使原固定大小的特征配置文件发生变形,进而触发机器人模块报错。
结果导致核心代理系统(负责处理客户流量的系统)对所有依赖机器人模块的流量返回HTTP 5xx错误代码。这同时影响了依赖核心代理的Workers KV和Access服务。
与本次事件无关的是,我们此前及目前正将客户流量迁移至新版代理服务,内部代号为FL2。两个版本均受到该问题影响,但观察到的影响程度不同。
部署在新版FL2代理引擎的客户遭遇HTTP 5xx错误。使用旧版代理引擎(简称FL)的客户虽未出现错误提示,但机器人评分生成异常,导致所有流量均显示零分。部署机器人拦截规则的客户因此遭遇大量误报。未在规则中使用机器人评分的客户未受影响。
另一个明显症状是Cloudflare状态页面意外下线,这曾使我们误判为攻击事件。该页面完全托管于Cloudflare外部基础设施,与Cloudflare系统无任何依赖关系。虽然最终证实是巧合,但当时部分诊断团队成员曾怀疑攻击者可能同时针对我们的系统和状态页面发起攻击。当时访问状态页面的用户看到的是错误提示:

在内部事件聊天室中,我们担忧这可能是近期高强度Aisuru DDoS攻击的延续:

查询行为变更
如前所述,底层查询行为的变更导致特征文件中出现大量重复行。该数据库系统采用ClickHouse软件。
为便于理解,需先了解ClickHouse分布式查询的工作原理。ClickHouse集群由多个分片构成。为查询所有分片数据,我们在名为default的数据库中创建了分布式表(由Distributed表引擎驱动)。该引擎通过查询r0数据库中的底层表来获取数据,而这些底层表正是存储在ClickHouse集群各分片上的数据存储位置。
分布式表查询通过共享系统账户运行。为提升分布式查询的安全性和可靠性,我们正推进相关工作,使其转为在初始用户账户下运行。
此前,ClickHouse用户仅能在查询系统表(如system.tables或system.columns)的表元数据时看到default数据库中的表。
鉴于用户已隐式拥有对r0底层表的访问权限,我们于11:05实施变更,将此访问权限显式化,使用户也能查看这些表的元数据。通过确保所有分布式子查询均能在初始用户权限下运行,查询限制和访问权限授予可进行更精细的评估,避免单个用户的异常子查询影响其他用户。
上述变更使所有用户均能获取其可访问表的准确元数据。遗憾的是,此前存在一个假设:此类查询返回的列列表仅包含“default”数据库:
SELECT name, type FROM system.columns WHERE table = ‘http_requests_features’ order by name;
请注意该查询未过滤数据库名称。随着我们逐步为特定ClickHouse集群用户授予显式权限,11:05变更后上述查询开始返回列的“重复项”——这些列实际存储于底层r0数据库的表中。
不幸的是,本节开头提及的文件生成逻辑中,机器人管理功能正是通过此类查询构建每个输入“特征”。
上述查询将返回类似下图所示的列表(简化示例):

然而,由于用户获得了额外权限,响应中现在包含了r0模式的所有元数据,这使得响应中的行数实际增加了一倍以上,最终影响了最终文件输出中的行数(即特征数量)。
内存预分配
运行在代理服务上的每个模块都设置了若干限制,以避免内存消耗失控,并通过预分配内存实现性能优化。在此特定场景中,机器人管理系统对运行时可使用的机器学习特征数量设定了上限。当前该限制值为200,远高于我们约60个特征的实际使用量。再次强调,设置此限制是因为出于性能考虑,我们为特征预分配了内存。
当包含超过200个特征的异常文件传播至服务器时,触发了该限制导致系统崩溃。引发未处理错误的FL2 Rust代码检查逻辑如下所示:

这导致了以下恐慌,进而引发了5xx错误:
线程fl2_worker_thread恐慌:对Err值调用了Result::unwrap()
事件期间的其他影响
事件期间,依赖我们核心代理的其他系统也受到影响,包括 Workers KV 和 Cloudflare Access。团队于 13:04 通过为 Workers KV 实施补丁绕过核心代理,成功降低了对这些系统的影响。此后,所有依赖 Workers KV 的下游系统(如 Access 本身)均观察到错误率下降。
Cloudflare Dashboard同样受到影响,原因在于内部使用Workers KV服务,且Cloudflare Turnstile作为登录流程组件被部署其中。
本次故障导致Turnstile服务中断,造成未保持活跃Dashboard会话的用户无法登录。如下方图表所示,服务可用性在两个时段出现下降:11:30至13:10,以及14:40至15:30。

首次中断发生于11:30至13:10,源于Workers KV服务受影响——部分控制平面及仪表板功能依赖该服务。13:10时Workers KV绕过核心代理系统后恢复正常。仪表板的二次中断发生在功能配置数据恢复后,大量登录请求积压导致系统不堪重负。该积压请求叠加重试机制,导致延迟显著上升,进而降低仪表盘可用性。通过扩展控制平面并发处理能力,系统于15:30左右恢复正常。
修复与后续措施
系统现已全面恢复正常运行,我们已着手实施强化措施以防范此类故障。具体包括:
- 对Cloudflare生成的配置文件实施同等于用户生成的输入数据的强化处理
- 为更多功能启用全局终止开关
- 消除核心转储或其他错误报告耗尽系统资源的可能性
- 全面审查所有核心代理模块的错误状态故障模式
本次是Cloudflare自2019年以来最严重的停机事件详情见。此前曾发生过导致控制面板不可用的故障。有些故障曾导致新功能暂时无法使用。但在过去六年多时间里,我们从未遭遇过导致绝大多数核心流量停止通过我们网络的故障。
今日发生的故障是不可接受的。我们始终致力于构建具备高度容错能力的系统架构,确保流量持续畅通。过往每次故障都促使我们打造出更具韧性的新系统。
我谨代表Cloudflare全体团队成员,为今日给互联网用户带来的困扰致以诚挚歉意。
| 时间(UTC) | 状态 | 描述 |
|---|---|---|
| 11:05 | 正常状态 | 数据库访问控制变更已部署 |
| 11:28 | 影响开始 | 变更影响客户环境,首次在客户HTTP流量中观察到错误 |
| 11:32-13:05 | 团队调查Workers KV服务流量激增及错误问题 | 初始症状表现为Workers KV响应率下降,导致下游其他Cloudflare服务受影响。尝试通过流量调控和账户限流等缓解措施使Workers KV服务恢复正常运行水平。首次自动化检测于11:31发现问题,人工调查于11:32启动,事件呼叫于11:35创建。 |
| 13:05 | 实施Workers KV与Cloudflare Access绕行方案——影响减弱。 | 调查期间,我们通过内部系统绕行机制使Workers KV和Cloudflare Access回退至核心代理的旧版本。尽管该问题在旧版代理中同样存在,但影响程度较小,具体如下所述。 |
| 13:37 | 工作重点转向将Bot Management配置文件回滚至最后已知良好版本。 | 我们确信Bot Management配置文件是本次事件的触发点。团队通过多条工作流推进服务修复,其中最快的工作流是恢复该文件的旧版本。 |
| 14:24 | 停止生成并传播新的机器人管理配置文件。 | 确认机器人管理模块是500错误的根源,且由错误配置文件引发。我们已停止新配置文件的自动部署。 |
| 14:24 | 新文件测试完成。 | 验证旧版配置文件成功恢复系统后,我们立即推进全球范围内的修复工作。 |
| 14:30 | 主要影响已解决。下游受影响服务开始观察到错误减少。 | 部署了正确的机器人管理配置文件,多数服务已恢复正常运行。 |
| 17:06 | 所有服务恢复正常。影响结束。 | 所有下游服务已重启,全部操作功能完全恢复。 |
Cloudflare连接云可保护整个企业网络,助力客户高效构建互联网级应用,加速任何网站或互联网应用, 抵御DDoS攻击,阻隔黑客入侵,并助您迈向零信任安全体系。
立即访问1.1.1.1,在任意设备上下载免费应用,让您的网络体验更快速、更安全。
若想了解我们构建更美好互联网的使命,由此开始。若您正在寻求职业新方向,欢迎查看我们的职位空缺。
本文文字及图片出自 Cloudflare outage on November 18, 2025
共有 363 条讨论
发表回复
你也许感兴趣的:
- Cloudflare 2025年8月21日 事故解析
- 2025年7月14日 Cloudflare 1.1.1.1 宕机事件解析
- Cloudflare 推出对人工智能数据抓取工具的默认阻断功能
- Cloudflare 将推出按爬取次数付费的 AI 爬虫服务
- 2025年10月19日亚马逊 us-east-1 宕机事故反思
- 【外评】谷歌云计算 VMware 引擎 (GCVE) 私有云宕机事故
- 又翻车!微软一次更新引爆大规模连锁反应,Bing、Copilot等多个软件集体宕机五小时!
- 腾讯云4月8日故障复盘及情况说明
- 从谷歌 20 年的站点可靠性工程(SRE)中学到的 11 个经验教训
- B站宕机事故复盘:2021.07.13 我们是这样崩的
这就是价值数百万美元的.unwrap()故事。在支撑互联网重要部分的关键基础设施路径中,对Result调用.unwrap()等同于宣告:“这绝不能失败,若失败则立即崩溃线程。” Rust编译器迫使他们承认可能失败(这正是Result存在的意义),但他们却刻意选择引发panic而非优雅处理。这正是教科书式的“解析而非验证”反模式。
我知道这属于“事后诸葛亮”,但面对这场让我耗费半天时间的大规模故障,我只能如此直言。
我在FAANG公司处理过多次事故,以下是我的见解。根本问题不在于Rust或编码错误,而在于:
1. 他们的机器人管理系统设计为快速向整个网络推送配置。这种快速响应攻击的机制虽必要,但相较于渐进式变更的系统,它带来了额外风险。
2. 尽管全网快速推送配置的风险显著提升,他们仍耗费2小时才锁定配置是直接诱因,又花1小时才完成回滚。
系统故障的标准操作是回滚至已知良好状态。若采用渐进式推送,当金丝雀节点出现故障时,就能获得明确的回滚信号。此处存在特殊情况:他们需要系统快速传播变更至所有节点,这本身是巨大风险,但其可见性与快速回滚能力并未与风险匹配到位。
虽然分析代码根源确实有帮助,但代码不可能完全无缺陷。可靠性不仅在于规避漏洞。更在于掌握如何清晰洞察变更与行为之间的关联,并具备快速回滚至已知良好状态的能力。
Cloudflare 多年来在可用性方面表现卓越,其Rust代码现已支撑全球20%的互联网流量。这支团队确实了不起。
> 今日多位朋友告知Cloudflare服务中断。作为Cloudflare第一代FL的核心开发者,我想分享几点见解。
> 这并非攻击事件,而是由“隐含假设+配置链”引发的经典连锁反应——权限变更暴露了底层表结构,导致生成的功能文件行数翻倍。这超出了FL2的内存预设值,最终触发核心代理进入恐慌状态。
> Rust虽能缓解部分错误,但边界层、数据流和配置管道的复杂性仍超出该语言能力范围。真正的挑战在于设计健壮的系统契约、隔离层和容错机制。
> 向Cloudflare工程师们致敬——身处前线扑灭火势的他们承受着此类事件的最大压力。
> 技术细节: 即便正确处理解包操作,仍会发生内存不足。核心问题在于功能导入时缺乏契约验证。配置系统需采用“异常→拒绝,保留最后已知有效值”的逻辑。
> 为何持续如此之久?全局终止开关未能有效触发快速断路。早期对攻击的怀疑也导致延误。
> 为何不回滚软件版本或重启?
> 回滚不可行,因问题根源并非代码缺陷,而是持续扩散的错误配置。在缺乏版本控制和终止开关的情况下,重启只会加速所有节点加载错误配置,导致崩溃加剧。
> 为何不回滚配置?
> 配置缺乏版本控制,更像持续更新的流式数据。只要ClickHouse管道保持活跃,手动回滚将在数分钟内导致新损坏文件再生成,覆盖所有修复成果。
https://x.com/guanlandai/status/1990967570011468071
人们似乎对 unwrap 存在认知盲区,或许因其在示例代码中频繁出现。在生产环境中,unwrap 或 expect 的使用应与 panic 同等严格审查。
若本就打算调用 panic,生产代码中使用 unwrap 并非绝对无效。但正如每个unsafe代码块都需要安全注释,生产代码中的每次unwrap操作都需添加无故障注释。clippy::unwrap_used可强制执行此规范。
那么切片/映射/向量中的索引操作呢?每个
foo[i]都需要添加无误性注释吗?因为它们本质上等同于get(i).unwrap()。是吗?有趣的是,我在Rust中很少使用索引访问。要么是遍历数据结构的元素(这种情况下我会使用迭代器),要么是使用不可信的索引值(这种情况下我会显式处理错误情况)。在极少数能保证索引值永不失效的场景(例如图遍历中索引值永远不会超出遍历作用域),我才会为不安全访问创建安全封装并记录不变量。
若果真如此,实属难得。你描述的情况与我实际见闻大相径庭。事实上,我从未见过任何库或生产代码库能为每个切片访问都提供绝对安全保证。即便通过安全审计的加密库也做不到。就个人经验而言,在图结构密集型代码中避免索引操作相当困难,因此我始终在探索增强访问安全性的创新方法。若有相关代码示例,非常期待分享。
我的经验法则是:当数组/映射及其索引/键值均为函数或结构体的私有实现细节时,未检查的访问是可接受的——因为此时不变量在严格作用域内易于手动验证。我见过以下场景采用此做法:
* 接收访问者函数作为参数的图/树遍历函数
* 对排序数组的二分搜索
* 二叉堆操作
* 开放寻址哈希表的桶探测
> 图论密集型代码
能否提供更具体的场景示例?技术手段虽多,但不存在万能解决方案。
当然,最近我主要在开发几个编译器。假设我要构建固定大小的SSA中间表示(IR)。每条指令包含操作码和两个操作数(本质上是其他指令的指针)。IR在第一阶段填充,第二阶段进行降低。降低过程中我会对IR执行若干窥孔优化和代码移动优化,最后进行寄存器重新分配+汇编代码生成。该阶段会导致中间表示发生变异,索引需失效/更新。关键在于此阶段对性能至关重要。
编译器在违反假设时触发异常是合理的,但讨论中的Cloudflare代码并非如此。
通常情况下,你应该采用函数式编程风格,用迭代器处理几乎所有切片或其他容器的迭代操作。
对于那5%超出标准迭代器处理能力的复杂场景?我从不费心证明索引正确性,但也不认为这样做不可行。
在Rust中极少需要安全注释,因为绝大多数代码本身就具备安全性。该语言还提供了避免手动迭代的工具(不仅出于安全考虑,更因其能让编译器消除边界检查),因此编写这些注释其实相当可行——毕竟它们仅在处理非常规操作时才必要。
我未重申讨论代码的背景:必须避免panic。若你不在意代码是否抛出异常,那么尽可使用unwrap/expect/索引操作,因为这符合你选择的错误处理方案。对于命令行工具或独立子进程等场景,这种做法完全可行且能大幅简化代码审查。
因此:首先识别绝对不能引发panic的代码。在该代码范围内,确实,当你罕见地使用[i]时,至少需要尝试论证为何认为其在边界内。但最好避免这样做。
目前有几种尝试让编译器证明代码不会引发panic的方法(例如no-panic库)。
在我看来,既然有迭代器可用,索引操作本就相对罕见。若目标是彻底规避恐慌风险,算术溢出反而更值得警惕。
Cargo需要为可证明不会引发恐慌的库添加标签(暂不考虑分配等控制流外的因素)。
我希望将会引发恐慌的库从依赖链中禁用。
语言确实需要这方面的额外静态保证机制。我愿意支持。
我更倾向于编译时保证。
允许我为函数(或整个库)添加“无恐慌”标签注解,当函数或其调用项存在 可达 恐慌时触发编译错误。
只要常量传播能证明恐慌不可达,此方案即可兼容大量未修改的库。该方案还允许库提供恐慌版与非恐慌版API (许多库已实现此功能)。
迭代场景确实如此。但其他场景也需考虑,例如处理大量关联数据结构时。若需高性能,往往必须采用索引+区域策略。这类策略在数学代码库中也很常见。
我的意思是…嗯,通常来说。迭代器就是为此而存在的。
这和人们对Java检查异常的盲点如出一辙。人们常 resort to 采用“宝可梦式异常处理”,要么盲目忽略异常,要么将其重新抛出为运行时异常。当Rust流行时,我曾困惑于人们盛赞Result——它本质上是缺乏堆栈跟踪的检查异常。
“检查异常其实很棒”阵营,起来吧!:p
若当时能提供更多语法糖,采用情况本会截然不同。例如,若能用简洁语法声明:“本方法中任何上报的(检查型)DeepException异常,应立即替换为包含原始异常的(检查型)MylayerException异常。”
虽然懒惰的程序员仍可能把所有异常都塞进泛型MylayerException,但这种混乱远比散落各处的RuntimeException更容易后期修复。
我完全赞同!受检查异常其实很有价值,对它们的厌恶实在太短视了。针对受检查异常的批评同样适用于静态类型系统,但人们都承认静态类型在编译时预防错误的巨大价值。受检查异常具有同等价值,却莫名遭到贬低。
这种厌恶可能源于两个原因:
1. 大多数情况下,开发者既不想处理
InterruptedException或IOException,又需要将它们向上抛出。这种情况下代码会变得冗长。2. 它导致lambda表达式与函数无法兼容。例如:若将函数传递给forEach方法,就必须将其包装为运行时异常。
3. 由于上述两点,多数人会偷懒使用
throws Exception,这反而抵消了异常机制的初衷优势。在业务应用程序(Java使用最广泛的领域)中,未捕获的异常并非大问题。它会向上冒泡并在栈中更高的位置(例如服务器日志器)得到处理,而不会干扰应用程序的其他部分。这降低了要求每个函数都抛出InterruptedException/IOException的实用性,因为这些异常几乎不会发生。
确实,这两种情况都涉及分层问题:你的代码有责任决定需要跨越哪些抽象层,并执行该决策。将深层函数的异常类型进行转换/封装,本质上与在相同位置转换/封装返回类型并无二致。
我认为这本质上是心理或使用场景问题:人们厌恶思考错误处理,因为这类硬核工作总会消耗超出预期的时间——不仅在数字世界,实体机器也是如此。而且这类任务更容易被拖延到“以后处理”。
这种方式更轻量:堆栈跟踪的生成需要大量开销;而结果值在失败时不产生额外开销。只有当错误无法处理时才会触发开销(恐慌)。(多数Java/C#书籍未说明抛出异常存在显著性能开销。)
异常机制强制所有错误触发恐慌,因此它们本应仅用于“异常”情况。当预期错误发生时(文件末尾、套接字损坏、文件不存在),若要避免抛出异常,你必须使用不自然的返回类型,或承受“抛出”时恐慌带来的性能代价。
在Rust中,堆栈跟踪发生于panic(unwrap)时——即错误未被处理的时刻。换言之,它并非发生在文件未找到时,而是错误未被处理之时。
个人偏好:unwrap()应被废弃并重命名为_panic()。这更符合标准库其他方法的命名规范,也更符合其危险性。
这正是我之前提到的盲点所在。“unwrap”和“expect”这两个词本应像“panic”一样成为警示红旗,但不知为何很多人并未意识到其危险性。
针对糟糕的 unwrap 方法家族,我们需要采取多项措施:
部分建议:
– 禁止在生产环境的 Rust 代码中编译通过
– 仅允许在 unsafe 代码块内使用
– 需工程师显式添加 “safe” 注解(尽管此机制可能随时间推移失效)
– 需允许在Cargo中禁止依赖项及传递性依赖项使用unsafe
Rust中
unsafe关键字具有特定含义,而Rust定义中抛出panic并不属于unsafe行为。有时无法避免使用偏函数,而解包操作(或任何你想要命名的方法)正是向编译器提供(运行时检查)证明该函数实际为全函数的一种方式。恐慌应显式触发,而非隐式发生。
unwrap() 应当实质上作为
Result<>工作,用户必须在失败分支中手动触发 panic。若匹配和 panic 操作过于冗余,可设计特殊语法。这类似于无法通过静态手段防范的隐式空指针异常。
我需要一种方法,能从依赖链中静态屏蔽任何采用此行为的库。
unwrap 本身就是显式的。
不确定你所谓“作为
Result<>工作”的含义…unwrap 本就是 Result 的方法。你是否只是主张取消 unwrap/expect 方法?> 人们似乎对 unwrap 存在认知盲区
这不就像人们对 Rust 整体存在认知盲区吗?
这篇文章的核心观点不正是:基础设施故障并非源于根本原因,而是由单个正确的组件组合不当引发的吗?读完《工程构建更安全的世界》后,我认为根本原因分析过于简化论,因为问题不仅在于unwrap操作,更在于有效负载超出常规规模——这源于未按数据库筛选的查询,而ClickHouse又使更多数据库可见。我认为很难简单归咎于“拆包操作”,尤其在探讨未来修复方案时。文章列举了诸多有效思路,远不止“禁止拆包”这一条,例如为功能模块增设全局终止开关,或消除核心转储等错误报告耗尽系统资源的可能性。
> 这便是价值数百万美元的.unwrap()故事。
我认为这种表述过于语义化。真正的故障模式是“强制不变量失效”。若当时编写了明确的请求失败逻辑,最终结果完全相同。
>> 这便是价值数百万美元的.unwrap()故事。
> 我认为这过于纠结语义了。真正的故障模式是“强制不变量失效”。若当时编写了明确的请求失败逻辑,最终结果完全相同。
问题在于,外围函数(
fetch_features)返回的是Result类型,因此第82行的unwrap本质上只是开发者基于“features.append_with_names绝不会失败”的假设而采取的捷径。该例程本应在Result内部处理。> 该例程本应在
Result内部运行。但这属于致命错误。无论隐式还是显式处理,结果都相同。
或许您想表达“显式处理更佳”,作为普遍原则我并不反对。
但这与实际缺陷无关——核心问题在于不变量验证失败。开发者选择在特定语言语义中如何实现不变量检查与失败机制,本无关紧要。
恕我直言,这不过是事后诸葛亮的典型表现。
语义?还是吹毛求疵?
[已删除]
不确定这是否严肃,但若按字面理解:此类机制在Rust中的价值并非完全防止崩溃,而是防止_隐式_失败。它迫使程序员明确选择是否触发崩溃。
许多场景下
unwrap()确实合理。在我们团队,首先会尽量避免使用它(存在许多可规避的模式)。但当无法规避时,我们会留下注释说明其安全依据。语言语义不会显化
unwrap的使用场景,也不作任何保证。其使用应严格限制在unsafe代码块内。> 许多有用的代码中,
unwrap()的使用是合理的。在我的团队中,我们首先尝试避免使用它(并且有许多模式可以做到这一点)。但当无法避免时,我们会留下注释说明其安全原因。我更倾向于使用匹配/if-else/if let 等固定模式来引起注意。若必须显式抛出 panic,更优方案是直接返回错误 Result。
无论工程师多么优秀,糟糕的 unwrap 操作都可能通过重构、业务逻辑变更、先决条件改变、新数据引入等方式悄然渗入。
> 应当限制其仅在 `unsafe` 代码块中使用。
这将大幅扩展Rust中
unsafe的内涵——姑且这么说吧。更何况我认为此举毫无意义:将unwrap()标记为unsafe既无法“暴露unwrap用法”也无法“提供任何保证”,因为安全函数完全可以包含unsafe代码块,而函数签名中对此毫无提示。> 显著扩展 Rust 中 `unsafe` 的含义
我期待的是 panic-free 行为的扩展。虽然受限于内存分配等因素我们永远无法完全实现,但这正是语言设计旨在解决的错误类型。
这最终导致了空指针异常,而这正是Rust旨在消除的问题。
我甚至希望能够静态保证所有依赖库都不使用unwrap()方法。我们应当能够设计出在最大程度上可证明避免恐慌的库。
unwrap()在技术细节上很容易出错。
> 我希望扩展无恐慌行为的边界。
当然,我绝不会反对官方提供保证零恐慌的方法,但将 unwrap() 标记为 `unsafe` 绝非有效途径。
> 但这正是语言旨在修复的错误类型。
是吗? 我完全没看出这里存在内存安全问题。
> 它最终演变成空指针,这正是Rust应消除的错误类型。
这里存在微妙差异——Rust旨在消除空指针解引用导致的 未定义行为 。我认为Rust从未试图消除 恐慌 。某些场景下恐慌仍不可取,但恐慌与不受限制的未定义行为并非等同。
> 我们应当能设计出尽可能避免恐慌的可证明库。
确实如此。但再次强调,将unwrap()标记为
unsafe并非有效方案。据我所知,dtolnay的no_panic是当前最佳方案,另有几款处于实验阶段的证明器工具也能实现类似效果。不过我认为这两者都尚未成熟到足以被官方采纳。
将 unwrap 限制在 unsafe 代码块内反而会降低语言价值。这无法防止 unwrap 错误(当前随意使用该函数的开发者只会改用 “foo = unsafe { bar.unwrap() };” 这种写法)。更会模糊unsafe的初衷——引入与内存安全无关的用法。这绝非良策。
> 更会模糊unsafe的初衷——引入与内存安全无关的用法。
那么我们需要围绕panic行为构建更完善的安全语义。比如添加panic标签或注解,使其影响所有调用。
此外,我需要静态手段确保所有依赖项都不存在此类问题。
归咎语言本身并非解决之道。工程师编写劣质代码是开发者责任,而非语言缺陷。
这段糟糕代码本不该进入生产环境,与Rust语言无关。
不。别说“你用错了”。这语言号称“安全”,它宣传的是安全性。这种情况本不该发生。
这是空指针。在Rust里。
解包操作必须消失。我们都该努力消除它。
> 这语言号称“安全”。它宣称安全。
Rust宣称的是 内存 安全(以及其他密切相关的事物,如无未定义行为、数据竞争安全等)。我认为它并未承诺其他类型安全性的硬性保障。
panic机制是安全的,你在说什么?这和空指针根本不是一回事。
这假设程序在处理格式错误的特性文件时还能执行合理操作。实际情况可能是该文件仅是众多配置文件之一,程序在发现特定配置无效时可通过默认值继续运行。但普遍而言,若程序依赖配置文件且缺失时无法运行,触发panic是正常行为。对于 Nginx 之类的程序,配置文件语法错误时除了显示友好的错误信息外,根本不存在优雅的处理方式。
真正的问题在于更上游环节——错误的特性文件是在缺乏充分检查的情况下被创建并部署的。
> 触发恐慌是正常操作
我认为当大型网络代理内部的机器人检测模型出现配置错误时,不应引发恐慌导致整个代理崩溃,进而拖垮20%的互联网流量。这种系统本应实现优雅失败,但实际未能做到。
> 真正的问题
如此庞大的系统中存在单一的“真正问题”吗?问题其实持续产生(比如不该出现的解包操作、对数据库模式使用者的错误假设),只有当它们叠加时才会显现。
没错,Rust很安全但并非万能。不过Nginx遇到格式错误的配置时不会恐慌,而是退出并附带有用的错误代码和信息。关键在于:Cloudflare代码能否像Nginx那样通过更优雅的退出机制实现可恢复性,而非直接崩溃?
按理说错误信息应满足“提供更具指导性的退出提示”这一标准?但从事后分析来看,他们似乎根本没意识到系统已陷入崩溃状态
我对特性文件分发机制了解有限,但若读取新文件失败,记录错误并沿用旧版本文件岂不是更稳妥?
单个特性出现此类故障时,理应记录错误并采取失败关闭策略。它不该拖垮整个业务前端的大型代理系统。
完全正确!有时彻底崩溃反而是最不坏的选择,这完全是合理做法。
公平地说,在非Rust路径中也出现了同样故障,因为机器人管理模块将所有流量都识别为机器人。不过确实,FL2需要捕获组件级别的恐慌异常,但我不确定“失败开启”模式是否必然更优(本次案例中确实有效,但下次故障很可能正是“失败开启”导致的)。
更普遍地说,可以在FL2层捕获恐慌异常来主动决策——我认为该层逻辑存在缺失。
若系统存在任何不安全代码,捕获panic恐怕并非良策。(跨panic场景下,不安全代码块能否真正维持堆不变量?)
不安全代码块与此无关。没错——它们与安全代码块保持完全相同的不变性,否则那些不安全代码块无论是否发生panic都存在逻辑缺陷。但实现架构的方式有千万种(例如通过监督进程检测FL2层崩溃位置,重启代理时直接禁用该层)。此处存在挑战:需判定何为永久性崩溃(例如仅20%站点异常时如何处理?是否应禁用?)。更普遍的情况是,你仍需决定采用故障开放还是故障关闭策略,这本应通过标注各层来实现。
但更重大的变革在于确保配置变更逐步推进而非一次性生效。这才是99%大规模故障的根源。
增量配置变更听起来可能引发大量 bug
我认为楼主暗示应通过监督进程(Erlang 风格)“捕获”恐慌异常,而非字面意义上使用
catch_unwind在同一进程内恢复。我虽不推崇Rust,但这并非唯一值得关注的问题。所有系统对输入都有预设假设,一旦假设被打破,就必须在某个环节进行捕获。这次似乎在系统过深层被捕获了。
或许验证代码本应处理超大尺寸的情况,但数据库查询本身也返回了无效数据——这种情况本就不该发生。
Swift 支持隐式解包 (!) 和显式解包 (?)。
我不喜欢使用隐式拆包。即使是 绝对存在 的元素,我也坚持显式处理(例如在视图控制器中使用 (self.view?.isEnabled ?? false) 而非 self.view.isEnabled )。
我始终将@IBOutlet重新定义为:
改为:
我算是“双保险”类型的人。
那么如果最终为nil会怎样?你的应用如何响应?
在此特定情况下,我宁愿让应用崩溃。崩溃报告更易于排查,还能获得清晰的堆栈跟踪。
对用户而言,无声失败终究是糟糕的体验。
注:对于可控项,我会严格建模状态,彻底避免强制解包。但面对此类不可控情况,我宁可终止程序,也不愿在无法理解的状态下继续运行。
没错,@IBOutlets通常是唯一允许隐式拆包的可选类型。它们与使用Interface Builder的storyboards及xib文件相关联。我同意,若尝试访问空值的可选项时,程序确实应该直接崩溃。要么是你在初始化及访问UI组件时存在根本性错误,需要在开发阶段捕获这类问题;要么是UIKit/AppKit系统出了极其严重的故障,导致storyboard/xib文件加载异常。
这句话更像是“若无法判断下游故障是否相关,A/B部署毫无意义”——至少我这么理解。
某些语言和编程规范严禁抛出未捕获/未妥善处理的异常。谷歌C++禁止使用异常,主要通过
absl::Status传递错误,调用方必须主动检查状态。虽不熟悉Rust,但其unwrap操作似乎正是会被禁止的类型。甚至为此设计了代码检查规则,但人们常因不耐烦直接覆盖规则,或争论着要取消默认设置。
老生常谈:这是人性的问题,而非技术问题。近年虽取得诸多进步,但人性终究难以改变。
人会犯错
在某个阶段,机器在编程方面会更胜一筹,因为机器代码本质上就是机器指令任务
就像国际象棋,引擎能超越人类特级大师,因为这是可解的数学领域
编程亦是如此
> 就像国际象棋,引擎能超越人类特级大师,因为这是可解的数学领域
值得注意的是,你对国际象棋的描述存在些许偏差。严格来说国际象棋尚未被完全破解——即尚未确定任意棋局下的最优走法;当前棋类引擎主要采用高级暴力搜索策略,其硬件与搜索算法的结合仅实现了超越人脑的计算能力。因此棋类引擎仍可能犯错,尽管实际利用这些错误颇具挑战性。
不存在“最佳走法”和“错误走法”这种绝对概念
“棋类引擎仍可能出错”——恕我直言,这种说法有误
存在不精确性但不构成根本性错误
Unwrap机制的出现,正是为了解决C++中可能引发未定义行为的场景。全面禁止它毫无意义,就像为了防范指针空值而禁止所有解引用操作——即便你刚确认过指针不为空。
Rust的Result等同于C++的std::expected。调用std::expected::value怎么会是未定义行为?
Rust的
foo: Option<&T>大致等同于C++的const T* foo。C++的*foo等效于Rust的unsafe{ *foo.unwrap_unchecked() },或在安全代码中等效于*foo.unwrap()(后者将未定义行为转换为panic)。Rust 的 unwrap 与 std::expected::value 不同。前者会引发 panic——即根据上下文 或 终止程序 或 进行回溯,通常不应被处理。后者仅抛出预期应被处理的异常。panic与异常使用类似机制(至少在特定编译器选项下如此),但二者并非等价——例如析构函数中的嵌套panic始终会终止程序。
在不应发生崩溃的代码中,
unwind应被视为“我保证此情况永不发生”的承诺标记。正如C++中需保证解引用指针有效、加法运算的带符号整数不会溢出,此类承诺是高效编程的必要组成部分。题外话:有趣的是,若无错误发生,调用
std::expected::error反而会触发异常 😀> 不熟悉Rust,但
unwrap这类操作似乎会被禁止。恐慌(panic)并非异常,Rust中的任何“恐慌”都可视为进程终止(Rust二进制文件可显式选择将恐慌实现为进程终止)。Dropbox等公司在其基于Rust的系统中正是如此操作,因此Cloudflare采用相同做法也不足为奇。
“禁止异常”在此毫无意义,你真正需要的是“禁止部分函数”(Haskell意义上的)。
我明白,但 unwrap 本质上不就是一种轻描淡写的方式吗:(1) 放弃捕获被调用方抛出的异常/错误(即
Result<T, E>中的 E 部分);同时 (2) 将其升级到无人能捕获的程度。这个名字听起来多么无害啊。读完文章后这观点实在站不住脚。若要构建基于最大容量硬性假设的预分配系统——采用panic/unwrap策略是合理的。
真正奇怪的是配置漏洞竟能潜入生产环境,且未被立即发现定位。
在协议测试中演练panic场景是合理的。问题在于放弃错误恢复机制——没人会检查跨职责域传播的故障。
好奇优雅处理会怎样?听起来像是性能下降(总比可靠性下降强!)。
另外好奇:采用分片架构为何不采取渐进式变更部署并持续监控?
我未必完全认同。虽然也认为.unwrap()这种编程习惯是漏洞温床,但这个案例未必符合。
根本原因在于文件存在轻微损坏(我猜是重复条目),而其他地方的 验证 检查提示“此文件过大”。
但若这是验证失败,那么失败本身是正确的?真正错误的是失败结果竟进入生产环境。正确做法应该是将验证机制统一化,由生成文件的环节在进入生产环境前就标记异常。
这与函数返回值的API管理无关。本应终止程序的逻辑完全在其他模块,即便在解包爆炸场景下(比如烟雾测试或发布前检查),这种处理也无可厚非。
听起来系统原本是设计了验证机制,但从未考虑过验证可能失败的情况——一旦验证失败,崩溃就成了唯一选择。你本质上是将解析/验证错误转化成了断言错误。
理想情况下 ,每项验证都应具备明确的失败路径。例如配置文件轮换时,新配置验证失败可保留旧配置并记录高优先级错误;面对用户提供的格式错误数据,则可丢弃请求并为安全分析目的进行日志记录。对于“π突然等于4”这类检测,最合理的做法或许是故意崩溃——显然存在 严重 错误,应用状态已遭破坏,任何继续运行的尝试只会加剧问题。
但所有验证失败后的行为背后都应有 合理依据 。当达到某个临界点时,仅依赖“.unwrap()失败时的默认处理”已远远不够。
此外,该系统似乎未采用任何1%/10%/50%/100%分阶段部署机制。若实施此类部署,毒性输入导致任务终止的问题本应轻易暴露。
我理解的情况是:负责生成配置文件的缺陷软件进行了渐进式部署,但这些文件仅在一台机器上生成,每5分钟向整个网络传播一次。
> 只有当查询在已更新的集群部分运行时才会生成错误数据。因此每五分钟就存在生成有效或无效配置文件并快速传播至全网的风险。
非DBA背景,如何实现数据库权限分阶段部署?
> 这正是教科书式的“解析而非验证”反模式。
为何如此?“解析而非验证”指将输入转换为类型化值以防止无效状态的表示,但解析过程仍需正确执行。未经检查的拆包操作与此毫无关联。
通常源于“快速失败”和“彻底失败”原则,理论上关键缺陷应在开发/测试阶段被捕获
若设计让程序员更倾向于偷懒抛出panic而非正确处理错误,那便是糟糕的语言设计
Facebook将某些“逃生舱”函数命名为视觉污染巨兽,例如DANGEROUSLY_CAST_THIS_TO_THAT(危险强制转换)或INVOKE_SUPER_EXPENSIVE_ACTION_SEE_YOU_ON_CODE_REVIEW(调用超级昂贵操作_代码审查见)。这充分说明此类操作除非极端特殊情况,否则绝不可用。
若unwrap()被命名为UNWRAP_OR_PANIC(),人们就不会如此轻率地使用它。更理想的是存在超级严格模式:所有可能引发panic的场景都视为编译时错误,除非明确包裹在may_panic_intentionally!()等特殊机制中。
> 让它们看起来像个巨大的视觉污染
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED 这种警示语立刻浮现脑海。我确实曾不得不使用过它,但它确实能有效避免在示例代码中出现这类内容,也能在阅读其他实现时降低风险显而易见的概率。
后来它被改名为__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,就没那么有趣了。
没错,如果语言设计者将其命名为UNWRAP_OR_PANIC(),人们自然会质疑:为什么不能直接用try-catch包裹代码让开发更轻松?
但panic可以被安全捕获处理(例如通过std::panic工具)。我认为这才是异常的正确用例(问问Martin Fowler就知道了)。
该代码 本身 已包裹在try/catch中,返回Result类型,而你却能不加检查就贸然调用.unwrap()。
正确做法应使用问号运算符:当Result包含错误时,它会立即返回当前函数的错误。这与重新抛出异常完全等效,却只需输入一个字符“?”。
恐怕不行,因为作为值的错误远优于异常。
为何如此?异常本身就是一种值,它被赋予概念上最恰当的处理节点,既能保持“正常流程”的代码简洁性,又能将“异常情况”的处理提升至合理的抽象层级。
使用异常处理能大幅减少冗余代码,因为你无需刻意编写异常行为的处理逻辑——除非确实有必要。而按值返回的方法本质上实现了相同的行为:通过返回操作将异常处理上报至概念上合适的位置,但需要 大量 额外编写代码。两种方式都需要谨慎处理,因为都可能出现异常未正确向上传递的情况(异常不重新抛出,处理后不返回)。
关于这个话题已有大量讨论,但基于两种编程风格的实践经验,异常机制反而让程序员更容易忽视问题。而将错误作为值传递则迫使你必须显式处理,或将其抛回栈中。或许其他语言有更优的异常处理机制,但在Python中简直糟糕透顶。大型项目中你几乎永远无法预知何时何地会出错。
我持相反观点。若不捕获异常,程序将直接终止。
使用返回值时,忽略异常轻而易举。
实际开发中,我见过更多忽略返回错误的情况,因为每次函数调用都需进行类型处理实在太机械化。
这种做法源于数十年的库编写经验。我曾尝试过不使用异常的库实现——这确实是我早年盲目效仿的偏好——但所有库用户普遍忽略错误的现象,促使我如今在任何返回错误值的异常中都强制包含默认值为True的“raise”类型布尔值,以此强制异常及其处理成为默认行为。
> 在大型项目中,你基本无法预知何时或如何会出错。
这与返回值的本质区别何在?观察高阶函数时,你无法知晓其 如何 出错,只能通过错误值向上传递得知其 确实 出错。唯一的区别在于错误传递的 机制 。
这场口水战或许需要加点水来浇灭。;)
我同意我们存在分歧 🙂
异常是隐藏的控制流,而错误值则不是。
这正是zig语言不采用异常机制的核心原因。
我更倾向于将它们归类为“事件处理器”而非“隐藏机制”。在底层你无法预知执行路径,但这正是关键所在:你根本无需关心。只需在需要关注的节点放置处理器即可。
更正:未检查异常才是隐藏的控制流。检查异常具有高度可见性,因此我认为更多语言应采用这种机制。
…但你真的能做到吗?相比各种检查Result的方式,try-catch通常更不直观。
vs
更值得比较的是未处理错误的危险场景:
vs
在前者(未真正尝试捕获的try-catch版本)中,错误处理的缺失是隐性的。这可能没问题!你可能只是依赖调用方使用
try。而在后者中,编译器强制你使用UNWRAP_OR_PANIC(实际上只需解包),否则data将无法获得预期类型,编译会立即失败。我猜你真正想表达的是(因为这更合理):
这种做法虽合理,但现实中真的该让四个独立来源的错误汇聚后,再通过检查
e来逐个排查吗?这固然省事,却也让编译器完全无法察觉那些悄然滋生的隐性问题。unwrap()并非懒惰的代名词,它更像断言机制——调用时你是在宣告Result绝不应失败,若失败则应终止整个流程。问题根源在于开发者的错误假设,而非unwrap()本身的用法。
这种写法还能让代码清晰可见:此处正发生极其危险的操作。作为代码审查者,看到unwrap()就该警铃大作。而在其他语言中,关键错误往往更隐蔽。
我讨厌它被设计成方法。代码审查时,这种操作很容易在方法链中被忽略。
若采用函数或关键字形式,就能中断这种操作链,降低滥用诱惑。
嗯,你可以让Clippy告诉你这些。我在业余项目里就是这么做的。
> 问题出在开发者的假设上,而非unwrap的使用。
你究竟能多少次真正证明
unwrap()是正确的,而且你 还 需要那点性能优势?暂且不提这种操作带来的性能提升往往是侥幸,要证明这种正确性,你必须清楚调用返回值的内部机制。这种认知仅在编写
unwrap()时成立,后续未必有效。更何况,这是否意味着你默许了任何修改函数的人,都要为那些在调用点自作聪明的开发者检查
unwrap操作?这简直荒谬至极。我怀疑这个unwrap并非出于性能考虑而添加;更可能是开发者在处理其他任务时,暂时不想处理他们认为不太可能出现的错误情况;而其他系统未能识别出unwrap被遗留下来,导致它在部署到生产服务器前未被标记。
若我是Cloudflare,会立即审计代码库中所有unwrap用法(或类似rust panic的expect等惯用法),确保其要么被移除,要么明确记录为何值得在此处让程序崩溃,然后在CI系统中添加代码检查工具——一旦有人提交包含unwrap的新代码,立即触发警告。
panic用于处理意外错误,例如调用方传递了无效数据。而返回值用于处理预期错误,例如调用方传递了数据但需要你判断其有效性。
因此 unwrap() 的核心价值并非证明功能正确性。它如同断言机制,揭示了函数实现者无法保证的预设条件。这并非否定 unwrap() 可能被误用,而是强调其本身是代码中合理存在的操作。
需注意:上述讨论均与性能无关。
> 执行unwrap()时,你是在宣告Result绝不应失败
按定义,返回Result意味着方法可能失败。
是的,我认为此处表述有误。更准确的说法应是:“执行unwrap()时,你是在宣告此特定路径上的错误不应可恢复,我们应采取失败安全机制。”
除法可能失败,但我能编写确保除法绝对不会失败的程序。
> 返回Result的定义本身就意味着方法可能失败。
这就像说返回int的定义意味着方法可能返回-2一样荒谬。
> 就像返回整数本身就意味着方法可能返回-2。
什么?返回整数确实意味着方法可能返回-2。我不明白你在此处的论点,因为你看似在反驳对方,实则认同其观点。
什么?Result类型具有有限且明确定义的错误状态。
某些调用点指向返回Result的函数时, 永远 不会返回Error。
某些调用点指向返回int的函数时, 永远 不会返回-2。
有时你知道类型系统不知道的事情。
区别在于:返回Result的函数是主动选择这种返回方式,因为它们可能失败。当前实现或配置下或许不会出错,但未来可能改变,而你可能直到问题发生才察觉。类型系统本就是为辅助而存在——为何要忽视它?
因为要深入库中重写不返回Result的版本极其麻烦。所以你只能忍受类型系统在某些方面存在缺陷。虽然可以预先添加错误处理代码,但此时这些代码会成为死代码,同样不可取。
实际情况更微妙些。开发阶段需要简化错误处理流程,以便先专注于核心逻辑的正确性;但正式部署或发布时,必须严格执行完整检查。采用调试模式与发布模式切换不同静态分析规则的做法似乎更合理。
在Rust中,
.unwrap()需要九个字符,而通过?传递Result只需一个字符。所以…基本上所有语言都是这样?
也许除了Haskell。
还有Gleam
除极少数语言外,所有语言都存在类似unwrap这样的逃生通道
https://en.wikipedia.org/wiki/Crash-only_software
当Erlang系统能优雅处理异常时才有效:报告错误并重启。
> 这导致访问客户网站的互联网用户看到错误页面,提示Cloudflare网络内部故障。
作为随机网页的访问者,我对此深表赞赏——远胜于他们那完全虚假的“正在检查您的连接安全性”提示。
> 该问题并非由任何形式的网络攻击或恶意活动直接或间接引发,而是源于我们某数据库系统权限设置的变更。
同样值得称道的是这份坦诚。
> 2025年11月18日协调世界时11:20(本博客所有时间均为协调世界时),Cloudflare网络开始出现核心网络流量传输严重故障[…]
> 截至14:30,核心流量基本恢复正常。随后数小时内,我们持续缓解网络各节点因流量激增造成的负载压力。截至17:06,Cloudflare所有系统均恢复正常运行。
为何耗时如此之久?我通读全文后理解了故障 成因 ,但当大部分网络瘫痪时,为何不立即回滚近期配置变更——即便看似与故障无关的设置?(或者是我漏读了相关说明?)
当然,事后诸葛亮总是容易的。从故障发生到启动调查仅耗时7分钟确实值得称赞,但后续耗费4小时解决问题,加上恢复全部服务共计8小时的总耗时仍不尽如人意。
因为我们最初以为是攻击。而当我们查明真相后,又缺乏将正确文件注入队列的手段。随后我们不得不重启全球(大量)机器上的进程,才能清空它们的错误文件。
感谢说明!这让我想起去年CrowdStrike的故障:
– 产品依赖频繁配置更新来防御攻击者
– 错误数据文件被推送至生产环境
– 系统无法轻松/自动从错误数据文件中恢复
(不过CrowdStrike的故障严重得多,当时导致整台计算机瘫痪,修复需要人工干预数千台桌面设备;而Cloudflare在故障期间部分功能仍可使用,且问题在数小时内完全解决)
Richard Cook #18(以及#10)再次出手!
https://how.complexsystems.fail/#18
很想了解你们如何 程序化 应对这类事件(或许这只是我近期的执念)。比如你们是否在桌面推演这种场景?团队是否制定了快速解决的运行手册?如何权衡“这需要改变分布式系统的运作机制”与“与其增加复杂性,不如建立流程在故障时快速甚至预判性地将系统该部分恢复到已知良好状态”?
感谢说明!
附带思考(我们正在构建100%链上系统,目标是保障数字资产安全):
公共链(如EVM)可作为防篡改门控机制:仅在满足(a)延迟或多签审核期结束,且(b)简洁证明显示该配置文件满足安全不变量(如≤200个功能、去重、符合X模式等)时,才允许其传播。
这本可在超大文件抵达边缘节点前就阻断其传播 🙂
旁观者提问:为何不设置虚拟/预演小型节点,优先接收特性文件变更并拦截错误以阻止全量生产推送?
或者你们确实有类似机制,但本次数据库权限变更仅在生产环境失败
我认为其背后的逻辑在于文件推送的特性——根据事后分析:
“该特征文件每隔几分钟刷新一次并发布至整个网络,使我们能够应对互联网流量变化。它让我们能快速响应新型机器人及攻击手段。由于恶意行为者战术变化迅速,频繁快速的部署至关重要。”
本例中文件故障发生迅速。仅需尝试加载文件的预测试即可发现问题。几分钟时间完全足以完成此类检测。
纯粹好奇:处理该问题大约需要多少员工参与?是加班处理还是从原定工作中抽调人手?
为何伦敦地区的Warp服务被临时禁用?尽管更新公告中提及此变更,但根本原因分析中却未作说明。
这对伦敦用户造成了更严重的临时影响。
确实,我能想象这次插入操作是项高压任务。
一如既往,值得称赞的是你们能在服务中断后不到24小时就发布事后分析报告——极少数技术公司能做到这一点。
我很好奇他们的内部政策如何运作,竟能如此迅速且透明地发布事后分析。
换作其他大型企业,层层“利益相关方”的审批必然拖慢进程,更别说允许代码公开了。
嗯…我们确实秉持着严肃的透明文化。法学院的三年时光曾让我觉得职业生涯中屡屡荒废,但像今天这样的时刻证明了它的价值。我几乎全程参与了紧急视频会议。事态稳定后又花时间安抚客户,随后才回家。此刻我身处里斯本的欧盟总部。我联系了前CTO、现任董事约翰·格雷厄姆-卡明——我向来钦佩他行文的清晰度。他带着儿子赶来(“要让孩子明白工作不总是有趣的”)。首席法律官道格恰巧在城里,也赶来会合。团队已整理出详尽的技术文档,按时间线逐条记录事件经过。我锁上阳台门,用信赖的BBEdit文本编辑器撰写引言与结论。约翰着手处理技术核心部分,道格则对表述不清处逐条修改。期间约翰点了寿司外卖,但那家店可选菜品有限,而我对海鲜过敏,便点了墨西哥卷饼。团队持续完善事件细节。写作过程中不断涌现疑问:数据库权限变更如何影响查询结果?我们为何要修改权限?这些问题都写进了谷歌文档,很快得到解答。几小时前我们宣布完成。我当着道格、约翰及其儿子的面逐字朗读报告。我们都心怀愧疚——对发生的事感到难堪——但仍确认内容真实准确。我将草稿发给旧金山的米歇尔,技术团队快速审阅后,社交媒体团队将其发布到公司博客。我发短信问约翰是否想发到Reddit黑客新闻版。几分钟后他未回复,我便自行发布。流程就是如此。
> 我发短信问约翰是否要发到HN。几分钟后他未回复,我便自行发布
该死的公司业力耕耘真残酷,仅几分钟服务协议就得接管业力。看来我天生就不适合这种大企业服务协议。
你称这叫透明?却回避了最关键问题:卷饼里装的什么?好吃吗?值得推荐吗?
里斯本Coyo Taco的鸡肉卷饼。我对此并不自豪。这比在Chipotle点餐还糟糕。但里斯本没有Chipotle分店……至少目前没有。
感谢对流程的额外透明化处理。
你们如何处理信息删减?即便由可信人员负责撰写,仍可能存在意外泄露风险,或许需要专门团队审查疏漏。
感谢分享见解。
这回应充满人性且真实可信,令人欣喜。
对招聘工作也大有裨益。
> 他几分钟后仍未回复,于是我接手了
仅凭这点我就考虑申请入职了
毕竟CEO亲自发布复盘报告,说明上层决策者层级并不复杂。至于工程师发布的其他复盘,马修曾说过工程团队自主运营博客,就算他想否决也根本不知道该怎么做[0]
[0] https://news.ycombinator.com/item?id=45588305
发布这篇博客文章和Hacker News帖子的人是马修·普林斯——Cloudflare技术实力雄厚的亿万富翁创始人之一。我敢肯定,只要他想让某件事发生,它就会发生。
据我观察,这取决于你是否是一家“工程型公司”。
而且写得相当出色。相比AWS的事故报告,这简直堪称文学作品。
但它完全没记录解决过程中伦敦Warp团队采取的具体行动。
[已删除]
感觉你的用户名给讨论增添了不少趣味。现在请回家吧。
可以作证:全程未使用大型语言模型。就算想用也做不到。老派作风。对此倒也不完全引以为傲。
有实力的CEO
能做到的人很多。只是多数懒得做。
他们能这么快完成事后分析已让我惊讶,更别说还愿意这么快公之于众。
与此同时,在X平台上,白人至上主义者/格罗伊派/美国优先美国唯一论者正散布阴谋论,声称Cloudflare服务中断是因印度裔员工和H1B签证持有者过多所致。例如:
https://xcancel.com/DiggingInTheDi1/status/19908503102026673…
此类帖子远不止这一则,但令人哭笑不得/毛骨悚然的是,这类观点在所有帖子中获得了数百万点赞。
为何要让这类内容获得更多曝光/传播?
我相信这并非您的本意,因此希望我的评论能促使您反思:无论源自何种平台,传播此类愚蠢言论都会产生怎样的影响。
主要是让大家意识到Cloudflare故障期间发生的事情。当然我可以避免传播这些内容,但它们正在自行扩散蔓延,我认为忽视并不能阻止它们,所以希望提高认知能有所帮助。我注意到针对华裔、印裔及其他背景劳动者的公开种族歧视激增,即便他们持合法签证入境——这些签证是我们国家为自身利益而选择授予的。
玛乔丽·泰勒·格林(MTG)日前提出的全面禁止H1B签证的立法提案,以及要求禁止其他签证类型的呼声,将对科技行业乃至美国整体创新能力造成重大负面影响。社交媒体上的愚蠢言论虽仅限网络,却为现实立法进程及政府可能采取的行动提供了助推力。遗憾的是,许多国会议员正因网络舆论而改变立场。
观点中肯;在提升认知与扩大影响之间确实需要平衡,而我坦白说并不清楚这条界限究竟在哪里。
这与Crowdstrike的混乱事件如出一辙。机器生成的配置文件破坏了使用它的软件。
“该文件随后传播到我们网络中的所有机器…”紧接着“导致软件故障”——这分明在呼唤分阶段部署/回滚机制。我理解“鉴于恶意行为者战术变化迅速,频繁快速部署至关重要”,但今日的故障凸显出快速部署并非全无弊端。
修复方案部分未提及分阶段部署、验收测试及快速回滚等措施是否纳入计划。
我认为该系统不应被视为CI/CD意义上的“部署”;它本质是分布式机器人检测系统的控制通道,(显然)通过发布的配置文件触发(带有consul-template的风格特征,虽不确定是否如此)。
正因如此我将其类比为Crowdstrike。它本质是将签名数据库的消费者搞垮的签名数据库。(你可能看到我编辑中途的版本,回复的正是我后来觉得不妥删掉的讽刺段落。)
编辑:与Crowdstrike类似,该机器人检测器在出现故障时本应回退到最后已知有效的签名数据库,而非持续陷入故障状态。
说得对。
这难道是 consul-template?(我患有 consul-template 后遗症)。
欢迎大家分享对 Consul 的见解。
我认为Consul很棒,尽管我们之前用得有点过火。
https://fly.io/blog/a-foolish-consistency/
https://fly.io/blog/corrosion/
你知道吗:PCTSD影响着五分之二以上的工程师。
他们居然不用任何模拟器就直接把变更推到生产环境,真令人震惊。
我敢肯定读完这个帖子他们就会改变。感觉他们并非不愿改进,而是无力改进?
> 我们已着手强化系统以防范此类故障。具体措施包括:
> 对Cloudflare生成的配置文件实施同等于用户输入的强化处理机制
> 为更多功能启用全局终止开关
> 消除核心转储或其他错误报告耗尽系统资源的可能性
> 审查所有核心代理模块的错误条件故障模式
值得注意的是,上述清单中缺失了金丝雀部署机制,以及跨故障隔离边界进行配置文件的增量/波次部署(其风险往往不亚于代码变更)——前提是Cloudflare确实存在这样的隔离边界。未来他们将如何控制故障影响范围?
去年CrowdStrike事件本应成为行业的警示,但显然我们仍有漫长的改进之路要走。
此外,启用全局 任何功能 (即“为功能启用全局关闭开关”)听起来是个极其危险的主意。试想全局开关存在漏洞,导致禁用某项功能时竟会瘫痪整个系统。
他们要求机器人管理配置能快速更新和传播以应对攻击——但这种情况下,若先更新现有实例本可察觉异常并停止部署。
不解为何在此使用ClickHouse存储功能开关,毕竟它本身就存在数据冗余陷阱[0],同样可能导致查询规模暴增2/3倍。oltp/sqlite似乎更合适,但他们肯定有其考量
[0] https://clickhouse.com/docs/guides/developer/deduplication
我认为SQLite在权限管理和容错性等方面根本达不到他们的要求。它并非万能的数据库解决方案。
另外,你提供的链接是关于存储层最终去重,而非查询时的去重机制。
我认为核心思路是随处部署SQLite数据库。
这个方案并非不可行——既能在持续集成环境中测试特定数据库引擎二进制文件,又(从定义上)避免了单点故障风险。
我认为你过度简化了他们面临的问题,建议深入研读文章细节。问题并非出在数据库本身,而是生成配置文件的查询语句。因此若类似问题出现在针对多个临时复制的sqlite数据库的查询中,故障依然会发生。
sqlite在某些场景确实出色,但它绝非万能的数据库解决方案。
该配置服务采用持续滚动部署模式,但由于消费端服务每隔数分钟自动更新配置,即便少数配置提供端出现故障也会引发连锁影响。如此高频更新显然存在合理需求——推测是为了快速响应威胁行为者。
尽可能快速缓解威胁符合各方利益。但更重要的是,全球核心网络基础设施服务商不应因过快传播错误配置而导致互联网大面积瘫痪。关键在于平衡响应速度与安全性,我不确定他们是否做到了恰到好处。所幸此次影响未如预期般持久而严重。
在该系统语境下,这与其说是“配置”,不如说是“持久化状态”。
三十年可靠性工程经验告诉我,这种区分毫无实质意义。
人们总认为配置更新(或状态更新,随你怎么称呼)天生比代码更新更安全,但历史(乃至当下!)都证明并非如此。然而即便是经验丰富的工程师,也会放任此类变更未经监管就进入生产环境——那些连单行代码都不敢跳过完整CI/CD流程就上线的人,却会对此视若无睹。
他们最终将问题锁定在机器人管理系统中的Rust代码:该代码通过返回错误强制执行配置项数量硬性限制,但调用方却盲目地解包了错误。
代码中潜伏的缺陷往往是此类事故的先决条件。当后续出现异常输入时,漏洞便会显现。这类缺陷可能潜伏数年甚至数十年,甚至可能永远不会暴露。
核心要点在于:将 每次 变更都视为风险源,采用防御性架构设计。
他们犯了分布式系统的经典错误——实际执行了操作。切勿贸然采取行动!
若执意将配置直接推入生产环境,至少应配备充分的缓解机制,包括金丝雀部署和故障隔离边界。这正是本讨论串的核心要义。
也希望fly.io具备这些机制 🙂
我们已详尽阐述过此问题的复杂性。
有相关链接吗?
最近几周刚更新过(但翻阅博客前两页就能找到更多内容):
https://fly.io/blog/corrosion/
你们推进区域化工作很棒。确实困难,但若不以单元化设计为起点,难度将增加百倍。正如我在讨论开头所言,这正说明CloudFlare需要像你们这样持续投入资源。
我对最后那句话感到不适,并非因为我对Cloudflare抱有支持立场,而是过去几年在Fly.io的工作经历,已将理查德·库克的《复杂系统如何崩溃》†深深烙印在我的脑海中。你所言恰恰触犯了库克定律第18条: 零故障运营需要经历故障的洗礼。
倘若Cloudflare再次遭遇 完全相同的 事故,那他们就活该挨批。但此刻我感觉本讨论串的参与者们正在精准、彻底、彻底地重蹈理查德·库克及其追随者竭力告诫人们 不要 犯的错误——将复杂系统故障视为可预测的根本性缺陷,而非构建弹性系统的必经过程。
† https://how.complexsystems.fail/
假设他们今天拥有蜂窝式架构,但其他所有条件完全相同。他们仍会遭遇故障!但这次故障将被 控制 ,损害程度将大大降低。
火灾每日发生。烟雾警报响起,消防员出动,应急响应启动,人们从事件中汲取教训(并据此更新消防与建筑规范)。
尽管如此,整座城市几乎不再被烧毁。而我们希望保持这种状态。
正如库克所言:“安全是系统的特性,而非其组件的特性。”
您指的是哪种细胞架构变体?能否提供相关链接?我对此深感兴趣,曾带领团队将运行在AWS上的单体解决方案拆解为细胞架构。成果虽佳但非奇迹,从失败中学习的过程并未终止,但方式已然转变(且更趋完善)。
无论采用何种架构、流程、软件、框架或系统,无论对所有故障模式进行多么详尽的规划和测试,都无法100%预测所有场景并宣称“细胞架构能解决这个问题”。这包括实现100%故障“可控”——这根本不现实。
若您的AWS服务已实现区域化部署,这便是细胞架构的最低要求。您的服务是否曾遭遇多区域同时故障?
区域内的细胞架构是更高阶的挑战,但只要遵循禁止跨区域耦合的原则,仍可实现:
https://docs.aws.amazon.com/wellarchitected/latest/reducing-…
https://docs.aws.amazon.com/wellarchitected/latest/reducing-…
你根本没认真思考我的观点。链接倒是谢了。
这不值得深究。我不会为自己从未提出过的论点和绝对主张辩护。关键在于缓解风险而非追求完美。
> 若你的AWS服务实现合理区域化部署,这已是细胞架构的最低配置要求
亚马逊曾因错误配置导致多区域故障,因此很难相信你提出的方案能通过多区域部署解决这个具体问题。
仔细想想,今天Cloudflare的故障就是另一个反例。
AWS上一次出现跨区域同步故障已是遥远往事。即便上月us-east-1区域Route 53控制平面功能失效时,只要客户预先配置了故障转移记录、部署了应用程序恢复控制器,或通过Global Accelerator为API/网站提供前端支持,仍能优雅地切换至备用区域。
客户每天都在通过跨区域故障转移应对事件(即使没有AWS区域故障,部署失误等原因也可能导致故障)。你听不到这些案例,恰恰说明机制运转良好。
他这番话简直印证了我的观点(或者说,我才是他的观点)。(我永远不会错过探讨库克主义的机会)。
重新定义问题:关键并非机器人规则传播,而是新客户注册或现有客户新增服务——这类操作在Cloudflare每秒都会发生多次。若仍用“向生产环境推送新配置”的逻辑思考,是否合理?
这些并非我们面临的事实。此外,涉及特定客户或用户的CRUD操作通常不会引发今日这般大规模的事件。
确实不是,这是对你所谓“状态”与“配置”不可区分论点的回应。
全局配置有助于快速响应攻击,但必须建立完善的机制来识别错误的全局配置推送,并能迅速回滚。
在此案例中,旧代理将机器人活动归类为“故障关闭”显然优于“故障崩溃”,但每次全局变更都需经过严格验证才能确保其特性良好。
建立服务配置与版本的下游映射关系,能显著简化全局事件的排查工作——通过清晰呈现变更的因果链条,使调查人员更易追溯问题根源。
问题根源永远是配置推送。人们对代码部署持谨慎态度,却未对配置实施同等管控。但配置即代码,这个盲区导致了过多大规模故障的发生。
“另一个明显迹象是Cloudflare状态页面瘫痪,这让我们误以为可能遭遇攻击。该页面完全托管在Cloudflare基础设施之外,与Cloudflare无任何依赖关系。虽然最终证实是巧合,但导致部分诊断团队成员认为攻击者可能同时针对我们的系统和状态页面。”
遗憾的是他们未说明状态页面同时下线的原因。(这种情况常见吗?否则似乎是极大巧合)
我们不得而知。推测可能是突发流量激增导致底层基础设施扩展失败。
状态页面托管在AWS Cloudfront上对吧?这确实像是Cloudfront被流量激增压垮的情况,有点令人担忧。希望他们能发布说明。
没错,很可能是大量自动化机器人在发现生产环境故障后涌入状态页面进行检测。
这和我们南美工程师今天随机遇到的CloudFront错误很像。我怀疑是AWS出现了小范围故障,但无法证实。
这似乎说明:尽管他们 认为 状态页面完全独立于CloudFront,但如今互联网对CloudFront的依赖程度已足够高,导致他们对状态页面独立性的认知根本错误。
我觉得你把Cloudflare和CloudFront搞混了。
啊哈哈,哎呀。确实是个问题。我手头有两个项目分别依赖其中一个服务,总是分不清它们。
我的意思是,这需要statuspage.io发布事后分析吧?这是Cloudflare运营的服务吗?
Atlasian
很可能是流量过载导致的。
不清楚Atlassian Statuspage的客户群体,但Cloudflare的规模可能远超常规。
大家都吐槽unwrap,但我觉得更奇怪也更有意思的是:居然花了3小时才查明原因?即便存在DDoS误导线索,难道不该有崩溃日志或相关遥测异常吗?此外,后续处理和解决方案是否应更侧重此方面?毕竟这是识别恐慌性故障的高效工具,而非仅仅防止随机怪异边缘案例#9999999的复发?
我虽远未具备管理如此复杂系统的经验,但能感同身受。高压情境下最显而易见的问题反而会被忽略。当有人认定系统X存在故障时,思维会自动跳跃推导出其他系统性能下降皆是其下游影响。因果关系就此颠倒。
有时会议室里会有聪明人深入挖掘找出真相,但你不能总是指望这种情况发生。
我深有体会,经历过太多类似情境。问题不在于“难以置信竟耗时如此之久”(尽管确实有些意外),而在于我不同意Hacker News评论区及博客本身的核心结论——它们过度聚焦于修复罕见边界案例问题(如错误的ClickHouse查询和导致unwrap异常的配置文件),而非通过优化调试与监控体验来缩短所有问题的平均修复时间(MTTR)。
我更怀疑博客中
> 消除核心转储或其他错误报告占用系统资源过多的问题
这一表述背后,实际问题远比叙述所暗示的更为复杂。
他们发现问题后,因缺乏加载新功能文件的机制而陷入困境,不得不另寻解决方案,最终不得不重启所有机器。
这耗费了三个小时中的一个小时;从11:28到13:37才确认配置文件异常是问题根源。
https://x.com/guanlandai/status/1990967570011468071
最令我惊讶的是,这次故障根源排查竟耗时3小时,暴露了平台可观测性中的重大漏洞。即便考虑到服务最初是间歇性故障,在故障持续发生后仍耗费1.5小时才找到根源。但该服务在启动时就已崩溃。核心服务启动时出现这种致命异常,理应触发警报或至少能通过日志聚合轻松定位。或许因误判为攻击导致时间延误,但更令人费解的是无人追问“最近做了哪些变更?”——这通常是我处理事故时的首要问题。
为何Cloudflare允许代码中出现unwrap操作?我本以为他们会用clippy代码检查工具阻止这类情况。既然函数本身已是Result类型,为何不直接匹配{ ok(value) => {}, Err(error) => {} }?
至少他们本可以添加 expect(“这种情况绝不该发生,若出现则说明数据库模式错误”)。
将错误作为值处理的核心意义正是为了防止此类问题…虽然无法阻止此次故障,但至少能大幅简化诊断流程。
若Cloudflare员工看到此文,请务必让我加入代码库维护团队 🙂
我虽非Cloudflare员工,但常写Rust代码。任何涉及网络调用的代码都存在惊人的高风险。开发阶段使用unwrap()很正常,但生产环境中我常保留expect()——因为有时别无选择。
确实,即便没有 unwrap(),也可能存在某些错误处理机制能避免进程崩溃。但若所有请求都转入错误路径,系统仍将陷入瘫痪。
我深有同感,至少 expect() 能提供错误发生时的线索。不过对库开发者而言这可能引发新问题。有时Rust被要求绝不引发panic,尤其在WASM等场景。这对亚马逊Prime视频这类公司构成重大挑战——其电视应用运行于WASM环境,任何panic都会导致崩溃。我个人通常选择创建自定义错误类型(首选方案)或通过Dyn Box Error消除错误(别无选择)。随机的unwrap和expect操作至今仍让我噩梦缠身。
虽然我强烈反对在Rust代码中使用
unwrap和expect,并确保Clippy会提醒我每次使用它们的情况,但我同样理解:若没有这些特性,Rust可能仅被视为一种学术性的奇趣语言。它们是逃生舱。没有这些特性,你的语言就永远无法腾飞。
但关键在于:逃生舱如同紧急出口。团队不该借此溜去附近餐厅吃午饭。
—
Cloudflare或许该投资更完善的代码检查和CI/CD预警系统。更不用说隔离测试——即先将变更部署到小范围环境监测,再进行大规模部署。
事后诸葛亮谁都会,当然。但令我惊讶的是,最近我仅在业余项目中使用Rust,连我都清楚在初始迭代阶段后就不该使用
unwrap和expect。—
我曾多次倡导:当前Rust若能在发布模式下默认禁用这些不安全API,将获益匪浅。虽然我理解他们不愿这么做的原因——恐怕数百万CI/CD管道会一夜之间崩溃。但在过渡期,或许能在
Cargo.toml里添加rustc标志来启用更严格模式?或者让该标志直接在编译时移除所有引发panic的API?不过我认为这可能是项艰巨工程,恐怕永远不会实现(可惜)。无论如何,我预料到Cloudflare会出现许多其他问题,但唯独不该出现_这个_问题。
这种观点在我看来并不合理。unwrap/expect本就是表达代码路径返回Option/Result不可达的惯用方式。
错误或None的向上传递并不能使程序正确。引发恐慌可能是唯一合理的处理方式。
若因系统输入错误导致恐慌不可避免,那么问题出在测试环节。
我同意问题在于测试环节,但你可以在运行时恐慌发生前,在被判定为不可能触发的代码路径中向APM系统发出警报。
我并非在贬低他们——我自己也曾犯过类似错误——只是期望他们做得更好罢了。
你绝对想不到我收到过多少关于“不可能”错误的警报。
确实,当时能做的确实有限。但他们本应投入更多精力提升可视性并加强全面性。说真的,业余Rust开发者似乎做得更好。
这确实让我有些失望。如前所述,我能理解并同情其他许多错误,但这个错误确实让我感到刺痛。
这里确实涉及规范要求,但通常是确保所有线程都具备展开安全(通过AssertUnwindSafe实现),并在进程持续崩溃/经过固定次数重试仍无法启动时记录堆栈跟踪。这样就能立即锁定问题根源。
我只是想反驳“unwrap()不安全”的说法——它本身并无问题,甚至不能称之为“自掘坟墓”。代码按设计执行,当遇到垃圾输入时因无法判断后续操作而崩溃。这种特性在可靠系统中恰恰值得肯定(当然,监控和测试机制才是保障可靠性与可修复性的根本)。
我们观点一致,我的核心论点更具普适性,确实偏离了原话题:
unwrap和expect让许多Rust开发者过于安逸,这两者如同极具诱惑力的情妇。使用它们必须保持极强的自律性。我承认它们确实存在合理用例,但在实际生产环境的Rust代码中,这种情况实属罕见。人们往往急于推进开发,却忘记回头添加完善的错误处理机制。不过本例确实是个例外。但我的核心观点依然成立:在触发panic前,本应针对“不可能”的代码路径发出APM警报。
当然。我甚至认为应该引入类似“main()中所有可达代码路径均需确保解缠绕安全”的静态分析规则——这对许多应用(如一次性命令行工具)而言是 重锤 ,但对负责关键基础设施的长驻守护进程或服务器绝对必要。
我们不该每月都遭遇影响整个互联网的关键性中断。我们的系统架构方式存在系统性缺陷。
> 线程 fl2_worker_thread 发生恐慌:对 Err 值调用了 Result::unwrap()
我不使用 Rust,但许多 Rust 开发者宣称“能编译就行得通”。
Rust 终究无法避免常规编程错误。此处并非指责Cloudflare团队。我热爱Cloudflare及其推出的卓越工具。
归根结底——选择语言应基于技术热情。若你钟情Rust,尽可全天候使用。我其实想尝试将其应用于工业机器人或小型控制器等领域。
没有糟糕的语言,只有我们这些使用者偶尔操作失误。
你误解了Rust的保障机制。Rust从未承诺解决或保护程序员免受逻辑错误或编程缺陷的影响。事实上,没有任何语言能做到这一点,Haskell也不例外。
拆包操作是Rust中极其强大且重要的断言机制——程序员通过它明确声明内部值不会出错,否则触发panic。这是作者与运行时之间的契约。正如你所言,这是人类的失误,而非语言的缺陷。
试想一下,若用C++实现一个全球分布式网络入口代理服务,会是什么样子——其中将存在多少内存漏洞…光是想想就令人不寒而栗…(注:nginx)
这正是经典案例:当系统崩溃时,人们过度关注故障原因,却忽视了借用检查器保障的数千亿次零故障内存访问。
我认为,无论此次Cloudflare故障造成数百万或数千万美元损失,其代价远低于安全内存访问所节省的成本。
参见:https://en.wikipedia.org/wiki/Survivorship_bias
> 试想一下,若用C++实现全球分布式网络入口代理服务会是何种景象——其中可能存在的内存漏洞数量……光是想想就令人不寒而栗
这根本是个无法证伪的论断,实在不公平。
而我们确实存在真实的Rust漏洞,曾让互联网大面积瘫痪数小时。若是C++服务导致此类事故,所有人都会归咎于语言本身,但Rust信徒却总急于将责任推给“不符合惯例的Rust代码”。
允许此类事故发生的语言,本质上就是设计缺陷的产物。
> Rust 无法让你免于常规编程错误。
不同意。Rust 至少会让你在这种情况下产生“确定要执行吗?”的疑虑。调用 unwrap() 本应是警示信号,代码审查者理应要求你解释;若你愿意,甚至可通过静态分析工具完全禁止此操作。
若你执意要写出错误代码,任何语言都无法阻止;若你付出超凡努力,任何语言都能编写出正确代码。但现实往往介于两者之间,而像Rust这样的工具能极大改变小错误演变成大问题的概率。
> 不同意。Rust至少在此处提供了“确定要执行吗?”的确认机制。调用unwrap()本应是警示信号,代码审查者理应要求你解释;若需更严格,可通过静态分析工具完全禁止该操作。
但现实中没人这么做,几乎所有Rust项目都充斥着unwrap操作——即便在Cloudflare这类生产系统中也不例外。
好吧,我避开那些不懂的人。这根本是Rust入门基础。
我参与过的商业代码库表现更好,耸肩。
没错,unwrap()和unsafe就像逃生舱门,必须有充分理由才能使用。对于不在乎崩溃的临时脚本还行,但严肃的生产环境软件要么禁止使用,要么需要极其严格的审查。
人们强调的是:规范的生产级Rust代码不使用unwrap/expect(二者在值的“异常”分支都会引发panic)——而是通过值匹配将问题推给调用链上层处理。
调用栈上层会如何处理?假设用
?将异常向上传播。错误终须被处理。若未引入逻辑处理重复数据库,当类型不匹配时除了unwrap或输出更友好的错误信息外还能做什么?或许可忽略该模块的错误请求,但若涉及比机器人防御更关键的服务,仍会出现500错误的症状。> 调用栈上游会发生什么?
正如帖子所述,这些文件每5分钟生成一次并推送到整个集群。
因此在此场景中,调用栈上游是“监听更新文件并加载”组件。
该组件在收到错误时,可直接继续使用5分钟前加载的现有文件。
同时可增量记录Prometheus指标(或类似指标),用于统计“尝试加载定义文件时的错误次数”。该指标在正常状态下应为零,因此可轻松编写告警规则,通知相关团队定义文件存在某种故障。
这并非完整解决方案——尤其无法解决集群扩容需求,因为新启动的实例不会加载“先前有效的”定义文件。但它能让现有实例优雅降级至受限状态。
根据我的经验,在足够大的系统中,“这种情况绝不可能发生,所以即使发生也允许直接崩溃”的策略,几乎总能被“记录不可能发生事件的实际发生次数”指标及对应的“该指标应为零”告警规则更好地替代。
鉴于系统其他环节存在缺陷(配置文件解析器异常失败),你提出的多数方案缺乏合理性。
系统崩溃应记录日志,并通过堆栈跟踪进行分组(如Prometheus在进程外部监控)。此方案可覆盖各类崩溃场景,包括云端规模常见的内核缺陷和硬件错误。
同样地,通过进程外部的快速重启与退避机制进行缓解,能以更低复杂度覆盖更多故障场景。
你方案遗漏的关键场景是“监控配置文件接口崩溃”——本次故障中若所有服务器突然恢复监控,此类情况很可能发生。
当然,你也可以为此添加错误处理器,同时处理Prometheus响应迟缓等无数其他情况。或者,直接将进程管理和报告功能移出进程。
我在某些旧系统中观察到,它们会始终保留前一版本配置以便回退。其逻辑大致如下:
1. 启动时加载最后已知有效的配置。
2. 接收到配置信号时加载新配置。
3. 当新配置通过验证后,将“已知良好配置”指针更新为新版本。
这种机制基于“过期配置优于服务持续中断”的理论,使崩溃情况可恢复。变体方案还会记录最后尝试的配置版本,确保在配置再次变更前不会尝试解析最新版本。
对于Cloudflare而言,将步骤#3延迟至约5分钟后执行颇具吸引力,这样能捕获那些非即时崩溃的故障。
推测需将错误提升至“若解析新配置失败则保留旧配置”的级别
配置文件子系统才是漏洞所在,而非解包操作的代码,因此此类修改属于“确保解包操作永不失败,再修复API使其不再需要解包”的特殊情况。
没错,这就是我的意思。
> 不过Rust也救不了你免于常见的编程错误
这并非Rust的问题。有人刻意选择不处理错误,可能想着“这种情况永远不会发生”。随后另有人审阅(希望是认真的)包含unwrap()的PR时放任其通过。
> 我不用Rust,但很多Rust开发者都说“能编译就行”。
不过你明白unwrap的问题出在哪儿吗?
规范的Rust代码不会这么写。代码库允许这种写法,更说明了该项目/模块/组件的工程实践问题。无论谁写了那个
unwrap调用,都 必须 意识到它可能引发panic,却依然选择这么做。这属于程序员失误,但Rust至少迫使你承认“好吧,我这儿要犯蠢了”。这种强制性本身具有实际价值。
虽然我认同Rust通过增强显式性做对了,但C/C++中许多错误同样能通过良好的工程实践轻易避免。所谓“C/C++问题主要归咎于编程语言本身”的Rust论调,始终是极不公平的夸大其词。如今面对完全可预见的“.unwrap”灾难(泛指此类场景而非具体案例),所谓“真正的Rust开发者绝不会在生产环境使用unwrap”的谬论,既可悲又可笑。
或许有人质疑——为何使用unsafe Rust?但我们无从知晓原始代码的部署环境,更不清楚PR获批的背景。
可能是紧迫的截止期限、管理层压力,或是偶发的疏忽。
许多人(或许有理)指出 unwrap() 调用是问题根源。这或许成立,但对我而言,更值得深究的是:为何在明确代码行发生的合理“干净”panic,竟未能被任何错误监控系统及时捕捉?
假设使用了类似Sentry的工具,它理应能捕捉到系统开始崩溃时大量进程崩溃的现象。而这种明确界定的干净崩溃,理论上也应在系统崩溃时出现的随机错误中脱颖而出——恰恰因为它总在完全相同的点发生故障。
我将Turnstile与故障容错策略集成,该策略今日经受了实战检验。核心机制是:当浏览器无法加载Turnstile JS(或出现特定前端错误)时,允许用户提交含虚拟验证令牌的表单。后端会正常处理该令牌,若验证Turnstile的siteverify接口时发生错误或超时,则触发故障容错机制。
当然仍有部分用户被拦截——因浏览器未加载Turnstile JS,但后端后续的siteverify验证成功。但总体而言,故障开放机制显著减轻了对客户的影响。
Turnstile的故障开放机制能奏效,是因为我们还部署了其他机器人防御措施,足以在Cloudflare故障时作为后备方案。
所以用户只需阻止脚本加载就能绕过验证码?这种方法确实可行,但仅适用于非定向攻击?
除非攻击者能阻断后端服务器执行的siteverify验证。这并非我们部署Turnstile试图防范的攻击类型。
Cloudflare:
> 另一个明显迹象是Cloudflare状态页面瘫痪,这让我们误以为可能遭遇攻击。该状态页面完全独立于Cloudflare基础设施运行,不依赖其服务。
另据Cloudflare说明:
> Cloudflare控制面板同样受到影响,原因在于内部使用的Workers KV服务及登录流程中部署的Cloudflare Turnstile双重因素。
您似乎将Cloudflare状态页面与控制面板混淆了?二者并非同一事物。
Cloudflare状态页面:https://www.cloudflarestatus.com/
Cloudflare控制台:https://dash.cloudflare.com/
Cloudflare Access仍存在异常问题(要求用户通过单点登录访问我们的公共网站,尽管完全独立区域的区域规则并未变更)。
我认为基础设施尚未如他们所言完全恢复…
近期大量服务中断似乎与自动化配置管理相关。
企业似乎对配置文件的自动推送极为信任,这些文件未经人工审核便直接部署到运行系统中。考虑到配置文件的重要性,是否应当先将其部署到测试/隔离网络进行监控观察,再推送至生产系统?
并非在此高谈阔论——这些系统比我维护过的任何系统都复杂得多。只是试图思考可能适用于所有人的最佳实践。
真正的教训在于:如此多的功能依赖于少数几家企业。这是设计上的根本缺陷,随着赢家通吃的局面加剧,问题正逐年恶化。并非说他们不配成功,但事实摆在眼前——系统缺乏健壮性。不过,倒也无所谓。系统停摆了一阵子罢了。或许我们不该指望互联网永远“在线”。
经典错误组合:
特征表采用转置结构(包含200列feature1、feature2等),导致查询必须通过元查询访问system.columns获取所有特征列,使得查询极易受权限变更影响(尤其在数据库重复场景下)。
类似Crowdstrike的配置更新影响所有节点,却明显未经过任何QA测试或分阶段部署策略(应用程序因新文件立即崩溃的事实基本证明了这点)。
最后,机器人管理配置文件的错误本应禁用机器人管理功能,而非导致核心代理崩溃。
我好奇的是,他们为何选择将此错误归咎于Clickhouse——任何其他数据库都可能引发相同问题。不过我理解,副本更新导致结果反复翻转确实会让事件响应人员非常沮丧。
没错,但这种模式在基于数据库(或任何大型中央数据源)的分布式系统中相当常见;在类似系统中,这可能就是 核心 问题所在。幸运时边界案例显而易见;去年我们遭遇的大规模故障中,数据库新增一行数据触发了if-let/互斥锁死锁,系统便恪尽职守地(且极快地)将该死锁传播至整个网络。
解决该问题的关键并非加强数据库组合测试或优化预发布环境(尽管后来我们确实做了这些改进)。真正的解决方案是:(1) 在代理服务器中部署看门狗系统捕捉任意死锁(该系统后来还发现了其他问题);(2) 将全局变更广播域拆分为区域广播域,使生产环境部署隐式分阶段进行;(3) 建立一套 流程 ,让运维人员能在故障初期快速将系统恢复至已知良好状态。
(Cloudflare的应对方案会与我们不同,我真正想强调的是:所需的改进措施往往无法直接从故障的表面现象推导出来。)
哇。约两小时内产生2600万次5xx HTTP状态码错误。这意味着约1870亿次HTTP错误中断了用户(及系统)的正常访问!
“使用旧版代理引擎FL的客户未见错误提示,但机器人评分生成异常,导致所有流量均被判定为零分。”
这意味着新版本FL2的异常处理能力形同虚设,其代码逻辑远不及FL版本。
但愿这不是AI驱动效率提升的代价。
在多数领域,当逻辑未能完成计算时静默返回0,远比明确报错更糟糕。
人们莫名热衷于贬低Rust。这并非Rust的问题,任何语言都无法避免此类缺陷。事实上编译器本应对此发出警告。
我理解不该因语言流行而盲选,但若说哪家公司的用例与Rust完美契合,那非Cloudflare莫属。
没错,即便不用unwrap()处理这种情况,只要走的是不引发panic的错误路径,但若每个请求都触发错误路径,服务照样会瘫痪。
为何要在返回
Result<_,_>的函数中调用 .unwrap()?对于如此关键的场景,为何不用代码检查工具识别并阻止引发 panic 的代码?这本是 Rust 在该领域最大的优势之一。
可能是因为这种情况更接近断言而非错误检查。
Rust为此提供了调试断言。在预期条件永远不会/不可能发生时,使用expect配合注释说明原因才是符合惯例的做法。
这段代码更像是 append 带名称时返回的错误类型并非 (ErrorFlags, i32),且无法简单转换为该类型,因此有人临时保留了 unwrap 操作,打算“以后再修复”——但谁知道呢。
Fly团队大量使用Rust开发,你们在生产环境允许使用
unwrap()吗?在Modal公司我们仅允许expect(“...”),且错误信息需遵循推荐格式[1]。Cloudflare竟让unwrap代码进入生产环境,导致其六年最严重的服务中断,实在令人震惊。
1. https://doc.rust-lang.org/std/option/enum.Option.html#recomm…
经历2024年重大if-let故障后,我们全面审查了所有代码中的if-let/rwlock问题,修改了大量代码,并立即添加了死锁看门狗机制。代码审查几乎毫无成效;而看门狗机制则切实发挥了作用。
我对Cloudflare的具体情况了解有限,不敢妄下结论(更 绝不敢 像本帖众多Rust专家那样指手画脚),但若置身其境,我会更关注如何确保单个
unwrap错误不会引发稳定的故障模式,而非执着于彻底清除所有unwrap操作。但问题在于,
unwrap这个痛点成了所有程序员的抓手——我们都怀有某种心理自我安慰的本能,总想抓住某个能明确修复的根本原因(或者更刺激多巴胺的,找机会抨击他人)。在类似讨论中我深有体会:若换作是我,本能会回避提及
unwrap调用的细节——会用更模糊的表述——因为深知(我对这个社区有着病态的亲近感)这正是HN用户会激烈的反应模式。讽刺的是,正因没有回避这个话题,Prince的文章反而更出色了。有道理。我也认同直接说“是unwrap的问题”就草草收场是错误的。最近我们在Worker模块上做过一项演练:“假设最恶劣的恐慌性解包发生,确保Worker能正常处理”。
但我强烈认为expect模式是极具价值的控制手段,而裸解包几乎总是表明开发者未能合理评估变更的可靠性。核心代理系统中的解包操作,本质上暴露了变更管理流程(代码审查、静态分析等环节)存在缺陷。
既然如此,或许应该采用expect而非unwrap,以明确说明为何断言绝不应失败。
完全同意,这本应是正确的处理方式。
显然unwrap_or_default()会更合适——若功能获取失败,应以空规则集继续处理而非终止系统。
你的意思是,在C语言发布版本中(其中assert被定义为无操作),这种情况不会发生?
真好奇这些老家伙当初为何选择这种方案。
我正是这些老家伙之一(至少我从90年代就开始发布C代码了),若能选择,我宁愿在生产环境的服务器端代码中保留断言;这总比完全不可预测的错误路径要好。
> 你是说在C语言发布版本中,当断言被定义为空时就不会发生这种情况?
据我所知,只有Go和Java会强制你暂停并显式处理这些异常。
Rust也是,但他们选择在错误条件下触发panic。真疯狂。
> 还有Rust,但他们选择在错误条件下触发panic。真疯狂。
unwrap()不是隐式触发panic吗?
我不认为“隐式panic”是准确描述,因为unwrap()存在的全部意义就是:若解包错误条件则触发panic。使用 unwrap() 时,你是在主动选择触发恐慌行为。
另一种理解方式是:
Result<T, E>类似于 Java 的受检查异常——除非声明处理 E/受检查异常的方案,否则无法获取 T。在此语境下 unwrap() 相当于将受检查异常包装成 RuntimeException 并抛出。没错,生产环境代码中不能使用 .unwrap()(测试环境则无妨)
如同 goto 语句,unwrap 只是有特定适用场景的工具。没必要把它妖魔化。
没错,它本就该用于测试代码。若确信不会失败,请改用.expect()——这样能体现开发者的主动选择,而非疏忽。
胆小鬼才会在生产环境用.expect()
公平地说,若你未达“这个高度”,就不该考虑在C程序中使用goto。毕竟多数人没那么高。
unwrap本身不是问题…
为何追加名称有200个字符限制?
万物皆有界限。你可以主动定义它,或在意外发现时大吃一惊。
此类系统中的限制通常是合理的。它们明确说明了背后的逻辑。问题似乎在于限制的处理方式出了差错,且在审核中被忽略。
故障原因:为发布新功能切换至Chad IDE环境。
赞赏这篇博文的快速发布,文笔精炼且值得称道。
不过我有点困惑——最初为何会误判为攻击行为?
难道内部没有监控5xx错误的触发位置吗?我惊讶于竟没有“此请求在
<机器人检测逻辑>处终止”这类错误映射机制,这本可让你们第一时间排除攻击嫌疑。更意外的是如此关键的场景竟允许使用.unwrap()方法。
期待更多解析!
容我补充:Matthew Prince是Cloudflare的CEO,本职是律师(且为人相当友善)。这份事后分析本身质量上乘,而出自他之手更令人对公司肃然起敬。
Cloudflare的报告清晰明了。一个微小变更引发超预期扩散,他们精准指出了流程失效环节。这再次提醒我们:可靠性不仅取决于基础设施,更依赖于严谨的工作流程。
我不明白为何最初要使用那条SQL查询。它似乎在运行时动态获取功能名称,而非使用静态硬编码的模式。考虑到这决定了全局配置的模式,我认为这种动态性并非良策。
好奇为何他们没有暂时禁用机器人管理功能来恢复系统。相比服务中断本身,网站暂时没有该功能也能维持运行。
6小时/5年计算得出约99.98%的正常运行时间。
今晚心情好,愿意把0.99986四舍五入到99.99%
疑问:用户遇到问题时无法切换DNS绕过服务,为何控制平面与数据平面同步更新?若能临时修改DNS记录,多数用户本可保障业务连续性
等等——当我用C语言或类似语言访问数据库时,若想严格控制内存使用量,我会通过查询语句显式限制行数。
从来不存在不加约束的“从某表选取全部行”操作,除非使用“仅获取前N行”或“limit N”
既然你认为这种设计过于僵化,为何不直接通过查询实现控制?
我漏掉了什么关键点吗?
因为没有强制要求,他们也未曾想到。编写查询代码的人可能认为操作的表永远不超过60行,觉得“这很小”,所以懒得设置限制。而设定文件大小限制的人可能认为“60行数据量不大”,便制定了极小的文件限制,且未与前者协调。
无论使用何种语言构建SQL查询,你都没有义务强制设置最大行数限制。
我认为防范此类问题的手段多种多样,负责优化决策的人本应添加防护措施。在数据层可创建视图,确保从基础表返回的数据不超过200行;在代码中可采用迭代器机制。我虽非Rust专家,但熟悉C语言的防御性编程规范,或许代码审查时他们遗漏了关键环节。
若这种情况发生时能在登录页关闭他们的Turnstile功能就好了,这样我们就能尝试在故障期间将流量绕开Cloudflare。至少应该提供个简单应用程序来修改这个设置。
我本该调侃一句“问题不在于DNS”。
“…波动最终稳定在故障状态。”
看来运维团队今天真是够呛。
若系统变更部署当天就出问题,首要嫌疑对象(无论看似多么不可能)都应是这次变更。
面对未知错误时,我的首要问题是:“最近一次变更是什么?何时部署的?”
这种情况下,他们有充分理由先怀疑是又一次创纪录的DDoS攻击,其次才是配置错误。
我始终无法适应错误出现在调用点而非函数内部的情况——尤其当函数内部发生了Err的提前返回时。这根本不算“更简洁”,因为在调用点你根本无从知晓是哪行代码或哪个文件导致了错误。默认情况下,返回机制本应支持设置标记,以便后续追溯到具体的行号()和文件名()。十多年过去了,这方面的人体工学设计依然缺失。
精彩的事故复盘,条理清晰。意外的是数值统计(恐慌线程数)竟未出现在遥测数据中。
> 我担心这是大型僵尸网络在炫耀实力。
更糟的是——那个掌控一切的小型僵尸网络。
哇。多么精彩的复盘。与其事后诸葛亮地讨论多少种预防方案,我更想听大家畅谈那些意外崩溃的环节。就我而言,完全没想到Cloudflare崩溃后竟连登录Porkbun修改DNS设置都成了不可能任务。
真不幸。我得查查Porkbun是否计划让认证系统脱离对Cloudflare的依赖,否则就得把几个域名从这家注册商迁出去了。
讽刺的是,我刚收到Cloudflare的“错误代码524”页面,因为blog.cloudflare.com已无法访问
虽然探讨导致此次服务中断的技术与流程因素很有必要,但更关键(且相互排斥)的讨论点应是:
为何我们构建/允许构建/订阅了如此无法容忍故障的“网络”?
这里的“我们”指谁?这不是陷阱问题,你认为具体哪些人做错了?我个人不使用Cloudflare,也不运营任何使用它的网站。那些选择将网站托管在Cloudflare后端的人可以停止使用,或许有人会这么做,但他们付费使用该服务,很可能是因为认为(或许确实如此)从中获得了价值。难道应该有某种力量强制他们停止使用Cloudflare吗?
Matt,期待重获Elon及其团队对使用CF的信任。
但愿埃隆能重新赢得我的信任!
这简直是配置更新后出现的未处理错误,和Crowdstrike事件如出一辙——要是他们用Rust这类类型系统更完善的编程语言,这种事根本不会发生。哦对了,他们没用。
我原以为是内部操作失误,以为是员工搞砸了某个文件。老方法有时比新方法更可靠。人工智能又让我们失望了!
希望没人因此被解雇
哇,股价跌得离谱……$NET现在是绝佳买入时机。
赞同。
当前股价水平下Cloudflare非常便宜。
> 实际触发原因是数据库系统权限变更,导致数据库向机器人管理系统使用的“特征文件”输出多条重复记录。
以下是他们使用的查询语句 **(注:实际略有差异):
有人在权限表新增一行后,该连接操作开始为每个独立特征返回两条重复记录。
** “此处为查询语句”仅为强调效果。我对他们使用的数据库类型一无所知,更遑论具体查询语句(不过我大概猜得到)。
后续补充:据帖子后文描述,这是针对ClickHouse元数据表的查询。由于用户被授予了额外数据库的访问权限(该数据库实为日常操作数据库的后端存储),某种行级安全机制导致行数翻倍。不过不明白为何生产环境查询要涉及system.columns表,显得过于动态化。
我记得他们提到过ClickHouse
这简直是连AI审核都通不过的SQL新手错误。他们既未在测试环境验证变更,日志里又充斥着难以定位的关键错误。天啊。
互联网多年已非昔日模样。它最初设计时就具备抗战能力。基于IP的互联网理念本应在网络中断时自动重定向数据包。去中心化曾是核心信条,这与AOL等早期中心化系统截然不同。
如今这一切都消失了。互联网已沦为少数巨头掌控的中心化系统。若AWS瘫痪,半数互联网随之崩溃;若Azure、谷歌云、甲骨文云、腾讯云或阿里云出故障,互联网大部将陷入瘫痪。
昨日Cloudflare故障时,我尝试访问的半数网站都报错。
互联网已死。
但你仍依赖权威DNS服务器吧?
> 数据库系统权限变更导致数据库向机器人管理系统使用的“特征文件”输出多条记录……该系统需实时更新以应对不断变化的威胁
> 软件对特征文件大小的限制低于其两倍容量,导致软件运行失败
配置错误竟能引发互联网级别的服务中断。我们正身处何等时代啊
编辑:读完后不得不表示惊讶,此类错误竟未在测试环境中被发现。若整个错误仅在于“ClickHouse节点迁移过程中,迁移→查询→配置文件的处理流程导致配置文件超出合法大小”,那么在测试环境执行相同迁移操作理应能识别出这个具体错误,不是吗?
我对分布式系统了解有限,或许过于天真,但坦白说看到那段未检查错误值就直接拆包的Rust代码片段,实在令人难以信服!
只有当数据库数据量相当时,错误才可能在测试环境暴露。若测试环境数据量仅为生产环境的一半,此类问题根本不会发生。不太清楚要让测试环境数据库在数据量和相似度等方面完全匹配生产环境究竟有多困难。
我认为很少有公司能让测试环境的存储规模和容量与生产环境完全一致。
> 我认为很少有公司能让测试环境的存储规模和容量与生产环境完全一致。
我们规模仅是Cloudflare的百万分之一,却为所有(或类似)查询部署了自动化测试,用于模拟数据量扩大20倍时的运行状态。
主要用于检测性能退化,但同样适用于发现此类问题。
不过这未必说明这种做法有多罕见,毕竟这也是我首次有机会在公司投入如此精力实现这种测试。
但现在想想Cloudflare这样的规模,仅为构建测试环境就需要多少额外数据——要让测试环境完全复现生产环境,成本至少翻倍。他们还得持续模拟海量请求,毕竟每天可能有数百甚至数千次部署。
在此案例中,涉事数据库表规模看似有限(仅用于机器学习功能),因此直观认为至少能保持测试环境与生产环境功能同步。但他们可能未意识到,当存在特定缺陷时,55行与60行等细微差异都可能成为系统崩溃的临界点。
若数据量未达Cloudflare的处理规模,使用20倍数据进行测试显然更轻松。
这仅意味着测试耗时更长。面对如此规模的数据量,或许无法在合理时间内完成测试,但既然已有10万台服务器每秒处理2500万次请求,临时再启动10万台服务器应该不是什么大问题?
无论如何,无需每次提交代码都进行测试,只需保持足够高的频率,确保在问题进入生产环境前及时发现即可。
Cloudflare发布此次事故报告的速度与透明度令人赞叹。
不过我认为“修复与后续”部分稍显不足,未提及如何在未来广泛部署前,普遍性地捕捉因数据库变更导致的查询结果退化问题。
即便测试环境数据量不足以触发机器人管理系统崩溃的相同故障模式,若能通过测试验证权限变更后查询结果的功能等效性,仍可发现异常。仅含单个http_requests_features列的模拟数据集就足以触发重复结果行为。
理论上存在几种通用检测方式,例如通过前后对比验证数据库权限变更是否导致常用查询结果退化——尤其针对预期不会引发功能行为变化的变更。
或许可通过自动化测试套件实现检测,例如:“创建新数据库,填入精选的玩具数据集,运行必须支持的关键查询集,并在规范化行序列后验证结果是否仍与已知的黄金输出结果等效”。这种回归测试方式存在脆弱性、维护负担重且易出错——尤其当需要进行功能变更并更新“黄金标准”输出时。但它能以较高概率检测到数据库变更导致的查询输出功能退化,且可在开发环境或持续集成阶段发现问题,避免变更进入生产环境。
这个野蛮的
unwrap()也让我有点措手不及。写出这段代码的人真是对自己信心十足啊。:)他们最近才用Rust重写了核心系统(https://blog.cloudflare.com/20-percent-internet-upgrade/)——考虑到系统的新颖性,以及“超过100名工程师参与了FL2开发,拥有130多个模块”这样的宣传语 后续出现类似故障我也不会惊讶。
用Rust重写系统反而导致互联网瘫痪的讽刺意味,我深有体会。
不禁怀疑这次变更是否涉及人工智能。
虽然我不认为CloudFlare属于这种情况,但每次GitHub近期的服务中断或性能问题…哎呀,我都要怪罪那些生锈的机器了!
很高兴问题已修复,继续前进吧!
难以置信这份事后分析的结尾竟是Cloudflare的 广告 。
我们最不需要的就是让 更多 互联网服务商加入Cloudflare。
这符合Cloudflare的利益,而且他们自己写的博客。
这正是变更管理大显身手的时刻——在规范的变更管理流程中,通过回滚机制本可避免此类事故,更不会在未进入质量保证阶段就直接推送至生产环境,更别提事前还需经过同行评审… 我不确定他们是否缺乏变更管理机制,但这确实值得深思
我认为问题在于数据而非代码——某种程度上需要更严格的代码规范和更完善的防护措施; 这就像当代理层仅允许64KB数据时,有人测试发送128KB会触发错误——但若后续有人发送128KB而代理层已变更,应用因超出64KB限制且存在断言检查而崩溃。要真正追踪溢出等错误数据引发的问题,与其依赖代码测试,不如采用模糊测试、暴力测试等手段——我认为这才是关键; 但这意味着我们需要强大的测试网络,且这些网络需更贴近互联网环境以反映真实问题,因此整个测试基础设施本身就难以完善——比如他们需要自建隧道系统等,或许可以隔离部分服务器构建具备更强错误诊断能力的测试系统。但在我看来,若能建立更完善的错误回溯机制,精准定位故障发生点,整体效果将显著提升。当然,这类工作应从测试网络开始实践。这其实是我长期思考的问题——我曾开发过一个简易RPC系统,用于从多个终端服务器实时回传Rust追踪日志(仅需使用常规追踪框架配合轻量RPC层),但主要用于精细化调试。我始终不解为何像systemd-journald这类系统在采用庞杂的万事通方案时,却未更侧重网络化设计——虽然存在dbus支持,但在我看来,它介于调试级代码与警告/信息级日志之间。即便仅记录1/20的日志信息,当大型文件逼近容量上限时,其产生的数据量仍可能激增。若能在运行时实时监测这类现象,并判断其是局部异常还是普遍问题,将有助于构建更具韧性的系统。或许已有类似方案存在,但我尚未发现足够被动的方式——毕竟像dtrace这类调试工具早已存在多年。
单一供应商连续数小时产生28M流量,每秒500次错误。这绝对是新纪录。
历史上从未出现过单一企业承载如此庞大的商业流量。不禁好奇若换作互联网时代之前,类似的故障会引发怎样的连锁反应。
类似大型电信运营商瘫痪的事件,例如1990年AT&T长途电话系统故障:
> 管理层首先尝试的标准程序未能使网络恢复正常运行,在工程师争分夺秒稳定网络的九小时内,近50%的AT&T呼叫未能接通。
> 直到晚上11:30,当网络负载降低到足以使系统稳定时,仅AT&T就因未接通的电话损失了超过6000万美元。
> 航空预订系统、酒店、租车公司及其他依赖电话网络的企业所遭受的损失至今仍无法估算。
https://users.csc.calpoly.edu/~jdalbey/SWE/Papers/att_collap…
> 不知互联网时代之前的类似大停电事件会是什么情形。
许多事件都与天空有关。或许是彗星引发的冰河期…
是的,绝不能把所有(大部分)鸡蛋放在一个篮子里。这正是建立服务的绝佳时机——先检测Cloudflare状态,再将网站DNS切换至Akamai作为备用方案。
就绝对流量而言[1],若以全球数字通信流量的相对百分比衡量,早期电报时代的规模或许更胜一筹。
在数字化之前,东印度公司在任何指标上都碾压其他企业——无论是掌控的商业规模、全球航运量、通信流量、私有军队规模、GDP占比还是雇佣劳动力比例,优势都极为显著。
历史上主流模式始终是大型垄断机构,比如贝尔实验室,再往前追溯还有标准石油公司等等。真正资本主义的红利,我们只享受过短暂的时光。
[1] 不过我怀疑近两年AWS或MS/Azure的停机率可能更高
> 历史上从未出现过单一企业承担如此庞大商业与流量责任的局面。
AWS在商业责任方面很可能已超越Cloudflare。亚马逊单体规模约占美国GDP的2.3%。
没有坏的宣传。
这是近期读过最精彩的事故分析,未来数年都将被研究。
颇具讽刺意味的是,他们内部的FL2工具本应让Cloudflare“更快更安全”,却导致大量系统瘫痪。正如其他人指出的,这种Rust用法极其危险,根本不该进入生产环境。
又漫长的一天…
但但但Rust明明能让一切更安全啊!!!!
兄弟,任何工具用得不对都会变成自杀武器
写得真棒。
这是首次涉及Rust代码的重大故障,正如你所见,.unwrap操作存在引发panic的风险,绝不该用于生产环境代码。
我能容忍很多失误,但这次实在不可原谅
我认为你们应该补偿我因这次故障造成的所有收入损失。谁批准在客户年收入高峰期修改核心基础设施?说真的,这是最高决策层的管理失职。我们从不在年度最繁忙时段调整服务器架构/技术栈,你们也不该这么做。若有替代方案,我立刻终止服务迁移系统。
我认为你们必须严格履行合同承诺。停机在所有基础设施中都可能发生,无论是计划内还是计划外。SLA和SLO条款正是对此事实的明确认可,也是合同不可或缺的部分。
对他们的决策感到不满是合理的——利用这点重新谈判合同条款。
除上述引述外,此处补充更多背景信息:ClickHouse权限变更导致元数据查询开始从额外模式返回重复列元数据,致使Bot管理配置文件的大小和特征数量翻倍以上。当这个超大功能文件部署到边缘代理时,它突破了机器人模块200个功能的限制,导致该模块崩溃,核心代理全局返回5xx错误。
综上所述:
从软件工程角度预防此类问题时,他们对数据库查询机制做出了错误假设(且未验证结果),同时忽视了自身应用程序的限制条件。他们既未编写检测输入是否触发限制的程序,也未设置警报机制来通知工程师问题根源。
从运维角度看,他们似乎未在模拟生产环境的非生产系统上进行测试;部署过程缺乏渐进式推进机制;当新部署的应用程序开始崩溃时,系统既没有断路器机制停止部署,也没有回滚方案。
人们总急于质问“回滚机制去哪了”,这种质疑固然合理,但需谨记:基于推测设计的回滚功能(即在系统真实错误模式出现前构建的回滚机制)本身就可能成为分布式系统中偶发亚稳态故障的根源。这一切都绝非易事。
最基础的测试呢?比如检查配置文件在应用中能否正常运行?那是个硬编码的内存限制;用MacBook运行git钩子测试套件就能发现问题。可他们偏不——居然不愿在部署前用这个配置运行0.01秒的应用来决定互联网的命运?
这简直是CrowdStrike漏洞在CDN中的翻版。这是你能想到最基础、最初级的零日测试。别提他们搞砸的其他事了——应用程序因配置文件崩溃,居然没人评估?!并非所有漏洞都能预防,但如此严重的测试缺失绝对可以避免。
这正是软件建设规范(如同电气规范中的UL认证,防止未经测试的电气元件烧毁房屋)旨在防范的风险。任何关键基础设施未经测试就投入使用,都绝不应被允许,句号。
看来你正拥有绝佳时机,用更优产品颠覆他们的市场。
就在这次故障前,我正在研究bunnycdn——毕竟Cloudflare掌控DNS的想法仍让我有些介怀。市场存在竞争者,但Cloudflare的规模优势确实能提升整体性能。不过过去大量测试中,我发现Cloudflare的性能表现糟糕透顶。其核心架构本质上是基于拉取而非推送的系统,因此当内容未及时更新时,缓存未命中时的性能表现相当糟糕。虽然他们的骨干回程路径已有所改善,但至少从新西兰访问时,其性能似乎仍逊于直接连接洛杉矶代理再转发至源服务器的方案。(不过谷歌此前也存在类似情况:无论是8.8.8.8还是www.google.co.nz/.com,经洛杉矶路径访问都比常规路径更快——我认为谷歌当时采用亚洲母节点策略,比如测试8.8.8.8时若缓存未命中,数据传输距离会异常遥远)。不过现在有了HTTP/3等技术,性能优化似乎更容易实现,而DDoS防护和机器人拦截反而成了核心竞争力。我认为Cloudflare的机器人防护机制总体上应该相当有效?