伺服器的 CPU 使用率的 “正確” 水平是什麼?如果你查看一個設計良好且運行良好的服務的監控儀表板,我們希望看到的 CPU 使用率在一兩天的平均值上應該是多少?
這是一個非常一般的問題,而且不清楚它是否應該有一個單一的答案。話雖如此,長期以來,我一般認為越高越好:我們應該盡量接近 100% 的使用率。為什麼?因為低於 100% 的任何數字都代表未使用的硬體容量,這意味著我們在浪費資源。如果一個服務沒有達到其 CPU 的最大使用率,我們可以將其移到一個更小的實例上,或者在該節點上運行一些額外的工作。
這種簡單的直覺,事實上,幾乎總是錯的。
假設我們達到了理想狀態,我們的服務運行接近 100% 的使用率。如果我們意外地變得熱門,並收到意外的流量激增,會發生什麼?或者如果我們想部署一個需要每個請求額外 CPU 的新功能呢?
如果我們的使用率是 100%,然後發生某些事情增加了負載,那麼我們就麻煩了!以 100% 的使用率運行讓我們沒有空間來吸收增量負載。我們要麼以某種方式降級,要麼不得不匆忙增加緊急容量,或者兩者兼而有之。
這個玩具示例是一個非常普遍現象的例子:效率的提升往往與韌性相互抵消,並且我們對系統的優化越深入,這種權衡往往變得越糟。
超過某個點,使系統更高效將意味著使其韌性降低,反之,建立穩健性往往會使系統的效率降低(至少在短期內)。這並不是說沒有雙贏的情況;有時候可以將 帕累托邊界 向外推進;修復 “愚蠢” 的性能錯誤有時會產生這種效果。然而,超過某個努力水平後,你將被迫做出權衡。
在這裡所說的 “韌性”,重要的是要注意我指的是比 “可靠性” 或 “保持運行的能力” 更廣泛的東西;我指的是 “吸收或應對變化的能力” 的更一般概念 —— 各種變化,包括錯誤或故障的變化,但也包括產品需求的變化、市場的變化、組織或團隊組成的變化,等等。
這種權衡的例子#
這種權衡發生在超出容量規劃的領域。它幾乎適用於任何技術系統或組織的幾乎每個層面。以下是我觀察到的一些其他地方:
冗餘#
運行多個服務實例並設置負載均衡器是非常常見的,這樣如果任何實例失敗,負載將透明地路由到其他實例。在複雜的組織中,這種模式可能應用於整個數據中心的層面,架構允許整個數據中心失效,並將其負載路由到其他地方。
為了使這一切正常運行,每個實例必須有足夠的閒置容量來吸收從故障實例轉移過來的增量負載。在穩態下,在沒有故障的情況下,該容量必須閒置,或者在最好的情況下,服務於可以隨時放棄的低優先級工作。我們想要的冗餘越多,我們在穩態下必須保持的閒置容量就越多。
優化#
複雜的性能優化通常通過利用問題領域的特定屬性或結構來實現。通過將不變性嵌入數據結構和代碼組織中,你通常可以獲得大量的性能。然而,你越是依賴特定的假設,改變它們就越困難,這使得高度優化的代碼往往更難演變或添加功能。
作為一個具體的例子,在我對 Sorbet 的反思中,我談到我們如何決定類型推斷將僅限於本地和單次通過,並將這一假設嵌入到我們的代碼和數據結構中。這一選擇帶來了可觀的效率提升,但在某種意義上使系統變得更加脆弱:一些競爭對手的類型系統擁有的許多功能在 Sorbet 的代碼庫中將是不可實現或極其困難的,因為這些假設。我仍然相信這是該項目的正確選擇,但這些權衡是值得承認的。
Hillel Wayne 提到了一個類似的特性,稱之為 “聰明的代碼”,他將其定義為
利用對問題的知識的代碼。
他也談到這種意義上的聰明代碼往往是高效的,但有時卻是脆弱的。
序列化格式#
很難找到比 “將你的內存 structs
直接複製到磁碟” 更高效的序列化格式;幾乎不需要任何代碼,並且保存或加載數據的序列化成本幾乎為零。
然而,這會導致一個脆弱的系統 —— 即使添加一個新字段也需要重寫數據或進行其他特殊處理。並且在不同的字節序或字長的機器之間共享數據變得具有挑戰性。
另一方面,將所有數據寫入 JSON 或某種類似的通用容器為添加新字段或新模塊之間的通信創造了無限的靈活性,而不影響現有代碼,但代價是在線路和 CPU 工作中序列化和反序列化數據流的可觀開銷。
分佈式系統#
我最喜歡的系統論文之一是 COST 論文,它考察了多個大數據平台,並觀察到它們中的許多具有隨著可用硬體線性擴展的理想特性,但代價是效率 荒謬地 低於調整過的單線程實現。
我發現這是一個常見的權衡。分佈式計算框架靈活且具有韌性,能夠通過擴展來處理幾乎任意的工作負載。它們可以通過擴展來處理某人部署的低效代碼,並透明地處理硬體故障。需要處理更多數據?只需添加更多硬體(通常是透明的,使用某種自動擴展)。
另一方面,仔細編寫的單節點解決方案往往會更快(有時快 10-100 倍!),但卻更加脆弱:如果數據集不再適合一個節點,或者如果你需要進行 10 倍更昂貴的分析,或者如果團隊中的新工程師不知不覺地在緊湊的內部循環中提交了慢代碼,整個系統可能會崩潰或無法執行其工作。
小團隊與大型組織#
小團隊 —— 包括 “團隊” 中的一個人 —— 可以非常高效且富有生產力。團隊越小,通信開銷越少,每位工程師腦海中保持豐富共享上下文的難度就越小。寫文檔的需求更少,關於變更的溝通需求更少,培訓新成員的時間更少,等等。小團隊可以使用 “仔細思考並努力嘗試” 等策略比大型團隊走得更遠,並且通常需要依賴較少的工具,如代碼檢查器和小心的防禦性抽象設計。
在適當的情況下,經過仔細設計和經驗豐富的工程師,小團隊有時能夠大致匹配其規模 10 倍的團隊的原始產出 —— 這是一個巨大的效率提升!
然而,小團隊對於組織、技術環境或項目業務需求的變化要脆弱得多,韌性也較差。如果一個 4 人團隊中的一個人離開,你的帶寬就下降了 25%—— 更糟的是,團隊幾乎沒有招聘和培訓新成員的經驗,大量的知識和文檔僅存在於剩餘成員的腦海中。
同樣,業務重心的變化或新產品的推出需要團隊系統的許多更多功能或其他開發,這相對容易超過團隊的支持能力,並且快速擴大團隊將面臨所有相同原因的挑戰。
自動化與人類過程#
一般來說,讓機器執行任務比讓人手動執行更高效 —— 更便宜、更快,且通常更可靠。
然而,人類是無窮的 適應性,而機器(包括物理機器和軟體系統)則更加脆弱且固執。擁有人的系統在應對情況變化或意外事件時有更多的選擇。
即使我們保留所有相同的人,但用自動化來加速他們工作的某些部分,我們也面臨 自動化依賴 的風險,即人類過度依賴自動化並不當信任,或者他們在沒有自動化的情況下的功能能力萎縮,以至於在需要時無法適當地 “手動” 介入。
餘裕#
許多這些具體觀察實際上是關於 餘裕(不是軟體產品)的觀察。
擁有健康餘裕的系統 —— 至少在短期內,從簡單的分析來看 —— 根本上是低效的:這些餘裕是時間或資源,正在 “閒置”,本可以產出。
不過,從更廣泛的角度來看,這些餘裕對於韌性至關重要:系統中的 “餘裕” 使系統能夠應對小的干擾或意外;開發人員或操作員可以利用這些餘裕來介入,處理意外負載或在問題變得災難性或外部可見之前解決潛在問題。
一個沒有餘裕的系統在運行時是高效的,但在任何變化出現時都會迅速崩潰。
結論#
我試圖觸及一些具體的例子,在這些例子中,效率和可靠性彼此對立,並且相互權衡或至少在相反的方向上施加壓力。希望我已經說服你這是一個廣泛的現象,即使在可能尚未存在嚴格權衡的情況下,這兩個價值也往往會相互對立並建議不同的決策。
不幸的是,僅僅這一觀察通常無法告訴我們該如何處理任何 特定 系統。如果我們查看某個系統,初步分析表明它在輸入的使用上效率低下,我們無法在不仔細檢查的情況下告訴它是否是一個功能失調和不良設計選擇的巢穴,或者這種表面上的低效率是否支持著大量的冗餘、靈活性和餘裕,使其能夠應對任何可能出現的變化。我們需要更仔細地觀察,幾乎總是需要特定團隊和特定問題的領域專業知識。
此外,有時候會有免費的午餐。一些設計選擇或決策確實會將帕累托邊界向外推進,而不僅僅是沿著它移動。這些情況在優化良好的系統中可能很少見,但我們不能忽視它們的可能性。而且許多系統尚未得到充分優化!
此外,設計空間中的最佳點會根據系統而有所不同。有時候,可靠性或韌性至關重要,我們有理由容忍巨大的第一階段低效率。然而,有時候,極端效率是正確的目標:也許我們的利潤微薄到這是我們唯一的選擇,或者也許我們對我們的領域及其對我們系統的需求的穩定性有足夠的信心,讓我們相信不會面臨過於劇烈的變化。
因此,主要是,我所能做的就是呼籲我們作為工程師、設計師和系統觀察者,對這種權衡及其影響保持 意識。當我們指責一個系統浪費和低效時,值得暫停一下,問問那種 “浪費” 可能帶來什麼。當我們著手優化一個系統時,暫停一下,了解當前系統中的關鍵接點和靈活性,以及哪些是至關重要的,並盡力保護這些。在為系統、團隊或組織設置效率的指標或目標時,讓我們意識到,在沒有對抗壓力的情況下,我們可能也在要求系統變得更加脆弱和脆弱。