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