如果 Cloudflare 不使用 Rust,就不會當機了,是這樣嗎?我看未必

在半個月前,我才剛開玩笑的和一位大神說:「在 Rust 裡面你偷懶,edge case 就會用 panic 懲罰你」,沒想到半個月後居然就親身碰上了。

台灣時間 2025 年 11 月 18 日晚上 7 點初(官方紀錄為 5 分)開始,許多網站開始陸續出現 Cloudflare 機器人驗證失敗,網站回應緩慢,最後甚至出現了根本無法存取的情況。剛好當時我也因此受害(網站機器人檢查瘋狂轉圈圈)所以對時間點特別有印象。新聞也動的很快,看來臺灣 Yahoo! 沒有用到 Cloudflare 的第二代核心代理(FL 2):https://tw.news.yahoo.com/share/ac41d1e8-9bb2-4fcd-9aa1-5d1e422abe65

那隔天起床,事件已經解決,之後官方也發布事件報告,事情乍看是要告一段落了。不過我看到不僅是 reddit 的 progammerHumor 上出現嘲諷 Rust 的貼文:https://redd.it/1p0zjw4https://redd.it/1p0xgfp ,甚至在一些非程式設計方面的社群也看到有人轉貼類似的言論,都是把這次當機原因推到 Rust 身上(不過 reddit 上也有不少類似「既然 Cloudflare 都在生產環境上使用 unwrap() 了,我也就不擔心我的 Rust 程式碼了。」等的搞笑留言,害我不小心笑了),因此讓我覺得有必要寫篇文刷刷存在感,告訴大家這次真的不是 Rust 的問題。

事件簡易說明

官方部落格原文:https://blog.cloudflare.com/18-november-2025-outage/

話說第一時間其實只有英文和簡體中文,我是看英文的,沒想到今天看居然出現繁體中文,也一併附上給大家參考:https://blog.cloudflare.com/zh-tw/18-november-2025-outage/

背景說明

  • Cloudflare 是 CDN、WAF 等的中間角色,也就是在 client 和 server 之間的「傳話兼審核員」
  • Cloudflare 有個 Bot Management,檢查流量是否為機器人,判斷依據「特徵」,可以想像成防毒軟體的病毒定義檔,CF 會依照特徵去檢測流量決定一個機器人分數(bot scores)決定是否放行流量。
  • Bot Management 特徵更新頻率約為幾分鐘

    This feature file is refreshed every few minutes

  • Cloudflare 為了效能考量,預設特徵檔案大小不會大幅變動,因此預先分配記憶體位置(memory preallocation)。
  • Cloudflare 的 core proxy──Frontline,簡稱 FL,使用 Nginx、LuaJIT(早期還有 PHP)實作;第二代稱為 FL2,使用 Rust 實作。

outage

  1. 更新特徵的系統帳號權限變更,原本只能看到 default 的系統帳號,現在可以看到其他的資料庫(r0

  2. 權限變更完成,select 的資料至少翻倍(default + r0

  3. 加上前述背景說明的預先記憶體分配,翻倍的特徵資料超出上限放不進預先分配好的記憶體位置,但程式設計師直接 unwrap() 所以 Rust 恐慌(panic)

    1
    thread fl2_worker_thread panicked: called Result::unwrap() on an Err value

個人觀點

Rust 相當重視早期揪錯,才會用各種方式強迫程式設計師進行各種處理。以這次的案例來說,append_with_names() 會回傳 Result,其中有兩種可能:Ok()Err(),分別代表成功和失敗兩種情況。Rust 會強迫程式設計師必須分別處理兩種情況,否則無法編譯。聽起來很棒,那為什麼這次還會爆炸?原因在於人都是懶惰的……不是,我是說編譯器保留彈性,畢竟有時有可能你可以保證絕對不會出錯,何必浪費時間去處理不存在的 Err()?因此有一個方法──也是本次主角──就是 unwrap()。這個方法想必學過 Rust 的人 100% 都用過,因為大家都想偷懶。其用途在於,如果 ResultOk(T),則回傳 Ok() 中的值 T;如果是 Err(),則引發 panic。

看出來了嗎?整個事件的問題點就在於,程式設計師太過相信內部服務不會捅你一刀,殊不知凡事都有意外(就像交往多年的女友某天突然說要分手一樣)。這次好死不死改個權限,誰也沒想到會導致查詢出來的資料翻倍(或是剛好沒檢查到)然後導致超出預先分配好的記憶體空間,加上前面說得太相信同事,結果就爆了。就算今天你換成其他程式語言結果也是壞的,做好例外處理才是整件事最大的重點(整合測試可能也能揪出問題)用什麼程式語言根本不重要。

Rust 社群我也有看到幾個關於這次事件不同的解決方案,像是「分配失敗就跳過這次更新」、「截斷多餘的特徵檔案部分」、使用 ? 或是 Anyhow 包一層等等。那因為我比較菜,問我的話可能是會使用正統 match 然後在錯誤區塊 log 或 alert 的方式:

1
2
3
4
5
6
7
8
9
// 示意用簡化版
match (feature_values.append_with_names(/* param */)) {
Ok(fv) => fv,
Err(e) => {
// log
// send alert
// skip
}
}

我在一般人的社群中看到有人轉發的觀點說,第一代不受影響,因為 failing gracefully,不要使用 Rust 就沒事了。的確,如果慈悲失敗今天 Cloudflare 就不會服務中斷,只是流量的機器人分數都是 0 分而已(實際上這就是第一代 FL 當時的情況)。

聽起來很合理對吧?但實際上問題點根本不在 Rust 或是預先分配記憶體。想像一下如果今天這個元件是自動駕駛的煞車系統,分數是代表需要剎車的可能或力道,你的慈悲失敗會導致多少人死傷?

不是不能 failing gracefully,而是要看情況。《The Rust Programming Language》有一個章節在告訴你,什麼時候要讓程式 panic,什麼時候不該。有興趣的人可以移步 Rust 台灣社群對此書貢獻的正體中文翻譯版:https://rust-lang.tw/book-tw/ch09-03-to-panic-or-not-to-panic.html

最後,即便是程式設計師也是人,人都無法避免犯錯。雖然這起事件我認為程式設計師責無旁貸,但最重要的還是要從事件中記取教訓。Cloudflare 重大障礙發生後都會有詳細說明,甚至還會有程式碼。這次事件也一如既往地貼出出錯的部分,讓我們可以從中一起學習。

啊,這次是真的最後了,我英文很爛,也沒有對 Rust 特別熟練,只是基本的看得懂而已。如果這次事件分析或 Rust 部分有錯誤懇請各方斧正,或是你有更好的方法也歡迎分享給大家。希望不管你是寫什麼程式語言,都要好好的處理例外和做該做的 log,至少出事的時候可以更快定位問題並處理(已經不知道被搞多少次了哀)。

圖片使用公眾領域的 https://rustacean.net/ 素材作為基底,交由 Google Gemini 加工,最後加上一點我自己的後製修正。