軟件設計雜談——性能優化的十種手段(下篇)

本文轉自 code2life,查看原文請點擊 這里

目錄

本篇也是本系列最硬核的一篇,本人技術水平有限,可能存在疏漏或錯誤之處,望斧正。仍然選取了《火影忍者》的配圖和命名方式幫助理解:

  • 八門遁甲 —— 榨干計算資源
  • 影分身術 —— 水平擴容
  • 奧義 —— 分片術
  • 秘術 —— 無鎖術

(注:這些“中二”的前綴僅是用《火影》中的一些術語,形象地描述技術方案)

八門遁甲 —— 榨干計算資源

讓硬件資源都在處理真正有用的邏輯計算,而不是做無關的事情或空轉

從晶體管到集成電路、驅動程序、操作系統、直到高級編程語言的層層抽象,每一層抽象帶來的更強的通用性更高的開發效率,多是以損失運行效率為代價的。但我們可以在用高級編程語言寫代碼的時候,在保障可讀性、可維護性基礎上運行效率更高、更適合運行時環境的方式去寫,減少額外的性能損耗《Effective XXX》、《More Effective XXX》、《高性能XXX》這類書籍所傳遞的知識和思想。

落到技術細節,下面用四個小節來說明如何減少“無用功”、避免空轉、榨干硬件。

聚焦

減少系統調用與上下文切換,讓CPU聚焦。

https://stackoverflow.com/questions/21887797/what-is-the-overhead-of-a-context-switch
https://stackoverflow.com/questions/23599074/system-calls-overhead

less copy, less context switch, less system call
fsync 10-50ms, ssd 100-10000μs (SATA NVME)
ctx switch : system call -> mode switch, thread switch: cache change, work set change, full ctx switch (1-30 μs)

大部分互聯網應用服務,耗時的部分不是計算,而是I/O。

減少I/O wait, 各司其職,專心干I/O,專心干計算,epoll批量撈任務,(refer: event driven)

//jolestar.com/parallel-programming-model-thread-goroutine-actor/

  • 利用DMA減少CPU負擔 - 零拷貝 NewI/O Redis SingleThread (even 6.0), Node.js

避免不必要的調度 - Context Switch

CPU親和性,讓CPU更加聚焦

蛻變

用更高效的數據結構、算法、第三方組件,讓程序本身蛻變。

從邏輯短路、Map代替List遍歷、減少鎖范圍、這樣的編碼技巧,到應用FisherYates、Dijkstra這些經典算法,注意每一行代碼細節,量變會發生質變。更何況某個算法就足以讓系統性能產生一兩個數量級的提升。

適應

因地制宜,適應特定的運行環境

在瀏覽器中主要是優化方向是I/O、UI渲染引擎、JS執行引擎三個方面。I/O越少越好,能用WebSocket的地方就不用Ajax,能用Ajax的地方就不要刷整個頁面;UI渲染方面,減少重排和重繪,比如Vue、React等MVVM框架的虛擬DOM用額外的計算換取最精簡的DOM操作;JS執行引擎方面,少用動態性極高的寫法,比如eval、隨意修改對象或對象原型的屬性。前端的優化有個神器:Light House,在新版本Chrome已經嵌到開發者工具中了,可以一鍵生成性能優化報告,按照優化建議改就完了。

與瀏覽器環境頗為相似的Node.js環境,
https://segmentfault.com/a/1190000007621011#articleHeader11

Java

C1 C2 JIT編譯器
棧上分配

Linux

  • 各種參數優化
  • 內存分配和GC策略
  • Linux內核參數 Brendan Gregg
    內存區塊配置(DB,JVM,V8,etc.)

利用語言特性和運行時環境 - 比如寫出利于JIT的代碼

  • 多靜態少動態 - 舍棄動態特性的靈活性 - hardcode/if-else,強類型,弱類型語言避免類型轉換 AOT/JIT vs 解釋器, 匯編,機器碼 GraalVM

減少內存的分配和回收,少對列表做增加或刪除

對于RAM有限的嵌入式環境,有時候時間不是問題,反而要拿時間換空間,以節約RAM的使用。

運籌

把眼界放寬,跳出程序和運行環境本身,從整體上進行系統性分析最高性價比的優化方案,分析潛在的優化切入點,以及能夠調配的資源和技術,運籌帷幄。

其中最簡單易行的幾個辦法,就是花錢,買更好或更多的硬件基礎設施,這往往是開發人員容易忽視的,這里提供一些妙招:

  • 服務器方面,云服務廠商提供各種類型的實例,每種類型有不同的屬性側重,帶寬、CP、磁盤的I/O能力,選適合的而不是更貴的
  • 舍棄虛擬機 - Bare Mental,比如神龍服務器
  • 用ARM架構CPU的服務器,同等價格可以買到更多的服務器,對于多數可以跨平臺運行的服務端系統來說與x86區別并不大,ARM服務器的數據中心也是技術發展趨勢使然
  • 如果必須用x86系列的服務器,AMD也Intel的性價比更高。

第一點非常重要,軟件性能遵循木桶原理,一定要找到瓶頸在哪個硬件資源,把錢花在刀刃上。如果是服務端帶寬瓶頸導致的性能問題,升級再多核CPU也是沒有用的。我有一次性能優化案例:把一個跑復雜業務的Node.js服務器從AWS的m4類型換成c4類型,內存只有原來的一半,但CPU使用率反而下降了20%,同時價格還比之前更便宜,一石二鳥。

這是因為Node.js主線程的計算任務只有一個CPU核心在干,通過CPU Profile的火焰圖,可以定位到該業務的瓶頸在主線程的計算任務上,因此提高單核頻率的作用是立竿見影的。而該業務對內存的消耗并不多,套用一些定制v8引擎內存參數的方案,起不了任何作用。

畢竟這樣的例子不多,大部分時候還是要多花錢買更高配的服務器的,除了這條花錢能直接解決問題的辦法,剩下的辦法難度就大了:

  • 利用更底層的特性實現功能,比如FFI WebAssembly調用其他語言,Java Agent Instrument,字節碼生成(BeanCopier, Json Lib),甚至匯編等等
  • 使用硬件提供的更高效的指令
  • 各種提升TLB命中率的機制,減少內存的大頁表
  • 魔改Runtime,Facebook的PHP,阿里騰訊定制的JDK
  • 網絡設備參數,MTU
  • 專用硬件:GPU加速(cuda)、AES硬件卡和高級指令加速加解密過程,比如TLS
  • 可編程硬件:地獄級難度,FPGA硬件設備加速特定業務
  • NUMA
  • 更宏觀的調度,VM層面的共享vCPU,K8S集群調度,總體上的優化

小結

有些手段,是憑空換出來更多的空間和時間了嗎?天下沒有免費的午餐,即使那些看起來空手套白狼的優化技術,也需要額外的人力成本來做,副作用可能就是專家級的發際線吧。還好很多復雜的性能優化技術我也不會,所以我本人發際線還可以。

這一小節總結了一些方向,有些技術細節非常深,這里也無力展開。不過,即使榨干了單機性能,也可能不足以支撐業務,這時候就需要分布式集群出場了,因此后面介紹的3個技術方向,都與并行化有關。

影分身術 —— 水平擴容

本節的水平擴容以及下面一節的分片,可以算整體的性能提升而不是單點的性能優化,會因為引入額外組件反而降低了處理單個請求的性能。但當業務規模大到一定程度時,再好的單機硬件也無法承受流量的洪峰,就得水平擴容了,畢竟”眾人拾柴火焰高”。

在這背后的理論基礎是,硅基半導體已經接近物理極限,隨著摩爾定律的減弱,阿姆達爾定律的作用顯現出來,https://en.wikipedia.org/wiki/Amdahl%27s_law

水平擴容必然引入負載均衡

多副本
水平擴容的前提是無狀態
讀>>寫, 多個讀實例副本 (CDN)
自動擴縮容,根據常用的或自定義的metrics,判定擴縮容的條件,或根據CRON
負載均衡策略的選擇

原理:并行化

奧義 —— 分片術

水平擴容針對無狀態組件,分片針對有狀態組件。二者原理都是提升并行度,但分片的難度更大。負載均衡也不再是簡單的加權輪詢了,而是進化成了各個分片的協調器

分片 - 百科全書分冊
Java1.7的及之前的 ConcurrentHashMap分段鎖 https://www.codercto.com/a/57430.html
有狀態數據的分片
如何選擇Partition/Sharding Key
負載均衡難題
熱點數據,增強緩存等級,解決分散的緩存帶來的一致性難題
數據冷熱分離,SSD - HDD

分開容易合并難

區塊鏈的優化,分區域

秘術 —— 無鎖術

Don’t communicate by sharing memory, share memory by communicating

有些業務場景,比如庫存業務,按照正常的邏輯去實現,水平擴容帶來的提升非常有限,因為需要鎖住庫存,扣減,再解鎖庫存。票務系統也類似,為了避免超賣,需要有一把鎖禁錮了橫向擴展的能力。

不管是單機還是分布式微服務,鎖都是制約并行度的一大因素。比如上篇提到的秒殺場景,庫存就那么多,系統超賣了可能導致非常大的經濟損失,但用分布式鎖會導致即使服務擴容了成千上萬個實例,最終無數請求仍然阻塞在分布式鎖這個串行組件上了,再多水平擴展的實例也無用武之地。

避免競爭Race Condition 是最完美的解決辦法。上篇說的應對秒殺場景,預取庫存就是減輕競態條件的例子,雖然取到服務器內存之后仍然有多線程的鎖,但鎖的粒度更細了,并發度也就提高了。
線程同步鎖
分布式鎖
數據庫鎖 update select子句
事務鎖
順序與亂序
樂觀鎖/無鎖 CAS Java 1.8之后的ConcurrentHashMap
pipeline技術 - CPU流水線 Redis Pipeline 大數據分析 并行計算
原理:并行化

TCP的緩沖區排頭阻塞 QUIC HTTP3.0

總結

以ROI的視角看軟件開發,初期人力成本的投入,后期的維護成本,計算資源的費用等等,選一個合適的方案而不是一個性能最高的方案。

本篇結合個人經驗總結了常見的性能優化手段,這些手段只是冰山一角。在初期就設計實現出一個完美的高性能系統是不可能的,隨著軟件的迭代和體量的增大,利用壓測,各種工具(profiling,vmstat,iostat,netstat),以及監控手段,逐步找到系統的瓶頸,因地制宜地選擇優化手段才是正道。

有利必有弊,得到一些必然會失去一些,有一些手段要慎用。Linux性能優化大師Brendan Gregg一再強調的就是:切忌過早優化、過度優化。

持續觀測,做80%高投入產出比的優化。

除了這些設計和實現時可能用到的手段,在技術選型時選擇高性能的框架和組件也非常重要。

另外,部署基礎設施的硬件性能也同樣,合適的服務器和網絡等基礎設施往往會事半功倍,比如云服務廠商提供的各種字母開頭的instance,網絡設備帶寬的速度和穩定性,磁盤的I/O能力等等。

多數時候我們應當使用更高性能的方案,但有時候甚至要故意去違背它們。最后,以《Effective Java》第一章的一句話結束本系列吧。

首先要學會基本的規則,然后才能知道什么時候可以打破規則。

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。