
作者 | Mitendra Mahto
譯者 | 張衛濱
代理層的關鍵脆弱性
反向代理是互聯網大規模基礎設施中默默無聞的核心組件。它們終止傳輸層安全(Transport Layer Security,TLS)、防御拒絕服務(DoS)攻擊、平衡負載、響應緩存,并連接快速演變的服務。無論你稱它為負載均衡器、邊緣代理、API 網關還是 Kubernetes Ingress 控制器,這一層都是所有流量匯聚的地方,而且我們也得承認,它也是很容易出問題的地方。
它的麻煩在于代理很少以整潔、教科書式的方式失敗。相反,有時候它們會在優化基準測試中表現出色,但是在真實工作負載下卻會崩潰;因為元數據中缺失了一個逗號,導致實時流量悄無聲息地中斷,它們也會因此而失敗。它們還可能因為旨在簡化技術棧的抽象變成了隱藏的脆弱點而失敗。
本文是運行大規模反向代理艦隊(fleet)的一系列戰爭故事的集合。它探討了適得其反的優化,觸發故障的常規變更,以及塑造我們今天設計和運行代理方式的運維教訓。
優化陷阱:
當優化變得有毒
優化是很誘人的。它們承諾了免費的性能提升,這在基準測試中看起來很棒,通常在小環境中能夠很完美地工作。
但一旦主機擴展至超過五十個核心,艦隊在數百個節點上服務數百萬 QPS 時,規則就會發生巨大的變化,一個地方的性能提升可以迅速成為大規模的負擔。
Freelist 爭用災難
我們擴展了 Apache Traffic Server(ATS),從一組較小核心的機器轉移到現代化的多核心主機。假設很簡單:更多的核心應該意味著成比例的吞吐量增加。在傳統硬件上,ATS 的 freelist 優化能夠按預期工作,減少了堆爭用并提高了分配速度。
但是,在 64 核主機上,同樣的 freelist 設計會適得其反。ATS 依賴于 freelist 訪問的單個全局鎖。數十個核心同時訪問它的話,會導致鎖成為熱點,造成系統抖動和浪費 CPU 周期。吞吐量沒有翻倍,相反尾部延遲增加,總體吞吐量下降。代理花更多時間爭奪 freelist 的使用權,而不是服務于流量。
起初我們對自己的分析持懷疑態度,我們原本以為 freelist 應該會從中受益。但一旦我們 禁用 它,吞吐量從大約 2k 請求每秒跳升到大約 6k 請求每秒,提高了 3 倍。
無鎖設計的隱藏成本
我們與 Read-Copy-Update(RCU) 模式進行了斗爭,這是一種在內核和高性能用戶空間中流行的模式,用于實現快速的無鎖讀取。它的權衡在于每次寫入都需要復制結構,并且只有在所有活動讀取器完成后,才能回收原始內存。
在大規模情況下,即使存在延遲,不斷進行 new/delete 周期的成本也會急劇上升。代理面對著數十萬主機。添加或刪除一個主機意味著復制大型的結構,在流量高峰期間會導致可觀的內存波動。無鎖讀取很快,但延遲的內存回收成為了降低性能的昂貴代價。令人驚訝的是,切換回簡單的基于鎖的方法不僅更有效,而且更可預測。
大規模的 DNS 崩潰
使用 HAProxy 時,我們曾經遇到一個故障,揭示了大規模場景下如何暴露出較小規模下可以忽略的數學問題。內置的 DNS 解析器 對某些場景會使用 quadratic-time 查找(這意味著查找時間與記錄或主機的數量的平方成比例增長)。在小型主機數量下,額外的工作是可以忽略的,系統能夠順暢運行。
但當我們在更大的艦隊上啟用這個代理時,成本一下子顯現出來。曾經只是一個小問題的事情在數百臺主機上變得難以收拾,這導致整個代理艦隊出現 CPU 峰值和崩潰。
后來,這個 bug 在上游進行了修復,但教訓一直伴隨著我們。低效的行為變得危險并不一定需要改變它們的復雜性。有時,規模會使隱藏的成本變得無法忽視。
生產教訓:在小規模下“工作正常”的代碼可能仍然隱藏著 O(N2) 或更糟的行為。在數百或數千個節點上,這些成本不再是理論上的,而是真正開始破壞生產環境。
平凡的故障:
默認值和例行任務的反噬
導致數十億美元的系統產生崩潰很少是由于冷僻的 zero-days 攻擊或深奧的協議錯誤。它們幾乎都是平平無奇的,比如,錯位的字符、被遺忘的默認值,或者操作系統功能過于完美地執行其工作。
引發致命災難的 YAMl 逗號
對于某些路由和策略決策,我們的代理會從遠程服務獲取運行時元數據。工程師在 UI 中能夠編輯這個值,它預期是一個逗號分隔的列表(a,b,c)。有一天,LinkedIn 的一位工程師漏掉了一個逗號,將列表變成了一個單一的畸形令牌。控制服務的驗證很少,因此將錯誤的載荷傳遞給了下游。我們的代理解析器更嚴格。當代理拉取更新并嘗試將值解釋為列表時,它無法對其進行處理并導致了崩潰。
因為這些元數據對啟動至關重要,所有需要立即重啟的實例在獲得相同的錯誤值后都會再次崩潰。更糟糕的是,用戶界面本身就位于代理之后,所以我們無法修復列表,直到我們執行了一次帶外(out-of-band)恢復。
沉默的殺手:文件描述符和 Watchdogs
基本的操作系統限制可能會變成災難性的失敗。在一次事件中,系統標準化將 最大文件描述符(FD)限制重置為一個更低的默認值,這對于大多數應用程序來說是合理的,但對于處理數十萬個并發連接的代理來說卻遠遠不夠。在高峰流量期間,代理耗盡了文件描述符(FD)。新的連接和正在進行的請求被默默地丟棄或延遲,導致級聯故障看起來比實際情況要復雜得多。
另一次中斷來自于“例行清理”。一位工程師發現了在用戶 nobody) 下運行的進程,并假設它們是離散的。許多 Unix 服務(包括我們的代理)故意以 nobody 身份運行,以減少權限。清理腳本在全艦隊范圍內殺死了它們,瞬間使網站的很大一部分癱瘓。
生產教訓:最具破壞性的故障往往并不引人注目。它們來自默認設置、不良輸入和每個人都視為理所當然的日常清理任務。我們要始終將遠程元數據視為不可信的,驗證語義,而不僅僅是語法。緩存并回退到已知的最新正確值。將控制平面與數據平面解耦,并在金絲雀后面分階段進行更改。如果可能的話,優先選擇靜態配置而不是動態元數據。在危險行為之前監控資源,并在每個全艦隊范圍內的行動周圍強制執行護欄。
信任但驗證:
測量熱路徑
在大流量的基礎設施中,缺乏根據的假設是一種毒藥。在大規模環境中,最小的函數可以悄悄地消耗不成比例的資源。
不是緩存的緩存頭信息
在代理中解析頭信息是代價昂貴的操作,我們的許多策略邏輯依賴于檢查特定的頭信息。為了優化這一點,我們的代碼庫使用了一個名為 extractHeader 的方法,它被注釋為值將會被緩存,并且頭信息只解析一次。從表面上看,代碼似乎就是這樣工作的,有一個 Boolean 標志指示結果是否已經被提取。
然而,當我們在大規模環境中分析 CPU 的使用情況時,頭信息解析不斷作為一個瓶頸出現。這有點出乎意料,函數名稱已經承諾會緩存頭信息。經過深入研究,我們發現多年來,該函數積累了新的邏輯。在某個地方,headerExtracted 標志被重置了,每次訪問頭信息時都強制進行完整的重新解析。在單個請求中,同一個頭信息可以被重新解析數百次。
調試持續了數周,因為方法的名稱創造了一種虛假的信任感。它看起來像是緩存,但實際上幾乎沒有緩存任何東西。
隨機數瓶頸
乍看上去,生成隨機數像是一個簡單的、無狀態的計算,是一個微不足道的、可以忽略的操作。實際上,常見的rand()實現依賴于一個全局鎖來保護其狀態。在低 QPS 下,鎖爭用是不可見的。但是,多核心機器在持續負載下,那個全局鎖就會變成一個熱點。請求堆積等待“隨機性”,本應是系統中成本最低的操作之一變成了延遲和吞吐量崩潰的根源。
我們的解決方案是切換到一個低成本、線程安全的 隨機數生成器,它專為并發設計,但是充分汲取了教訓。即便是我們認為無狀態的數學函數也可能存在隱藏同步和爭用成本,在大規模環境下引發大問題。
生產教訓:永遠不要假設“簡單”的庫調用是免費的。在熱路徑中對它們進行分析,特別是在多核心、高 QPS 工作負載下,隱藏的鎖會將微不足道的函數變成瓶頸。
蹩腳的頭信息檢查
有些問題不是來自錯誤的代碼,而是來自存在隱藏的昂貴副作用的慣用代碼。
一位開發人員曾經用 Go 編寫了一個簡單的檢查,以驗證 HTTP 頭信息是否為空:
if len(splitted_headers) > 1 { ... }這是完全慣用的 Go 代碼:清晰、可讀、安全。在單元測試和小規模運行中,它能夠完美地工作。但在生產環境中,面對每秒數千個請求,strings.Split的開銷就變得明顯了。每次調用都會分配一個新的切片,在熱路徑中會創建不必要的混亂。CPU 周期消失在分配操作中,延遲悄然上升。
解決方案也非常簡單,那就是避免分割。通過直接掃描字符,我們消除了分配操作,并將檢查變成了一個輕量級的操作。
生產教訓:慣用代碼并不意味著是可以部署到生產環境的代碼。在測試中看起來簡單或無害的操作在大規模環境中可能會成為隱藏的瓶頸。在熱路徑中,缺乏根據的假設代價高昂。客觀地進行分析,相信數據而不是假設。
異常不是常態:
保持公共路徑的清潔
在分布式系統中,優雅往往來自于統一,即一個規則涵蓋所有情況。但將正常路徑和異常合并在一起,將會使系統變得脆弱和緩慢。
哈希鍵爭用
在調試負載均衡器中的爭用問題時,我們注意到每個請求都要承擔哈希查找的成本,主機更新也會停滯。更令人驚訝的是,哈希表通常只包含一個鍵。
根本原因是一個罕見的部署。一個上游團隊將同一個應用程序拆分到多個集群中,每個集群都映射到不同的分片鍵。為了支持這個案例,代碼被泛化了,總是期望一個像這樣的結構:
{ app_name => { hash_key1 => host_list1, hash_key2 => host_list2 } }但對于幾乎所有的應用程序來講,只有一個主機列表。基于分片的間接性是一個例外,而不是常態,但它成為了每個人的默認設置。
我們將其簡化為:
{ app_name => host_list }這消除了不必要的哈希查找,消除了更新爭用,并使系統更快、更容易理解。罕見的基于分片的部署會被明確地進行處理,放在了公共路徑之外。
異常情況驅動的錯誤修復
在代理遷移期間,我們最初將大多數設置保留為默認值。遷移開始后不久,我們收到了一個涉及異常長頭信息和 cookie 的罕見用例失敗的報告。快速修復是非常簡單的:提高 限制。問題消失了,直到下周再次出現。我們再次提高了限制,然后一次又一次,每次都來自同一個異常。
只有當我們進行基準測試時,真正的成本才顯現出來:每次提高限制都會增加內存使用量,降低整體吞吐量。通過迎合一個異常值,我們降低了每個人的性能。
正確的答案不是讓系統適應那個異常。我們撤銷了對限制的更改,并要求那個單一用例繼續使用舊技術棧。我們這樣做了之后,新技術棧立即提供了更高的吞吐量和更低的延遲。團隊最終修復了有問題的 cookie,并在后來遷移到了新技術棧。
實驗膨脹
在我們的代理中,我們建立了對實驗的支持,旨在進行快速的 A/B 測試、功能推出或遷移。該機制非常有效,但需要仔細設置和驗證。后來,有人默認擴展了它:添加了工具來為每個服務自動生成實驗配置。
起初,這似乎是一個勝利:減少了手動工作,更容易啟動。實際上,大多數這種實驗都是無效的。它們沒有按預期工作,但給人的印象它們卻是按照預期運行的,這誤導了運維人員。調試變得很痛苦,因為失敗的實驗看起來像是路由問題,而且啟動序列經常在額外的復雜性下出現中斷。
最終,我們回滾了更改,移除了默認的自動生成功能,并要求實驗要明確且逐個進行添加。后來,工具更新為自動檢查其他來源是否需要這樣的實驗,并僅在這些場景中添加。這些更新大幅減少了路由規則的數量,節省了關鍵的 CPU 周期,并保持了網站的穩定。
生產教訓:永遠不要讓異常情況決定常態。明確地處理它們,將其隔離在特定路徑或層中,而不是污染主線邏輯。看起來像“靈活性”的東西通常只是延遲出現的脆弱性,等待在大規模場景上顯現。
為高壓下的運維人員而設計
機器運行著系統,但它們的恢復需要人類來執行。代理可能每秒處理數百萬請求,但當邊緣出現問題時,系統恢復依賴于一個在凌晨 3 點盯著終端的疲憊運維人員。
儀表板變黑的場景
在部分停電期間,我們的整個監控和警報管道都變黑了。儀表盤、追蹤 UI 和服務發現控制臺都離線了,我們甚至不能從受影響的數據中心失敗中脫離出去,因為故障轉移的用戶界面(UI)和命令行界面(CLI)都依賴于已經降級的服務。拯救我們的是基礎指令:ssh、grep、awk和netstat。有了這些肌肉記憶工具,以及最終在故障轉移工具中埋藏的手動工具,我們追蹤失敗的流程,隔離了錯誤的層,并強制執行故障轉移。如果團隊失去了對基礎的舒適感,或者如果沒有那個逃生艙口,我們就會手足無措。
我們還痛苦地認識到,可觀測性系統永遠不能依賴于它們要監控的代理。在某個時候,代理日志能夠正確地發送到一個中央平臺,但查看這些日志的 UI 和中央可視化服務器本身只能通過代理艦隊進行訪問。當艦隊限于泥潭時,運維人員無法再訪問儀表盤。日志仍在流動,但我們無法看到它們。
修復方法是在每個節點上保留一個本地日志路徑,始終可以使用grep和awk等簡單的 shell 工具訪問它們,即使這意味著冗余。但這保證了無論代理的狀態如何,都能看到系統。
負載均衡器的開關迷宮
另一個痛點是我們的負載均衡算法。它試圖處理一切:連接錯誤、預熱、垃圾收集(GC)暫停、流量激增,有幾十種開關,如閾值、步長、起始權重和衰減率。在紙面上,它看起來很強大,但在實踐中,它是混亂的。
當因為某種情況出現失敗時,運維人員要花費數小時在黑暗中嘗試調整各種開關,這有時能解決問題,有時卻讓問題變得更糟。想象一下凌晨 3 點的緊急呼叫,緊接著是六個小時的猜測性工作。最終,我們放棄了復雜性,轉而采用了一個簡單的基于時間的預熱機制,類似于 HAProxy 的慢啟動。恢復變得可預測、流程化且快速,這是最好的運維結果。
生產教訓:運維人員不會在完美條件下使用完美的儀表盤進行調試。他們會使用即便其他一切都在宕機時仍然有效的工具進行調試。設計你的邊緣層,以便在功能豐富的工具消失時,基本的日志、純文本、簡單命令仍然能給運維人員足夠的信息去觀察和行動。
結 論
反向代理是現代基礎設施中最繁忙和最脆弱的點。我們的教訓并不是來源于冷僻的協議或尖端算法,而是在規模擴大時才會出現的隱性成本、普通故障和運維人員現狀。通過保持公共路徑的精簡,驗證每一個假設,并為處于壓力狀態的運維人員設計功能,我們可以使得這個關鍵層既具有彈性又流程化,這是所有生產系統的理想結果。
When Reverse Proxies Surprise You: Hard Lessons from Operating at Scale(https://www.infoq.com/articles/scaling-reverse-proxies/)
聲明:本文為 InfoQ 翻譯,未經許可禁止轉載。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.