這份年薪百萬的架構師總結:性能優化其實真的不難,不要老一直自己以爲,記住這十條策略足矣了~~~

今天我們要來聊來哦你具體優化策略,看看你知多少???

現實中的性能問題和具體領域千差萬別,我也不可能面面俱到。但是爲了幫助你理解,我總結了十大常用的優化策略。

 

我將這十大策略分成五個類別,每個類別對應兩個相關策略,幫助你掌握。這五個類別是:時空相互轉換、並行 / 異步操作、預先 / 延後處理、緩存 / 批量合併、算法設計和數據結構。我們現在一個個來講。

一、時空轉換

第一個策略類別是“時空轉換”。我們看科幻電影和小說的時候,經常會看到時空轉換這個題材。性能優化裏面有兩個策略恰好組成了這個類別,包括“用時間換空間”和“用空間換時間”這兩個看似互相對立的策略。

1. 用時間換空間

用時間換空間的策略,出發點是內存和存儲這樣的“空間”資源,有時會成爲最稀缺的資源,所以需要儘量減少佔用的空間。比如,一個系統的最大性能瓶頸如果是內存使用量,那麼減少內存的使用就是最重要的性能優化。

這個策略具體的操作方法有幾種:

  • 改變應用程序本身的數據結構或者數據格式,減少需要存儲的數據的大小;
  • 想方設法壓縮存在內存中的數據,比如採用某種壓縮算法,真正使用時再解壓縮;
  • 把一些內存數據,存放到外部的、更加便宜的存儲系統裏面,到需要時再取回來。

這些節省內存空間的方法,一般都需要付出時間的代價。

除了內存,還有一種常見的場景是,降低數據的大小來方便網絡傳輸和外部存儲。壓縮的方法和算法有很多種, 比如現在比較流行的 ZStandard(ZSTD)和 LZ4。這些算法之間有空間和時間的取捨。

衡量任何壓縮算法,基本上看三個指標:壓縮比例壓縮速度以及使用內存

如果系統的瓶頸在網絡傳輸速度或者存儲空間大小上,那就儘量採取高壓縮比的算法,這樣用時間來換空間,就能夠節省時間或者其他方面的成本。

2. 用空間換時間

“用空間換時間”就是對“用時間換空間”策略反其道而行之。有些場景下,時間和速度更加重要,但是空間尚有富餘,這時我們就可以考慮用空間來換時間。

這裏要注意的一點是,我們後面還會講一條關於使用緩存的策略。雖然緩存的策略理論上也是一種“空間換時間”的方式,但我們在這裏把它分開來講,這是因爲緩存策略的“空間”定義與一般的“空間換時間”不同。一般來講,“緩存”使用的空間,和原來的空間不在同一個層次上,添加的緩存往往比原來的空間高出一個檔次。而我們這裏“空間換時間”的策略,裏面的“空間”是和原來的空間相似的空間。

互聯網的服務往往規模很大,比如全國的服務甚至是全球的服務。用戶分佈在各地,它們對訪問時間的要求很高,這就要求被訪問的數據和服務,要儘量放在離他們很近的地方。“空間換時間”就是對數據和服務進行多份拷貝,儘可能地完美覆蓋大多數的用戶。我們前面講過的 CDN 內容分發網絡技術就可以歸類於此。

其實我們部署的任何大規模系統,都或多或少地採用了用空間換時間的策略,比如在集羣和服務器間進行負載均衡,就是同時用很多個服務器(空間)來換取延遲的減少(時間)。

二、預先和延後處理

優化策略的第二大類是“預先和延後處理”,這一類別也有兩個互相對立的策略。一個是預先或者提前處理,另外一個是延後或者惰性處理。

3. 預先 / 提前處理

預先 / 提前處理策略同樣也表現在很多領域,比如網站頁面資源的提前加載。Web 標準規定了至少兩種提前加載的方式:preload 和 prefetch,分別用不同的優先級來加載資源,可以顯著地提升頁面下載性能。

很多文件系統有預讀的功能,就是提前從磁盤讀取額外的數據,爲下次上層應用程序讀數據做準備。這個功能對順序讀取非常有效,可以明顯地減少磁盤請求的數量,從而提升讀數據的性能。

CPU 和內存也有相應的預取操作,就是將內存中的指令和數據,提前存放到緩存中,從而加快處理器執行速度。緩存預取可以通過硬件或者軟件實現,也就是分爲硬件預取軟件預取兩類。

硬件預取是通過處理器中的硬件來實現的。該硬件會一直監控正在執行程序中請求的指令或數據,並且根據既定規則,識別下一個程序需要的數據或指令並預取。

軟件預取是在程序編譯的過程中,主動插入預取指令(prefetech),這個預取指令可以是編譯器自己加的,也可以是我們加的代碼。這樣在執行過程中,在指定位置就會進行預取的操作。

4. 延後 / 惰性處理

延後 / 惰性處理策略和前面說的預先 / 提前處理正好相反。就是儘量將操作(比如計算),推遲到必需執行的時刻,這樣很可能避免多餘的操作,甚至根本不用操作。

運用這一策略最有名的例子,就是 COW(Copy On Write,寫時複製)。假設多個線程都想操作一份數據,一般情況下,每個線程可以自己拷貝一份,放到自己的空間裏面。但是拷貝的操作很費時間。系統如果採用惰性處理,就會將拷貝的操作推遲。如果多個線程對這份數據只有讀的請求,那麼同一個數據資源是可以共享的,因爲“讀”的操作不會改變這份數據。當某個線程需要修改這一數據時(寫操作),系統就將資源拷貝一份給該線程使用,允許改寫,這樣就不會影響別的線程。

COW 最廣爲人知的應用場景有兩個。一個是 Unix 系統 fork 調用產生的子進程共享父進程的地址空間,只有到某個子進程需要進行寫操作纔會拷貝一份。另一個是高級語言的類和

容器,比如 Java 中的 CopyOnWrite 容器,用於多線程併發情況下的高效訪問。C++ 裏面經常使用的 STL 標準模板庫中的很多類,比如 string 類,也是具有寫時才拷貝技術的類。

三、並行 / 異步操作

優化策略的第三大類是“並行 / 異步操作”。並行和異步兩種操作雖然看起來很不一樣,其實有異曲同工之妙,就是都把一條流水線和處理過程分成了幾條,不管是物理上分還是邏輯上分。

5. 並行操作

並行操作是一種物理上把一條流水線分成好幾條的策略。直觀上說,一個人幹不完的活,那就多找幾個人來幹。並行操作既增加了系統的吞吐量,又減少了用戶的平均等待時間。比如現代的 CPU 都有很多核,每個核上都可以獨立地運行線程,這就是並行操作。

並行操作需要我們的程序有擴展性,不能擴展的程序,就無法進行並行處理。這裏的並行概念有不同的粒度,比如是在服務器的粒度(所謂的橫向擴展),還是在多線程的粒度,甚至是在指令級別的粒度。

絕大多數互聯網服務器,要麼使用多進程,要麼使用多線程來處理用戶的請求,以充分利用多核 CPU。另外一種情況就是在有 IO 阻塞的地方,也是非常適合使用多線程並行操作的,因爲這種情況 CPU 基本上是空閒狀態,多線程可以讓 CPU 多幹點活。

6. 異步操作

異步操作這一策略和並行操作不同,這是一種邏輯上把一條流水線分成幾條的策略。

我們首先在編程的領域澄清一下概念:同步和異步。同步和異步的區別在於一個函數調用之後,是否直接返回結果。如果函數掛起,直到獲得結果才返回,這是同步;如果函數馬上返回,等數據到達再通知函數,那麼這就是異步。

我們知道 Unix 下的文件操作,是有 block 和 non-block 的方式的,有些系統調用也是block 式的,如:Socket 下的 select 等。如果我們的程序一直是同步操作,那麼就會非常影響性能。採用異步操作的話,雖然稍微增加一點程序的複雜度,但會讓性能的吞吐率有很大提升。

現代的語言往往對異步操作有比較好的支持,使得異步編程變得更加簡單,可讀性也更好。

四、緩存 / 批量合併

“緩存 / 批量合併”是優化策略中的第四大類。緩存和批量合併這兩個策略,有些場景下會同時起作用,所以我把它們放在一起。

7. 緩存數據

緩存的本質是加速訪問。這是一個用得非常普遍的策略,幾乎體現在計算機系統裏面每一個模塊和領域,CPU、內存、文件系統、存儲系統、內容分佈、數據庫等等,都會遵循這樣的策略。

我們最熟悉的應該就是 CPU 的各級緩存了。在文件系統、存儲系統和數據庫系統裏面,也有快速緩存來存儲經常訪問的數據,目的是儘量提高緩存命中率,從而避免訪問比較慢的存儲介質。

對於一個基於 Web 的應用服務,前端會有瀏覽器緩存,有 CDN 存放在邊緣服務器上,有反向代理提供的靜態內容緩存;後端則還會有服務器本地緩存。

程序設計中,對於可能重複創建和銷燬,且創建銷燬代價很大的對象(比如套接字和線程),也可以緩存,對應的緩存形式,就是連接池和線程池等。

對於消耗較大的計算,也可以將計算結果緩存起來,下次可以直接讀取結果。比如對遞歸代碼的一個有效優化手段,就是緩存中間結果。

8. 批量合併處理

在有 IO(比如網絡 IO 和磁盤 IO)的時候,合併操作和批量操作往往能提升吞吐量,提高性能。

我們最常見的是批量 IO 讀寫。就是在有多次 IO 的時候,可以把它們合併成一次讀寫數據。這樣可以減少讀寫時間和協議負擔。比如,GFS 寫文件的時候,儘量批量寫,以減少IO 開銷。

對數據庫的讀寫操作,也可以儘量合併。比如,對鍵值數據庫的查詢,最好一次查詢多個鍵,而不要分成多次。

涉及到網絡請求的時候,網絡傳輸的時間可能遠大於請求的處理時間,因此合併網絡請求也很有必要。上層協議呢,端到端對話次數儘量不要太頻繁(Chatty),否則的話,總的應用層吞吐量不會很高。

五、更先進算法和數據結構

優化策略中的最後一個大類就是“更先進算法和數據結構”。這兩個策略是緊密配合的,比如先進的算法有時候會需要先進的數據結構;而且它們往往和程序的設計代碼直接相關,所以放在一起。

9. 先進的算法

同一個問題,肯定會有不同的算法實現,進而就會有不同的性能。比如各種排序算法,就是各有千秋。有的實現可能是時間換空間,有的實現可能是空間換時間,那麼就需要根據你自己的實際情況做權衡。

對每一種具體的場景(包括輸入集合大小、時間空間的要求、數據的大小分佈等),總會有一種算法是最適合的。我們需要考慮實際情況,來選擇這一最優的算法。

10. 高效的數據結構

和算法的情況類似,不同的數據結構的特性,也是千差萬別。

沒有一個數據結構是在所有情況下都是最好的,比如你可能經常用到的 Java 裏面列表的各種實現,包括各種口味的 List、Vector、LinkedList,它們孰優孰劣,取決於很多個指標:添加元素、刪除元素、查詢元素、遍歷耗時等等。我們同樣要權衡取捨,找出實際場合下最適合的高效的數據結構。

六、總結

各種性能問題的解決,需要採用一些策略;而且不同的人和不同的場景中,會採用有時相同有時迥異的策略,恰如韓愈所說的“草樹知春不久歸,百般紅紫鬥芳菲”。但花草樹木爭奇鬥豔,說到底是因爲“知春不久歸”。

同樣的道理,這些性能優化策略,有時候很容易想到,有時候並不是那麼直觀。所以,我們需要系統地有層次地思考,而這一講就是幫助你建立這樣的思路

通過總結十大策略,希望你可以多從不同角度,思考同一個問題;有時候一個問題看似無解,但多方位思考,可能會突然發現非常好的解決方案。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章