架構(gòu)設(shè)計|性能優(yōu)化的十種手段(下篇)
前文回顧,架構(gòu)設(shè)計|性能優(yōu)化的十種手段(上篇)中,我們總結(jié)了六種普適的性能優(yōu)化方法,包括索引、壓縮、緩存、預(yù)取、削峰填谷、批量處理,簡單講解了每種技術(shù)手段的原理和實際應(yīng)用。
架構(gòu)設(shè)計|性能優(yōu)化的十種手段(中篇)中,我們簡單了解了程序是如何消耗執(zhí)行時間和內(nèi)存空間的。
這一篇,再講另外幾類涉及更多技術(shù)細(xì)節(jié)的性能優(yōu)化方向。
本文轉(zhuǎn)載信息如下:
作者:code2life
鏈接:https://code2life.top/2020/08/13/0056-performance3/
本篇也是本系列最硬核的一篇,本人技術(shù)水平有限,可能存在疏漏或錯誤之處,望斧正。仍然選取了《火影忍者》的配圖和命名方式幫助理解:
八門遁甲 —— 榨干計算資源 影分身術(shù) —— 水平擴(kuò)容 奧義 —— 分片術(shù) 秘術(shù) —— 無鎖術(shù)
(注:這些“中二”的前綴僅是用《火影》中的一些術(shù)語,形象地描述技術(shù)方案)
八門遁甲 —— 榨干計算資源

讓硬件資源都在處理真正有用的邏輯計算,而不是做無關(guān)的事情或空轉(zhuǎn)。
從晶體管到集成電路、驅(qū)動程序、操作系統(tǒng)、直到高級編程語言的層層抽象,每一層抽象帶來的更強的通用性、更高的開發(fā)效率,多是以損失運行效率為代價的。但我們可以在用高級編程語言寫代碼的時候,在保障可讀性、可維護(hù)性基礎(chǔ)上用運行效率更高、更適合運行時環(huán)境的方式去寫,減少額外的性能損耗?!禘ffective XXX》、《More Effective XXX》、《高性能XXX》這類書籍所傳遞的知識和思想。
落到技術(shù)細(xì)節(jié),下面用四個小節(jié)來說明如何減少“無用功”、避免空轉(zhuǎn)、榨干硬件。
聚焦
減少系統(tǒng)調(diào)用與上下文切換,讓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)
大部分互聯(lián)網(wǎng)應(yīng)用服務(wù),耗時的部分不是計算,而是I/O。
減少I/O wait, 各司其職,專心干I/O,專心干計算,epoll批量撈任務(wù),(refer: event driven)
利用DMA減少CPU負(fù)擔(dān) - 零拷貝 NewI/O Redis SingleThread (even 6.0), Node.js
避免不必要的調(diào)度 - Context Switch。
CPU親和性,讓CPU更加聚焦。
蛻變
用更高效的數(shù)據(jù)結(jié)構(gòu)、算法、第三方組件,讓程序本身蛻變。
從邏輯短路、Map代替List遍歷、減少鎖范圍、這樣的編碼技巧,到應(yīng)用FisherYates、Dijkstra這些經(jīng)典算法,注意每一行代碼細(xì)節(jié),量變會發(fā)生質(zhì)變。更何況某個算法就足以讓系統(tǒng)性能產(chǎn)生一兩個數(shù)量級的提升。
適應(yīng)
因地制宜,適應(yīng)特定的運行環(huán)境
在瀏覽器中主要是優(yōu)化方向是I/O、UI渲染引擎、JS執(zhí)行引擎三個方面。I/O越少越好,能用WebSocket的地方就不用Ajax,能用Ajax的地方就不要刷整個頁面;UI渲染方面,減少重排和重繪,比如Vue、React等MVVM框架的虛擬DOM用額外的計算換取最精簡的DOM操作;JS執(zhí)行引擎方面,少用動態(tài)性極高的寫法,比如eval、隨意修改對象或?qū)ο笤偷膶傩浴?/p>
前端的優(yōu)化有個神器:Light House,在新版本Chrome已經(jīng)嵌到開發(fā)者工具中了,可以一鍵生成性能優(yōu)化報告,按照優(yōu)化建議改就完了。
與瀏覽器環(huán)境頗為相似的Node.js環(huán)境,
https://segmentfault.com/a/1190000007621011#articleHeader11
Java
C1 C2 JIT編譯器 棧上分配
Linux
各種參數(shù)優(yōu)化 內(nèi)存分配和GC策略 Linux內(nèi)核參數(shù) Brendan Gregg 內(nèi)存區(qū)塊配置(DB,JVM,V8,etc.)
利用語言特性和運行時環(huán)境 - 比如寫出利于JIT的代碼
多靜態(tài)少動態(tài) - 舍棄動態(tài)特性的靈活性 - hardcode/if-else,強類型,弱類型語言避免類型轉(zhuǎn)換 AOT/JIT vs 解釋器, 匯編,機器碼 GraalVM
減少內(nèi)存的分配和回收,少對列表做增加或刪除
對于RAM有限的嵌入式環(huán)境,有時候時間不是問題,反而要拿時間換空間,以節(jié)約RAM的使用。
運籌
把眼界放寬,跳出程序和運行環(huán)境本身,從整體上進(jìn)行系統(tǒng)性分析最高性價比的優(yōu)化方案,分析潛在的優(yōu)化切入點,以及能夠調(diào)配的資源和技術(shù),運籌帷幄。
其中最簡單易行的幾個辦法,就是花錢,買更好或更多的硬件基礎(chǔ)設(shè)施,這往往是開發(fā)人員容易忽視的,這里提供一些妙招:
服務(wù)器方面,云服務(wù)廠商提供各種類型的實例,每種類型有不同的屬性側(cè)重,帶寬、CP、磁盤的I/O能力,選適合的而不是更貴的 舍棄虛擬機 - Bare Mental,比如神龍服務(wù)器 用ARM架構(gòu)CPU的服務(wù)器,同等價格可以買到更多的服務(wù)器,對于多數(shù)可以跨平臺運行的服務(wù)端系統(tǒng)來說與x86區(qū)別并不大,ARM服務(wù)器的數(shù)據(jù)中心也是技術(shù)發(fā)展趨勢使然 如果必須用x86系列的服務(wù)器,AMD也Intel的性價比更高。
第一點非常重要,軟件性能遵循木桶原理,一定要找到瓶頸在哪個硬件資源,把錢花在刀刃上。如果是服務(wù)端帶寬瓶頸導(dǎo)致的性能問題,升級再多核CPU也是沒有用的。我有一次性能優(yōu)化案例:把一個跑復(fù)雜業(yè)務(wù)的Node.js服務(wù)器從AWS的m4類型換成c4類型,內(nèi)存只有原來的一半,但CPU使用率反而下降了20%,同時價格還比之前更便宜,一石二鳥。
這是因為Node.js主線程的計算任務(wù)只有一個CPU核心在干,通過CPU Profile的火焰圖,可以定位到該業(yè)務(wù)的瓶頸在主線程的計算任務(wù)上,因此提高單核頻率的作用是立竿見影的。而該業(yè)務(wù)對內(nèi)存的消耗并不多,套用一些定制v8引擎內(nèi)存參數(shù)的方案,起不了任何作用。
畢竟這樣的例子不多,大部分時候還是要多花錢買更高配的服務(wù)器的,除了這條花錢能直接解決問題的辦法,剩下的辦法難度就大了:
利用更底層的特性實現(xiàn)功能,比如FFI WebAssembly調(diào)用其他語言,Java Agent Instrument,字節(jié)碼生成(BeanCopier, Json Lib),甚至匯編等等 使用硬件提供的更高效的指令 各種提升TLB命中率的機制,減少內(nèi)存的大頁表 魔改Runtime,F(xiàn)acebook的PHP,阿里騰訊定制的JDK 網(wǎng)絡(luò)設(shè)備參數(shù),MTU 專用硬件:GPU加速(cuda)、AES硬件卡和高級指令加速加解密過程,比如TLS 可編程硬件:地獄級難度,F(xiàn)PGA硬件設(shè)備加速特定業(yè)務(wù) NUMA 更宏觀的調(diào)度,VM層面的共享vCPU,K8S集群調(diào)度,總體上的優(yōu)化
小結(jié)
有些手段,是憑空換出來更多的空間和時間了嗎?天下沒有免費的午餐,即使那些看起來空手套白狼的優(yōu)化技術(shù),也需要額外的人力成本來做,副作用可能就是專家級的發(fā)際線吧。還好很多復(fù)雜的性能優(yōu)化技術(shù)我也不會,所以我本人發(fā)際線還可以。
這一小節(jié)總結(jié)了一些方向,有些技術(shù)細(xì)節(jié)非常深,這里也無力展開。不過,即使榨干了單機性能,也可能不足以支撐業(yè)務(wù),這時候就需要分布式集群出場了,因此后面介紹的3個技術(shù)方向,都與并行化有關(guān)。
影分身術(shù) —— 水平擴(kuò)容
本節(jié)的水平擴(kuò)容以及下面一節(jié)的分片,可以算整體的性能提升而不是單點的性能優(yōu)化,會因為引入額外組件反而降低了處理單個請求的性能。但當(dāng)業(yè)務(wù)規(guī)模大到一定程度時,再好的單機硬件也無法承受流量的洪峰,就得水平擴(kuò)容了,畢竟”眾人拾柴火焰高”。
在這背后的理論基礎(chǔ)是,硅基半導(dǎo)體已經(jīng)接近物理極限,隨著摩爾定律的減弱,阿姆達(dá)爾定律的作用顯現(xiàn)出來。
https://en.wikipedia.org/wiki/Amdahl%27s_law
水平擴(kuò)容必然引入負(fù)載均衡

多副本 水平擴(kuò)容的前提是無狀態(tài) 讀>>寫, 多個讀實例副本 (CDN) 自動擴(kuò)縮容,根據(jù)常用的或自定義的metrics,判定擴(kuò)縮容的條件,或根據(jù)CRON 負(fù)載均衡策略的選擇
原理:并行化
奧義 —— 分片術(shù)
水平擴(kuò)容針對無狀態(tài)組件,分片針對有狀態(tài)組件。二者原理都是提升并行度,但分片的難度更大。負(fù)載均衡也不再是簡單的加權(quán)輪詢了,而是進(jìn)化成了各個分片的協(xié)調(diào)器。

分片 - 百科全書分冊 Java1.7的及之前的 ConcurrentHashMap分段鎖 https://www.codercto.com/a/57430.html 有狀態(tài)數(shù)據(jù)的分片 如何選擇Partition/Sharding Key 負(fù)載均衡難題 熱點數(shù)據(jù),增強緩存等級,解決分散的緩存帶來的一致性難題 數(shù)據(jù)冷熱分離,SSD - HDD
分開容易合并難
區(qū)塊鏈的優(yōu)化,分區(qū)域
秘術(shù) —— 無鎖術(shù)
Don’t communicate by sharing memory, share memory by communicating

有些業(yè)務(wù)場景,比如庫存業(yè)務(wù),按照正常的邏輯去實現(xiàn),水平擴(kuò)容帶來的提升非常有限,因為需要鎖住庫存,扣減,再解鎖庫存。票務(wù)系統(tǒng)也類似,為了避免超賣,需要有一把鎖禁錮了橫向擴(kuò)展的能力。
不管是單機還是分布式微服務(wù),鎖都是制約并行度的一大因素。比如上篇提到的秒殺場景,庫存就那么多,系統(tǒng)超賣了可能導(dǎo)致非常大的經(jīng)濟(jì)損失,但用分布式鎖會導(dǎo)致即使服務(wù)擴(kuò)容了成千上萬個實例,最終無數(shù)請求仍然阻塞在分布式鎖這個串行組件上了,再多水平擴(kuò)展的實例也無用武之地。
避免競爭Race Condition 是最完美的解決辦法。上篇說的應(yīng)對秒殺場景,預(yù)取庫存就是減輕競態(tài)條件的例子,雖然取到服務(wù)器內(nèi)存之后仍然有多線程的鎖,但鎖的粒度更細(xì)了,并發(fā)度也就提高了。
線程同步鎖 分布式鎖 數(shù)據(jù)庫鎖 update select子句 事務(wù)鎖 順序與亂序 樂觀鎖/無鎖 CAS Java 1.8之后的ConcurrentHashMap pipeline技術(shù) - CPU流水線 Redis Pipeline 大數(shù)據(jù)分析 并行計算 TCP的緩沖區(qū)排頭阻塞 QUIC HTTP3.0
原理:并行化
總結(jié)
以ROI的視角看軟件開發(fā),初期人力成本的投入,后期的維護(hù)成本,計算資源的費用等等,選一個合適的方案而不是一個性能最高的方案。
本篇結(jié)合個人經(jīng)驗總結(jié)了常見的性能優(yōu)化手段,這些手段只是冰山一角。在初期就設(shè)計實現(xiàn)出一個完美的高性能系統(tǒng)是不可能的,隨著軟件的迭代和體量的增大,利用壓測,各種工具(profiling,vmstat,iostat,netstat),以及監(jiān)控手段,逐步找到系統(tǒng)的瓶頸,因地制宜地選擇優(yōu)化手段才是正道。
有利必有弊,得到一些必然會失去一些,有一些手段要慎用。Linux性能優(yōu)化大師Brendan Gregg一再強調(diào)的就是:切忌過早優(yōu)化、過度優(yōu)化。
持續(xù)觀測,做80%高投入產(chǎn)出比的優(yōu)化。
除了這些設(shè)計和實現(xiàn)時可能用到的手段,在技術(shù)選型時選擇高性能的框架和組件也非常重要。
另外,部署基礎(chǔ)設(shè)施的硬件性能也同樣,合適的服務(wù)器和網(wǎng)絡(luò)等基礎(chǔ)設(shè)施往往會事半功倍,比如云服務(wù)廠商提供的各種字母開頭的instance,網(wǎng)絡(luò)設(shè)備帶寬的速度和穩(wěn)定性,磁盤的I/O能力等等。
多數(shù)時候我們應(yīng)當(dāng)使用更高性能的方案,但有時候甚至要故意去違背它們。最后,以《Effective Java》第一章的一句話結(jié)束本系列吧。
首先要學(xué)會基本的規(guī)則,然后才能知道什么時候可以打破規(guī)則。
以上就是本文的全部內(nèi)容,如果覺得還不錯的話歡迎點贊,轉(zhuǎn)發(fā)和關(guān)注,感謝支持。
參考:
《高性能JavaScript》 —— Nicholas C. Zakas 《Effective Java》 第三版 —— Joshua Bloch www.brendangregg.com/ —— Brendan Gregg https://colin-scott.github.io/personal_website/research/interactive_latency.html https://stackoverflow.com/questions/23599074/system-calls-overhead https://stackoverflow.com/questions/21887797/what-is-the-overhead-of-a-context-switch jolestar.com/parallel-programming-model-thread-goroutine-actor/ https://www.codercto.com/a/57430.html https://segmentfault.com/a/1190000007621011#articleHeader11
推薦閱讀:
