在2025年9月,網際網路最關鍵的基礎設施公司之一經歷了一次故障,這不是由複雜的駭客、大規模DDoS攻擊或伺服器故障引起的。相反,Cloudflare——這家經常抵禦太位元級攻擊並維持數百萬網站運行的公司——被一個許多初級開發者在第一週就學會避免的基本React程式設計錯誤搞垮了。
令人震驚的現實
諷刺幾乎是喜劇性的:Cloudflare成功抵禦了每秒7.3太位元的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"
}
}
關鍵的學習時刻
這個事件有力地提醒我們,基本的程式設計錯誤可以產生巨大的全球影響。這次故障影響了全球數百萬網站和無數企業,全都因為一個本可以通過適當工具和流程捕捉的錯誤。
正如ThePrimeagen,一位知名且著名的科技YouTuber,對這個事件評論說:「我們都犯過這個錯誤——我只是慶幸我在開發中發現了我的,而不是在生產中。」
關鍵要點
- 基礎知識很重要 — 即使在企業規模,基本的程式設計原則也是關鍵的
- 工具是必不可少的 — ESLint、React DevTools和適當的監控可以防止這些問題
- 流程失敗會放大技術失敗 — 多個安全網同時失效
- 漸進式部署拯救生命 — 即時全球發布對關鍵基礎設施來說很危險
- React的力量需要責任 — 框架的靈活性要求有紀律的開發實踐
展望未來
Cloudflare此後已實施:
- Argo Rollouts用於自動部署回滾
- 增強的API使用模式監控
- 更好的速率限制和斷路器模式
- 改進的前端變更程式碼審查流程
這個事件有力地提醒我們,在這個互聯的世界中,即使是最小的程式碼變更也可能產生巨大的全球影響。無論你是在建立簡單網站還是管理關鍵基礎設施,理解React基礎知識和實施適當的工程流程不僅僅是好的實踐——它對網際網路穩定性至關重要。
下次你寫useEffect hook時,記住:Cloudflare的工程師們可能也在仔細檢查他們的依賴陣列。