2024 年 6 月 20 日(星期四),兩起單獨的事件導致網際網路內容和 Cloudflare 服務的延遲和錯誤率增加,持續了 114 分鐘。在影響達到峰值的 30 分鐘內,我們發現 1.4% 到 2.1% 的 CDN HTTP 請求收到了通用錯誤頁面,並且觀察到第 99 百分位元的第一個位元組接收時間 (TTFB) 延遲增加了 3 倍。
發生這些事件的原因如下:
自動網路監控偵測到效能下降,以次優方式重新路由流量,並在世界標準時間 17:33 至 17:50 之間造成骨幹擁塞
世界標準時間 14:14 至 17:06 之間部署的新型分散式拒絕服務 (DDoS) 緩解機制觸發了我們的限速系統中的一個潛在錯誤,該錯誤允許特定形式的 HTTP 請求導致其處理程序在世界標準時間 17:47 至 19:27 之間進入無限迴圈
全球許多 Cloudflare 資料中心都受到了這些事件的影響。
對於骨幹擁塞事件,我們已經開始著手擴大受影響資料中心的骨幹容量,並改進我們的網路緩解措施,以便在採取動作時使用有關替代網路路徑上可用容量的更多資訊。在這篇部落格文章的其餘部分,我們將更詳細地介紹這些事件中影響更大的第二起事件。
作為我們保護機制例行更新的一部分,我們建立了一條新的 DDoS 規則,以阻止我們在基礎結構上觀察到的特定類型的濫用。這條 DDoS 規則按預期工作,但在特定的可疑流量案例中,它暴露了我們現有限速元件中的潛在錯誤。需要明確的是,我們沒有理由相信這個可疑流量是故意利用這個錯誤,也沒有證據表明存在任何類型的違規行為。
我們對所造成的影響深表歉意,並已進行變更以防止這些問題再次發生。
背景
對可疑流量進行限速
根據 HTTP 請求的設定檔和所請求的網際網路內容的設定,Cloudflare 可能會透過限制訪客在一定時間範圍內可以發出的請求數量來保護我們的網路和客戶的來源。這些速率限制可以透過客戶設定啟用,也可以回應偵測到可疑活動的 DDoS 規則而啟動。
通常,這些速率限制將根據訪客的 IP 位址套用。由於許多機構和網際網路服務提供者 (ISP) 可能讓許多裝置和個人使用者具有單一 IP 位址,因此基於 IP 位址的限速是一種大範圍的做法,可能會無意中封鎖合法流量。
平衡我們網路中的流量
Cloudflare 有多個系統共同提供持續的即時容量監控和重新平衡,以確保我們盡可能快速、高效地服務盡可能多的流量。
第一個是 Unimog,Cloudflare 的邊緣負載平衡器。到達我們 Anycast 網路的每個封包都會經過 Unimog,Unimog 會將其傳送到適當的伺服器來處理該封包。該伺服器的位置可能與封包最初到達我們網路中的位置並不相同,具體取決於運算能力的可用性。在每個資料中心內,Unimog 旨在使所有活躍伺服器的 CPU 負載保持均勻。
為了全面瞭解我們的網路,我們依賴於 Traffic Manager。在我們所有的資料中心位置,它會接收各種訊號,例如總體 CPU 使用率、HTTP 請求延遲和頻寬使用率,以指導重新平衡決策。它具有內建安全限制,可防止造成過大的流量轉移,並且在做出任何決策時還會考慮目標位置的預期負載。
事件時間表和影響
所有時間戳記均為 2024 年 6 月 20 日的世界標準時間。
14:14 DDoS 規則開始逐步部署
17:06 DDoS 規則在全球部署
17:47 第一個 HTTP 要求處理程序中毒
18:04 根據偵測到的高 CPU 負載自動聲明發生事件
18:34 在一台伺服器上進行服務重新啟動表明可以復原服務,在一個資料中心內測試完全重啟
18:44 服務重新啟動後資料中心的 CPU 負載恢復正常
18:51 開始對具有許多卡住程序的所有伺服器進行持續的全域重新載入
19:05 全球觀測點 HTTP 錯誤率峰值為 2.1% 服務不可用/總計 3.45%
19:05 Traffic Manager 首先採取動作復原服務
19:11 全球觀測點 HTTP 錯誤率減半至 1% 服務不可用/總計 1.96%
19:27 全球觀測點 HTTP 錯誤率降至基準線水準
19:29 認定 DDoS 規則部署可能是發生程序中毒的原因
19:34 DDoS 規則完全停用
19:43 工程師停止在有許多卡住程序的伺服器上例行重新啟動服務
20:16 事件回應停止
下面,我們從 Cloudflare 的一些內部指標來看一下影響。第一張圖表顯示了由於服務中毒導致無法存取而產生錯誤回應的所有觀測點(從外部裝置輸入)HTTP 請求的百分比。我們看到請求數最初增加到 0.5%,然後增加到 2.1%,之後由於服務重新載入而開始復原。
為了更全面地瞭解錯誤,我們可以看到在同一時間段內我們的網路傳回的所有 5xx 回應,包括來自來源伺服器的回應。這些回應達到 3.45% 的峰值,您可以更清楚地看到,在世界標準時間 19:25 到 20:00 之間,隨著 Traffic Manager 完成其重新路由活動,錯誤率逐漸恢復。世界標準時間 19:25 的下降與最後一次大規模重新載入一致,之後的錯誤增加主要源自上游 DNS 逾時和連線限制,這與高負載和不平衡負載一致。
以下是我們在第 50、90 和 99 百分位的 TTFB 測量結果,顯示第 99 百分位的延遲增加了近 3 倍:
錯誤的技術說明及其發生的原因
事件期間使用過多 CPU 的 HTTP 請求處理程序的全球百分比
早些時候,即世界標準時間 6 月 20 日 14:14 - 17:06 期間,我們在網路上逐步啟用了一條新的 DDoS 規則。Cloudflare 最近一直在構建一種緩解 HTTP DDoS 攻擊的新方法。此方法結合使用速率限制和 cookie,以允許被錯誤識別為攻擊部分的合法用戶端繼續執行。
使用這種新方法,被視為可疑的 HTTP 請求將經歷以下關鍵步驟:
檢查是否存在有效的 cookie,否則封鎖請求
如果找到有效的 cookie,則根據 cookie 值新增限速規則,以便稍後進行評估
在執行所有當前套用的 DDoS 緩解措施後,套用限速規則
我們使用這種「非同步」工作流程,因為在沒有限速規則的情況下封鎖請求更有效率,從而為套用其他規則類型提供了機會。
總的來說,該流程可以用以下偽程式碼來概括:
在評估限速規則時,我們需要為每個用戶端建立一個_金鑰_,用於查找正確的計數器並將其與目標速率進行比較。通常,此金鑰是用戶端 IP 位址,但還有其他選項可用,例如此處使用的 cookie 的值。我們實際上重用了限速邏輯的現有部分來實現這一點。在偽程式碼中,它看起來是這樣的:
for (rule in active_mitigations) {
// ... (ignore other rule types)
if (rule.match_current_request()) {
if (!has_valid_cookie()) {
// no cookie: serve error page
return serve_error_page();
} else {
// add a rate-limit rule to be evaluated later
add_rate_limit_rule(rule);
}
}
}
evaluate_rate_limit_rules();
這個簡單的_金鑰_產生函數存在兩個問題,這兩個問題與特定形式的用戶端請求相結合,導致處理 HTTP 請求的程序中出現無限迴圈:
function get_cookie_key() {
// Validate that the cookie is valid before taking its value.
// Here the cookie has been checked before already, but this code is
// also used for "standalone" rate-limit rules.
if (!has_valid_cookie_broken()) { // more on the "broken" part later
return cookie_value;
} else {
return parent_key_generator();
}
}
DDoS 邏輯產生的限速規則以意想不到的方式使用內部 API。這導致上述偽程式碼中的
parent_key_generator
指向get_cookie_key
函數本身,這意味著如果採用該程式碼路徑,該函數將無限期地呼叫自身由於這些限速規則僅在驗證 cookie 後新增,因此第二次驗證應該會得到相同的結果。問題是這裡使用的
has_valid_cookie_broken
函數實際上是不同的,如果用戶端傳送多個 cookie,其中一些有效,而另一些無效,則兩者可能會不一致
因此,結合這兩個問題:損壞的驗證函數告訴 get_cookie_key
該 cookie 無效,導致採用 else
分支並一遍又一遍地呼叫相同的函數。
許多程式設計語言都採取了一項保護措施來幫助防止此類迴圈,即對函式呼叫堆疊可以達到的深度進行執行階段保護限制。如果嘗試呼叫已達到此限制的函數,只需一次即會導致執行階段錯誤。閱讀上述邏輯時,初步分析可能表明我們在這一事件中已經達到了限制,因此請求最終導致錯誤,堆疊中反復包含這些相同的函式呼叫。
然而,這裡的情況並非如此。用於編寫此邏輯的部分語言(包括 Lua)還實施了一種稱為「正確的尾部呼叫」的最佳化。尾部呼叫是指函數的最後一個動作是執行另一個函數。我們不必將該函數新增為堆疊中的另一層,因為我們確信,之後不會將執行上下文傳回給父函數,也不會使用其任何區域變數,而是可以用此函式呼叫替換堆疊中的頂部幀。
最終結果是請求處理邏輯中出現迴圈,該迴圈永遠不會增加堆疊的大小。相反,它只會消耗 100% 的可用 CPU 資源,並且永遠不會終止。一旦處理 HTTP 請求的程序收到應套用該動作的單一請求,並且該程序混合了有效和無效的 cookie,該程序就會中毒,並且永遠無法處理任何進一步的請求。
每個 Cloudflare 伺服器都有數十個這樣的程序,因此單一中毒程序不會造成太大影響。然而,緊接著開始發生一些其他事情:
伺服器 CPU 使用率的增加導致 Unimog 降低伺服器接收的新流量的數量,將流量轉移到其他伺服器,因此在某個時候,更多的新連線將從部分程序中毒的伺服器轉移到中毒程序較少或沒有中毒程序的伺服器,從而降低 CPU 使用率。
資料中心 CPU 使用率的逐漸增加開始導致 Traffic Manager 將流量重新導向到其他資料中心。由於此動作無法修復中毒程序,CPU 使用率仍然很高,因此 Traffic Manager 繼續重新導向越來越多的流量。
兩種情況下的重新導向流量都包含中毒程序的請求,導致傳送此重新導向流量的伺服器和資料中心開始以相同的方式出現故障。
幾分鐘之內,多個資料中心就出現了大量中毒程序,Traffic Manager 已將盡可能多的流量從這些程序中轉移出去,但無法再轉移更多。這部分是由於其內建的自動化安全限制,但也因為越來越難以找到具有足夠可用容量作為目標的資料中心。
第一個中毒程序出現在世界標準時間 17:47,到世界標準時間 18:09(事件宣佈五分鐘後),Traffic Manager 已將大量流量重新路由出歐洲:
截至世界標準時間 18:09 的 Traffic Manager 容量動作摘要圖。每個圓圈代表流量被重新路由離開或到達的資料中心。圓圈的顏色表示該資料中心的 CPU 負載。它們之間的橙色絲帶顯示有多少流量被重新路由,以及從哪裡/到哪裡。
如果我們查看 HTTP 請求服務程序中使 CPU 達到飽和的程序所占的百分比,就會很容易明白原因。在這些時區的高峰流量時段,西歐的容量已消耗 10%,東歐的容量已消耗 4%:
使 CPU 達到飽和的所有 HTTP 請求處理程序的百分比(按地理區域劃分)
許多地點部分中毒的伺服器難以應對請求負載,其餘程序無法跟上,導致 Cloudflare 傳回最少的 HTTP 錯誤回應。
在世界標準時間 18:04,我們的全球 CPU 使用率達到某個持續水準,Cloudflare 工程師自動收到通知,並開始調查。我們許多當值的事件回應人員已經在處理由骨幹網擁塞引起的未決事件,在最初幾分鐘內,我們調查了與網路擁塞事件的可能關聯。我們花了一些時間才意識到 CPU 最高的位置是流量最低的位置,之後的調查不再將網路事件視為觸發因素。此時,焦點轉移到兩個主要方面:
評估重新啟動中毒程序是否能夠讓它們復原,如果可以,則在受影響的伺服器上大規模重新啟動服務
確定程序進入此 CPU 飽和狀態的觸發因素
在初始事件宣佈 25 分鐘後,我們確認重新啟動對一台樣本伺服器有幫助。五分鐘後,我們開始執行更廣泛的重新啟動——最初是一次性重新啟動整個資料中心,然後隨著識別方法的完善,對有大量中毒進程的伺服器進行重新啟動。一些工程師繼續在受影響的伺服器上定期重新啟動受影響的服務,而其他人則加入正在同步進行的另一項工作——識別觸發因素。世界標準時間 19:36,在全球範圍內停用了新的 DDoS 規則,在執行了又一輪大規模重新啟動和監控後,該事件被宣佈解決。
同時,事件所呈現的情況觸發了 Traffic Manager 中的潛在錯誤。觸發後,系統嘗試透過發起正常重新啟動來從異常中復原,導致其活動停止。該錯誤首次在世界標準時間 18:17 觸發,然後在世界標準時間 18:35 至 18:57 之間多次觸發。在這兩個時間段內(世界標準時間 18:35-18:52 和世界標準時間 18:56-19:05),系統未發出任何新的流量路由動作。這意味著,雖然我們已經復原了受影響最嚴重的資料中心的服務,但幾乎所有流量仍在重新路由。值班工程師在世界標準時間 18:34 收到有關該問題的警報。到世界標準時間 19:05,流量團隊已經編寫、測試並部署了修復程式。恢復後的首批動作對恢復服務產生了積極影響。
補救措施和後續步驟
為了解決請求中毒對我們網路的直接影響,Cloudflare 發起了受影響服務的大規模滾動重啟,直到確定觸發該情況的變更並復原該變更。該變更(即啟用一種新型 DDoS 規則)仍將完全復原,在我們修復損壞的 cookie 驗證檢查並完全確信這種情況不會再次發生之前,該規則都不會重新啟動。
我們非常重視這些事件,並認識到它們造成的影響有多大。我們已經確定可以採取一些步驟來解決這些具體情況,並確定了未來再次發生此類問題的風險。
設計:為我們 DDoS 模組使用的限速實施是一個遺留元件,客戶為其網際網路內容設定的限速規則使用具有更多現代技術和保護措施的較新引擎。
設計:我們正在探索經歷進程中毒的服務內部和周圍的選項,以限制透過尾部呼叫無限迴圈的能力。從長遠來看,Cloudflare 正在進入完全替換此服務的早期實施階段。此替換服務的設計將使我們能夠對單一請求的不間斷執行時間和總執行時間施加限制。
流程:新規則的首次啟動是在少數幾個生產資料中心進行驗證,然後在幾個小時後在所有資料中心進行。我們將繼續加強我們的暫存和推出程序,以最大限度地減少與變更相關的潛在影響範圍。
結論
Cloudflare 經歷了兩次接連的事件,影響了使用我們 CDN 和網路服務的大量客戶。第一起事件是網路骨幹擁塞,我們的系統已自動修復。對於第二起事件,我們透過定期重新啟動故障服務來緩解,同時我們識別並停用了引發故障的 DDoS 規則。對於由此給我們的客戶和試圖存取服務的終端使用者造成的任何干擾,我們深表歉意。
我們已經在生產環境中消除了啟動故障服務中的潛在錯誤所需的條件,且正在進行進一步的修復和偵測。