2025年9月,互联网最关键的基础设施公司之一经历了一次故障,这次故障不是由复杂的黑客、大规模DDoS攻击或服务器故障引起的。相反,Cloudflare——这家常规抵御TB级攻击并保持数百万网站在线的公司——被一个许多初级开发者在第一周就学会避免的基本React编程错误击垮了。
令人震惊的现实
讽刺几乎是喜剧性的:Cloudflare成功防御了每秒7.3 TB的DDoS攻击,却被一个带有不当依赖数组的React useEffect Hook搞垮了。这不是什么国家级黑客精英小组——这是一个典型的前端开发反模式,创建了API调用的无限循环。
来源:本文引用的技术细节和时间线基于Cloudflare的官方事故报告:“深入分析Cloudflare 2025年9月12日仪表板和API故障”
理解技术错误
有问题的代码模式
问题源于这个常见的React模式:
// 有问题代码的简化版本
function Dashboard() {
const params = { // 这个对象在每次渲染时都会重新创建
organizationId: user.orgId,
filters: currentFilters
};
useEffect(() => {
// 这个函数调用API
fetchDashboardData(params);
}, [params]); // ← 问题在这里
return "Dashboard rendered";
}
为什么这会创建无限循环
useEffect Hook使用浅比较来比较依赖项。以下是发生的情况:
- 组件渲染 → 创建新的
params对象 - useEffect运行 → 获取数据,可能更新状态
- 状态更新触发重新渲染 → 创建另一个新的
params对象 - React比较依赖项 →
params引用已更改 - useEffect再次运行 → 返回第2步,创建无限循环
为什么params对象每次都会重新创建?
在JavaScript中,像{ organizationId: user.orgId, filters: currentFilters }这样的对象字面量每次执行时都会在内存中创建一个新对象。即使里面的值相同,对象引用也是不同的。
// 每次这个函数运行时,都会创建一个新对象
const params = { organizationId: user.orgId, filters: currentFilters };
// 这等同于:
const params = new Object();
params.organizationId = user.orgId;
params.filters = currentFilters;
内存引用比较:
// 这些对象内容相同但内存引用不同
const obj1 = { name: "John" };
const obj2 = { name: "John" };
console.log(obj1 === obj2); // false - 不同的内存位置
// 只有相同引用才返回true
const obj3 = obj1;
console.log(obj1 === obj3); // true - 相同的内存引用
React的useEffect使用Object.is()(类似于===)来比较依赖项。由于每次渲染都会创建新的params对象,React认为依赖项已更改,即使里面的值可能相同。
即使params对象包含相同的值,React也认为它是”不同的”,因为它每次都是内存中的新对象引用。
问题的规模
根据Cloudflare的事故报告,这个简单的错误导致了:
- 单个仪表板会话每分钟数千次API调用
- 当乘以所有用户时API完全过载
- 整个基础设施的级联故障
- 影响数百万网站的全球故障
正确的解决方案
解决方案1:记忆化依赖项
import { useMemo, useEffect } from 'react';
function Dashboard() {
const params = useMemo(() => ({
organizationId: user.orgId,
filters: currentFilters
}), [user.orgId, currentFilters]); // 只有当这些改变时才重新创建
useEffect(() => {
fetchDashboardData(params);
}, [params]);
return "Dashboard rendered";
}
解决方案2:分离依赖项
function Dashboard() {
useEffect(() => {
const params = {
organizationId: user.orgId,
filters: currentFilters
};
fetchDashboardData(params);
}, [user.orgId, currentFilters]); // 直接依赖项
return "Dashboard rendered";
}
解决方案3:对函数使用useCallback
import { useCallback, useEffect } from 'react';
function Dashboard() {
const fetchData = useCallback(async () => {
const params = {
organizationId: user.orgId,
filters: currentFilters
};
await fetchDashboardData(params);
}, [user.orgId, currentFilters]);
useEffect(() => {
fetchData();
}, [fetchData]);
return "Dashboard rendered";
}
更广泛的工程教训
1. 代码审查失败
这个明显的无限循环是如何进入生产环境的?这次事故突显了几个工程流程失败:
- 本地开发测试应该立即显示数千个网络请求
- 代码审查流程应该捕获基本的React反模式
- 预发布环境应该复制生产负载模式
- 监控系统应该对异常API使用模式发出警报
2. 惊群问题
当Cloudflare试图通过清除用户会话来修复问题时,他们无意中制造了”惊群”问题——当服务恢复时数百万用户同时重新认证,导致第二次故障。
3. 速率限制和断路器
这次事故揭示了Cloudflare的内部API缺乏适当的:
- 速率限制以防止滥用
- 断路器以在负载下优雅失败
- 自动回滚机制用于有问题的部署
预防策略
对于开发者
- 使用ESLint规则如
exhaustive-deps来捕获依赖问题 - 安装React Developer Tools来监控组件重新渲染
- 添加网络监控在开发期间捕获异常API模式
- 实践防御性编程使用适当的错误边界
对于工程团队
- 实施渐进式发布而不是即时全球部署
- 建立适当的监控以监测API使用模式
- 制定代码审查清单以检查常见的React反模式
- 创建负载测试环境以模拟真实使用情况
必要的ESLint配置
{
"extends": ["plugin:react-hooks/recommended"],
"rules": {
"react-hooks/exhaustive-deps": "error"
}
}
关键学习时刻
这次事故是一个有力的提醒,说明基本的编程错误如何产生巨大的全球影响。这次故障影响了全球数百万网站和无数企业,而这一切都源于一个用适当的工具和流程就能捕获的错误。
正如著名的科技YouTuber ThePrimeagen对这次事故的评论:“我们都犯过这个错误——我只是很高兴我在开发中而不是生产中发现了我的错误。“
关键要点
- 基础知识很重要 — 即使在企业规模,基本的编程原则也至关重要
- 工具是必不可少的 — ESLint、React DevTools和适当的监控可以防止这些问题
- 流程失败会放大技术失败 — 多个安全网同时失效
- 渐进式部署可以救命 — 即时全球发布对关键基础设施来说是危险的
- React的强大需要责任 — 框架的灵活性需要有纪律的开发实践
展望未来
Cloudflare此后已实施:
- 用于自动部署回滚的Argo Rollouts
- 增强的API使用模式监控
- 更好的速率限制和断路器模式
- 改进的前端更改代码审查流程
这次事故是一个有力的提醒:在我们互联的世界中,即使是最小的代码更改也可能产生巨大的全球影响。无论你是构建简单的网站还是管理关键基础设施,理解React基础知识和实施适当的工程流程不仅仅是良好实践——它对互联网稳定性至关重要。
下次你写useEffect Hook时,请记住:Cloudflare的工程师们可能也在仔细检查他们的依赖数组。