1. 程式人生 > >多圖詳解!10大高效能開發核心技術

多圖詳解!10大高效能開發核心技術

程式設計師經常要面臨的一個問題就是:如何提高程式效能?

這篇文章,我們循序漸進,從記憶體、磁碟I/O、網路I/O、CPU、快取、架構、演算法等多層次遞進,串聯起高效能開發十大必須掌握的核心技術。

- I/O優化:零拷貝技術
- I/O優化:多路複用技術
- 執行緒池技術
- 無鎖程式設計技術
- 程序間通訊技術
- RPC && 序列化技術
- 資料庫索引技術
- 快取技術 && 布隆過濾器
- 全文搜尋技術
- 負載均衡技術

準備好了嗎,坐穩了,發車!

首先,我們從最簡單的模型開始。

老闆告訴你,開發一個靜態web伺服器,把磁碟檔案(網頁、圖片)通過網路發出去,怎麼做?

你花了兩天時間,擼了一個1.0版本:

  • 主執行緒進入一個迴圈,等待連線

  • 來一個連線就啟動一個工作執行緒來處理

  • 工作執行緒中,等待對方請求,然後從磁碟讀檔案、往套介面傳送資料,完事兒

上線一天,老闆發現太慢了,大一點的圖片載入都有卡頓感。讓你優化,這個時候,你需要:

I/O優化:零拷貝技術

上面的工作執行緒,從磁碟讀檔案、再通過網路傳送資料,資料從磁碟到網路,兜兜轉轉需要拷貝四次,其中CPU親自搬運都需要兩次。

零拷貝技術,解放CPU,檔案資料直接從核心傳送出去,無需再拷貝到應用程式緩衝區,白白浪費資源。

Linux API:

ssize_t sendfile(
  int out_fd, 
  int in_fd, 
  off_t *offset, 
  size_t count
  );

函式名字已經把函式的功能解釋的很明顯了:傳送檔案。指定要傳送的檔案描述符和網路套接字描述符,一個函式搞定!

用上了零拷貝技術後開發了2.0版本,圖片載入速度明顯有了提升。不過老闆發現同時訪問的人變多了以後,又變慢了,又讓你繼續優化。這個時候,你需要:

I/O優化:多路複用技術

前面的版本中,每個執行緒都要阻塞在recv等待對方的請求,這來訪問的人多了,執行緒開的就多了,大量執行緒都在阻塞,系統運轉速度也隨之下降。

這個時候,你需要多路複用技術,使用select模型,將所有等待(accept、recv)都放在主執行緒裡,工作執行緒不需要再等待。

過了一段時間之後,網站訪問的人越來越多了,就連select也開始有點應接不暇,老闆繼續讓你優化效能。

這個時候,你需要升級多路複用模型為epoll。

select有三弊,epoll有三優。

  • select底層採用陣列來管理套接字描述符,同時管理的數量有上限,一般不超過幾千個,epoll使用樹和連結串列來管理,同時管理數量可以很大。

  • select不會告訴你到底哪個套接字來了訊息,你需要一個個去詢問。epoll直接告訴你誰來了訊息,不用輪詢。

  • select進行系統呼叫時還需要把套接字列表在使用者空間和核心空間來回拷貝,迴圈中呼叫select時簡直浪費。epoll統一在核心管理套接字描述符,無需來回拷貝。

用上了epoll多路複用技術,開發了3.0版本,你的網站能同時處理很多使用者請求了。


但是貪心的老闆還不滿足,不捨得升級硬體伺服器,卻讓你進一步提高伺服器的吞吐量。你研究後發現,之前的方案中,工作執行緒總是用到才建立,用完就關閉,大量請求來的時候,執行緒不斷建立、關閉、建立、關閉,開銷挺大的。這個時候,你需要:

執行緒池技術

我們可以在程式一開始啟動後就批量啟動一波工作執行緒,而不是在有請求來的時候才去建立,使用一個公共的任務佇列,請求來臨時,向佇列中投遞任務,各個工作執行緒統一從佇列中不斷取出任務來處理,這就是執行緒池技術。

多執行緒技術的使用一定程度提升了伺服器的併發能力,但同時,多個執行緒之間為了資料同步,常常需要使用互斥體、訊號、條件變數等手段來同步多個執行緒。這些重量級的同步手段往往會導致執行緒在使用者態/核心態多次切換,系統呼叫,執行緒切換都是不小的開銷。

線上程池技術中,提到了一個公共的任務佇列,各個工作執行緒需要從中提取任務進行處理,這裡就涉及到多個工作執行緒對這個公共佇列的同步操作。


有沒有一些輕量級的方案來實現多執行緒安全的訪問資料呢?這個時候,你需要:

無鎖程式設計技術

多執行緒併發程式設計中,遇到公共資料時就需要進行執行緒同步。而這裡的同步又可以分為阻塞型同步和非阻塞型同步。

阻塞型同步好理解,我們常用的互斥體、訊號、條件變數等這些作業系統提供的機制都屬於阻塞型同步,其本質都是要加“鎖”。

與之對應的非阻塞型同步就是在無鎖的情況下實現同步,目前有三類技術方案:

  • Wait-free

  • Lock-free

  • Obstruction-free

三類技術方案都是通過一定的演算法和技術手段來實現不用阻塞等待而實現同步,這其中又以Lock-free最為應用廣泛。

Lock-free能夠廣泛應用得益於目前主流的CPU都提供了原子級別的read-modify-write原語,這就是著名的CAS(Compare-And-Swap)操作。在Intel x86系列處理器上,就是cmpxchg系列指令。

// 通過CAS操作實現Lock-free
do {
  ...
} while(!CAS(ptr,old_data,new_data ))

我們常常見到的無鎖佇列、無鎖鏈表、無鎖HashMap等資料結構,其無鎖的核心大都來源於此。在日常開發中,恰當的運用無鎖化程式設計技術,可以有效地降低多執行緒阻塞和切換帶來的額外開銷,提升效能。


伺服器上線了一段時間,發現服務經常崩潰異常,排查發現是工作執行緒程式碼bug,一崩潰整個服務都不可用了。於是你決定把工作執行緒和主執行緒拆開到不同的程序中,工作執行緒崩潰不能影響整體的服務。這個時候出現了多程序,你需要:

程序間通訊技術

提起程序間通訊,你能想到的是什麼?

  • 管道

  • 命名管道

  • socket

  • 訊息佇列

  • 訊號

  • 訊號量

  • 共享記憶體

以上各種程序間通訊的方式詳細介紹和比較,推薦一篇文章一文掌握程序間通訊,這裡不再贅述。

對於本地程序間需要高頻次的大量資料互動,首推共享記憶體這種方案。

現代作業系統普遍採用了基於虛擬記憶體的管理方案,在這種記憶體管理方式之下,各個程序之間進行了強制隔離。程式程式碼中使用的記憶體地址均是一個虛擬地址,由作業系統的記憶體管理演算法提前分配對映到對應的實體記憶體頁面,CPU在執行程式碼指令時,對訪問到的記憶體地址再進行實時的轉換翻譯。

從上圖可以看出,不同程序之中,雖然是同一個記憶體地址,最終在作業系統和CPU的配合下,實際儲存資料的記憶體頁面卻是不同的。

而共享記憶體這種程序間通訊方案的核心在於:如果讓同一個實體記憶體頁面對映到兩個程序地址空間中,雙方不是就可以直接讀寫,而無需拷貝了嗎?

當然,共享記憶體只是最終的資料傳輸載體,雙方要實現通訊還得藉助訊號、訊號量等其他通知機制。

用上了高效能的共享記憶體通訊機制,多個服務程序之間就可以愉快的工作了,即便有工作程序出現Crash,整個服務也不至於癱瘓。


不久,老闆增加需求了,不再滿足於只能提供靜態網頁瀏覽了,需要能夠實現動態互動。這一次老闆還算良心,給你加了一臺硬體伺服器。

於是你用Java/PHP/Python等語言搞了一套web開發框架,單獨起了一個服務,用來提供動態網頁支援,和原來等靜態內容伺服器配合工作。

這個時候你發現,靜態服務和動態服務之間經常需要通訊。

一開始你用基於HTTP的RESTful介面在伺服器之間通訊,後來發現用JSON格式傳輸資料效率低下,你需要更高效的通訊方案。

這個時候你需要:

RPC && 序列化技術

什麼是RPC技術?

RPC全稱Remote Procedure Call,遠端過程呼叫。我們平時程式設計中,隨時都在呼叫函式,這些函式基本上都位於本地,也就是當前程序某一個位置的程式碼塊。但如果要呼叫的函式不在本地,而在網路上的某個伺服器上呢?這就是遠端過程呼叫的來源。

從圖中可以看出,通過網路進行功能呼叫,涉及引數的打包解包、網路的傳輸、結果的打包解包等工作。而其中對資料進行打包和解包就需要依賴序列化技術來完成。

什麼是序列化技術?

序列化簡單來說,是將記憶體中的物件轉換成可以傳輸和儲存的資料,而這個過程的逆向操作就是反序列化。序列化 && 反序列化技術可以實現將記憶體物件在本地和遠端計算機上搬運。好比把大象關進冰箱門分三步:

  • 將本地記憶體物件編碼成資料流

  • 通過網路傳輸上述資料流

  • 將收到的資料流在記憶體中構建出物件

序列化技術有很多免費開源的框架,衡量一個序列化框架的指標有這麼幾個:

  • 是否支援跨語言使用,能支援哪些語言

  • 是否只是單純的序列化功能,包不包含RPC框架

  • 序列化傳輸效能

  • 擴充套件支援能力(資料物件增刪欄位後,前後的相容性)

  • 是否支援動態解析(動態解析是指不需要提前編譯,根據拿到的資料格式定義檔案立即就能解析)

下面流行的三大序列化框架protobuf、thrift、avro的對比:

ProtoBuf:

廠商:Google

支援語言:C++、Java、Python等

動態性支援:較差,一般需要提前編譯

是否包含RPC:否

簡介:ProtoBuf是谷歌出品的序列化框架,成熟穩定,效能強勁,很多大廠都在使用。自身只是一個序列化框架,不包含RPC功能,不過可以與同是Google出品的GPRC框架一起配套使用,作為後端RPC服務開發的黃金搭檔。

缺點是對動態性支援較弱,不過在更新版本中這一現象有待改善。總體來說,ProtoBuf都是一款非常值得推薦的序列化框架。

Thrift

廠商:Facebook

支援語言:C++、Java、Python、PHP、C#、Go、JavaScript等

動態性支援:差

是否包含RPC:是

簡介:這是一個由Facebook出品的RPC框架,本身內含二進位制序列化方案,但Thrift本身的RPC和資料序列化是解耦的,你甚至可以選擇XML、JSON等自定義的資料格式。在國內同樣有一批大廠在使用,效能方面和ProtoBuf不分伯仲。缺點和ProtoBuf一樣,對動態解析的支援不太友好。

Avro

支援語言:C、C++、Java、Python、C#等

動態性支援:好

是否包含RPC:是

簡介:這是一個源自於Hadoop生態中的序列化框架,自帶RPC框架,也可獨立使用。相比前兩位最大的優勢就是支援動態資料解析。

為什麼我一直在說這個動態解析功能呢?在之前的一段專案經歷中,軒轅就遇到了三種技術的選型,擺在我們面前的就是這三種方案。需要一個C++開發的服務和一個Java開發的服務能夠進行RPC。

Protobuf和Thrift都需要通過“編譯”將對應的資料協議定義檔案編譯成對應的C++/Java原始碼,然後合入專案中一起編譯,從而進行解析。

當時,Java專案組同學非常強硬的拒絕了這一做法,其理由是這樣編譯出來的強業務型程式碼融入他們的業務無關的框架服務,而業務是常變的,這樣做不夠優雅。

最後,經過測試,最終選擇了AVRO作為我們的方案。Java一側只需要動態載入對應的資料格式檔案,就能對拿到的資料進行解析,並且效能上還不錯。(當然,對於C++一側還是選擇了提前編譯的做法)


自從你的網站支援了動態能力,免不了要和資料庫打交道,但隨著使用者的增長,你發現數據庫的查詢速度越來越慢。

這個時候,你需要:

資料庫索引技術

想想你手上有一本數學教材,但是目錄被人給撕掉了,現在要你翻到講三角函式的那一頁,你該怎麼辦?

沒有了目錄,你只有兩種辦法,要麼一頁一頁的翻,要麼隨機翻,直到找到三角函式的那一頁。

對於資料庫也是一樣的道理,如果我們的資料表沒有“目錄”,那要查詢滿足條件的記錄行,就得全表掃描,那可就惱火了。所以為了加快查詢速度,得給資料表也設定目錄,在資料庫領域中,這就是索引。

一般情況下,資料表都會有多個欄位,那根據不同的欄位也就可以設立不同的索引。

索引的分類

  • 主鍵索引

  • 聚集索引

  • 非聚集索引

主鍵我們都知道,是唯一標識一條資料記錄的欄位(也存在多個欄位一起來唯一標識資料記錄的聯合主鍵),那與之對應的就是主鍵索引了。

聚集索引是指索引的邏輯順序與表記錄的物理儲存順序一致的索引,一般情況下主鍵索引就符合這個定義,所以一般來說主鍵索引也是聚集索引。但是,這不是絕對的,在不同的資料庫中,或者在同一個資料庫下的不同儲存引擎中還是有不同。

聚集索引的葉子節點直接儲存了資料,也是資料節點,而非聚集索引的葉子節點沒有儲存實際的資料,需要二次查詢。

索引的實現原理

索引的實現主要有三種:

  • B+樹

  • 雜湊表

  • 點陣圖

其中,B+樹用的最多,其特點是樹的節點眾多,相較於二叉樹,這是一棵多叉樹,是一個扁平的胖樹,減少樹的深度有利於減少磁碟I/O次數,適宜資料庫的儲存特點。

雜湊表實現的索引也叫雜湊索引,通過雜湊函式來實現資料的定位。雜湊演算法的特點是速度快,常數階的時間複雜度,但缺點是隻適合準確匹配,不適合模糊匹配和範圍搜尋。

點陣圖索引相對就少見了。想象這麼一個場景,如果某個欄位的取值只有有限的少數幾種可能,比如性別、省份、血型等等,針對這樣的欄位如果用B+樹作為索引的話會出現什麼情況?會出現大量索引值相同的葉子節點,這實際上是一種儲存浪費。

點陣圖索引正是基於這一點進行優化,針對欄位取值只有少量有限項,資料表中該列欄位出現大量重複時,就是點陣圖索引一展身手的時機。

所謂點陣圖,就是Bitmap,其基本思想是對該欄位每一個取值建立一個二進位制點陣圖來標記資料表的每一條記錄的該列欄位是否是對應取值。

索引雖好,但也不可濫用,一方面索引最終是要儲存到磁碟上的,無疑會增加儲存開銷。另外更重要的是,資料表的增刪操作一般會伴隨對索引的更新,因此對資料庫的寫入速度也是會有一定影響。


你的網站現在訪問量越來越大了,同時線上人數大大增長。然而,大量使用者的請求帶來了後端程式對資料庫大量的訪問。漸漸的,資料庫的瓶頸開始出現,無法再支援日益增長的使用者量。老闆再一次給你下達了效能提升的任務。

快取技術 && 布隆過濾器

從物理CPU對記憶體資料的快取到瀏覽器對網頁內容的快取,快取技術遍佈於計算機世界的每一個角落。

面對當前出現的資料庫瓶頸,同樣可以用快取技術來解決。

每次訪問資料庫都需要資料庫進行查表(當然,資料庫自身也有優化措施),反映到底層就是進行一次或多次的磁碟I/O,但凡涉及I/O的就會慢下來。如果是一些頻繁用到但又不會經常變化的資料,何不將其快取在記憶體中,不必每一次都要找資料庫要,從而減輕對資料庫對壓力呢?

有需求就有市場,有市場就會有產品,以memcached和Redis為代表的記憶體物件快取系統應運而生。

快取系統有三個著名的問題:

  • 快取穿透: 快取設立的目的是為了一定層面上截獲到資料庫儲存層的請求。穿透的意思就在於這個截獲沒有成功,請求最終還是去到了資料庫,快取沒有產生應有的價值。

  • 快取擊穿: 如果把快取理解成一面擋在資料庫面前的牆壁,為資料庫“抵禦”查詢請求,所謂擊穿,就是在這面牆壁上打出了一個洞。一般發生在某個熱點資料快取到期,而此時針對該資料的大量查詢請求來臨,大家一股腦的懟到了資料庫。

  • 快取雪崩: 理解了擊穿,那雪崩就更好理解了。俗話說得好,擊穿是一個人的雪崩,雪崩是一群人的擊穿。如果快取這堵牆上處處都是洞,那這面牆還如何屹立?吃棗藥丸。

關於這三個問題的更詳細闡述,推薦一篇文章什麼是快取系統的三座大山。

有了快取系統,我們就可以在向資料庫請求之前,先詢問快取系統是否有我們需要的資料,如果有且滿足需要,我們就可以省去一次資料庫的查詢,如果沒有,我們再向資料庫請求。

注意,這裡有一個關鍵的問題,如何判斷我們要的資料是不是在快取系統中呢?

進一步,我們把這個問題抽象出來:如何快速判斷一個數據量很大的集合中是否包含我們指定的資料?

這個時候,就是布隆過濾器大顯身手的時候了,它就是為了解決這個問題而誕生的。那布隆過濾器是如何解決這個問題的呢?

先回到上面的問題中來,這其實是一個查詢問題,對於查詢問題,最常用的解決方案是搜尋樹和雜湊表兩種方案。

因為這個問題有兩個關鍵點:快速、資料量很大。樹結構首先得排除,雜湊表倒是可以做到常數階的效能,但資料量大了以後,一方面對雜湊表的容量要求巨大,另一方面如何設計一個好的雜湊演算法能夠做到如此大量資料的雜湊對映也是一個難題。

對於容量的問題,考慮到只需要判斷物件是否存在,而並非拿到物件,我們可以將雜湊表的表項大小設定為1個bit,1表示存在,0表示不存在,這樣大大縮小雜湊表的容量。

而對於雜湊演算法的問題,如果我們對雜湊演算法要求低一些,那雜湊碰撞的機率就會增加。那一個雜湊演算法容易衝突,那就多弄幾個,多個雜湊函式同時衝突的概率就小的多。

布隆過濾器就是基於這樣的設計思路:

當設定對應的key-value時,按照一組雜湊演算法的計算,將對應位元位置1。

但當對應的key-value刪除時,卻不能將對應的位元位置0,因為保不準其他某個key的某個雜湊演算法也對映到了同一個位置。

也正是因為這樣,引出了布隆過濾器的另外一個重要特點:布隆過濾器判定存在的實際上不一定存在,但判定不存在的則一定不存在。


你們公司網站的內容越來越多了,使用者對於快速全站搜尋的需求日益強烈。這個時候,你需要:

全文搜尋技術

對於一些簡單的查詢需求,傳統的關係型資料庫尚且可以應付。但搜尋需求一旦變得複雜起來,比如根據文章內容關鍵字、多個搜尋條件但邏輯組合等情況下,資料庫就捉襟見肘了,這個時候就需要單獨的索引系統來進行支援。

如今行業內廣泛使用的ElasticSearch(簡稱ES)就是一套強大的搜尋引擎。集全文檢索、資料分析、分散式部署等優點於一身,成為企業級搜尋技術的首選。

ES使用RESTful介面,使用JSON作為資料傳輸格式,支援多種查詢匹配,為各主流語言都提供了SDK,易於上手。

另外,ES常常和另外兩個開源軟體Logstash、Kibana一起,形成一套日誌收集、分析、展示的完整解決方案:ELK架構。

其中,Logstash負責資料的收集、解析,ElasticSearch負責搜尋,Kibana負責視覺化互動,成為不少企業級日誌分析管理的鐵三角。


無論我們怎麼優化,一臺伺服器的力量終究是有限的。公司業務發展迅猛,原來的伺服器已經不堪重負,於是公司採購了多臺伺服器,將原有的服務都部署了多份,以應對日益增長的業務需求。

現在,同一個服務有多個伺服器在提供服務了,需要將使用者的請求均衡的分攤到各個伺服器上,這個時候,你需要:

負載均衡技術

顧名思義,負載均衡意為將負載均勻平衡分配到多個業務節點上去。

和快取技術一樣,負載均衡技術同樣存在於計算機世界到各個角落。

按照均衡實現實體,可以分為軟體負載均衡(如LVS、Nginx、HAProxy)和硬體負載均衡(如A10、F5)。

按照網路層次,可以分為四層負載均衡(基於網路連線)和七層負載均衡(基於應用內容)。

按照均衡策略演算法,可以分為輪詢均衡、雜湊均衡、權重均衡、隨機均衡或者這幾種演算法相結合的均衡。

而對於現在遇到等問題,可以使用nginx來實現負載均衡,nginx支援輪詢、權重、IP雜湊、最少連線數目、最短響應時間等多種方式的負載均衡配置。

輪詢

upstream web-server {
    server 192.168.1.100;
    server 192.168.1.101;
}

權重

upstream web-server {
    server 192.168.1.100 weight=1;
    server 192.168.1.101 weight=2;
}

IP雜湊值

upstream web-server {
    ip_hash;
    server 192.168.1.100 weight=1;
    server 192.168.1.101 weight=2;
}

最少連線數目

upstream web-server {
    least_conn;
    server 192.168.1.100 weight=1;
    server 192.168.1.101 weight=2;
}

最短響應時間

upstream web-server {
    server 192.168.1.100 weight=1;
    server 192.168.1.101 weight=2;
    fair;  
}

總結

高效能是一個永恆的話題,其涉及的技術和知識面其實遠不止上面列出的這些。

從物理硬體CPU、記憶體、硬碟、網絡卡到軟體層面的通訊、快取、演算法、架構每一個環節的優化都是通往高效能的道路。

路漫漫其修遠兮,吾將上下而求索。

喜歡我的文章,歡迎長按識別下面的二維碼關注我,更多硬核的原創乾貨等你來發現~

往期硬核乾貨

懂了!VMware/KVM/Docker原來是這麼回事兒

震撼!全網第一張原始碼分析全景圖揭祕Nginx

看過無數Java GC文章,這5個問題你也未必知道!

雜湊表哪家強?幾大程式語言吵起來了!

一網打盡!每個程式猿都該瞭解的黑客技術大彙總