1. 程式人生 > >高效能伺服器架構思路(一)——緩衝策略

高效能伺服器架構思路(一)——緩衝策略

在伺服器端程式開發領域,效能問題一直是備受關注的重點。業界有大量的框架、元件、類庫都是以效能為賣點而廣為人知。然而,伺服器端程式在效能問題上應該有何種基本思路,這個卻很少被這些專案的文件提及。本文正式希望介紹伺服器端解決效能問題的基本策略和經典實踐,並分為幾個部分來說明:

  1. 快取策略的概念和例項

  2. 快取策略的難點:不同特點的快取資料的清理機制

  3. 分佈策略的概念和例項

  4. 分佈策略的難點:共享資料安全性與程式碼複雜度的平衡

快取策略的概念

我們提到伺服器端效能問題的時候,往往會混淆不清。因為當我們訪問一個伺服器時,出現服務卡住不能得到資料,就會認為是“效能問題”。但是實際上這個效能問題可能是有不同的原因,表現出來都是針對客戶請求的延遲很長甚至中斷。我們來看看這些原因有哪些:第一個是所謂併發數不足,也就是同時請求的客戶過多,導致超過容納能力的客戶被拒絕服務,這種情況往往會因為伺服器記憶體耗盡而導致的;第二個是處理延遲過長,也就是有一些客戶的請求處理時間已經超過使用者可以忍受的長度,這種情況常常表現為CPU佔用滿額100%。

我們在伺服器開發的時候,最常用到的有下面這幾種硬體:CPU、記憶體、磁碟、網絡卡。其中CPU是代表計算機處理時間的,硬碟的空間一般很大,主要是讀寫磁碟會帶來比較大的處理延遲,而記憶體、網絡卡則是受儲存、頻寬的容量限制的。所以當我們的伺服器出現效能問題的時候,就是這幾個硬體某一個甚至幾個都出現負荷佔滿的情況。這四個硬體的資源一般可以抽象成兩類:一類是時間資源,比如CPU和磁碟讀寫;一類是空間資源,比如記憶體和網絡卡頻寬。所以當我們的伺服器出現效能問題,有一個最基本的思路,就是——時間空間轉換。我們可以舉幾個例子來說明這個問題。

水壩就是用水庫空間來換流量時間的例子

當我們訪問一個WEB的網站的時候,輸入的URL地址會被伺服器變成對磁碟上某個檔案的讀取。如果有大量的使用者訪問這個網站,每次的請求都會造成對磁碟的讀操作,可能會讓磁碟不堪重負,導致無法即時讀取到檔案內容。但是如果我們寫的程式,會把讀取過一次的檔案內容,長時間的儲存在記憶體中,當有另外一個對同樣檔案的讀取時,就直接從記憶體中把資料返回給客戶端,就無需去讓磁碟讀取了。由於使用者訪問的檔案往往很集中,所以大量的請求可能都能從記憶體中找到儲存的副本,這樣就能大大提高伺服器能承載的訪問量了。這種做法,就是用記憶體的空間,換取了磁碟的讀寫時間,屬於用空間換時間的策略。


方便麵預先快取了大量的烹飪操作

舉另外一個例子:我們寫一個網路遊戲的伺服器端程式,通過讀寫資料庫來提供玩家資料存檔。如果有大量玩家進入這個伺服器,必定有很多玩家的資料資料變化,比如升級、獲得武器等等,這些通過讀寫資料庫來實現的操作,可能會讓資料庫程序負荷過重,導致玩家無法即時完成遊戲操作。我們會發現遊戲中的讀操作,大部分都是針是對一些靜態資料的,比如遊戲中的關卡資料、武器道具的具體資訊;而很多寫操作,實際上是會覆蓋的,比如我的經驗值,可能每打一個怪都會增加幾十點,但是最後記錄的只是最終的一個經驗值,而不會記錄下打怪的每個過程。所以我們也可以使用時空轉換的策略來提供效能:我們可以用記憶體,把那些遊戲中的靜態資料,都一次性讀取並儲存起來,這樣每次讀這些資料,都和資料庫無關了;而玩家的資料資料,則不是每次變化都去寫資料庫,而是先在記憶體中保持一個玩家資料的副本,所有的寫操作都先去寫記憶體中的結構,然後定期再由伺服器主動寫回到資料庫中,這樣可以把多次的寫資料庫操作變成一次寫操作,也能節省很多寫資料庫的消耗。這種做法也是用空間換時間的策略。

拼裝傢俱很省運輸空間,但是安裝很費時

最後說說用時間換空間的例子:假設我們要開發一個企業通訊錄的資料儲存系統,客戶要求我們能儲存下通訊錄的每次新增、修改、刪除操作,也就是這個資料的所有變更歷史,以便可以讓資料回退到任何一個過去的時間點。那麼我們最簡單的做法,就是這個資料在任何變化的時候,都拷貝一份副本。但是這樣會非常的浪費磁碟空間,因為這個資料本身變化的部分可能只有很小一部分,但是要拷貝的副本可能很大。這種情況下,我們就可以在每次資料變化的時候,都記下一條記錄,內容就是資料變化的情況:插入了一條內容是某某的聯絡方法、刪除了一條某某的聯絡方法……,這樣我們記錄的資料,僅僅就是變化的部分,而不需要拷貝很多份副本。當我們需要恢復到任何一個時間點的時候,只需要按這些記錄依次對資料修改一遍,直到指定的時間點的記錄即可。這個恢復的時間可能會有點長,但是卻可以大大節省儲存空間。這就是用CPU的時間來換磁碟的儲存空間的策略。我們現在常見的MySQL InnoDB日誌型資料表,以及SVN原始碼儲存,都是使用這種策略的。

另外,我們的Web伺服器,在傳送HTML檔案內容的時候,往往也會先用ZIP壓縮,然後傳送給瀏覽器,瀏覽器收到後要先解壓,然後才能顯示,這個也是用伺服器和客戶端的CPU時間,來換取網路頻寬的空間。

在我們的計算機體系中,快取的思路幾乎無處不在,比如我們的CPU裡面就有1級快取、2級快取,他們就是為了用這些快速的儲存空間,換取對記憶體這種相對比較慢的儲存空間的等待時間。我們的顯示卡里面也帶有大容量的快取,他們是用來儲存顯示圖形的運算結果的。

通往大空間的郊區路上容易交通堵塞

快取的本質,除了讓“已經處理過的資料,不需要重複處理”以外,還有“以快速的資料儲存讀寫,代替較慢速的儲存讀寫”的策略。我們在選擇快取策略進行時空轉換的時候,必須明確我們要轉換的時間和空間是否合理,是否能達到效果。比如早期有一些人會把WEB檔案快取在分散式磁碟上(例如NFS),但是由於通過網路訪問磁碟本身就是一個比較慢的操作,而且還會佔用可能就不充裕的網路頻寬空間,導致效能可能變得更慢。

在設計快取機制的時候,我們還容易碰到另外一個風險,就是對快取資料的程式設計處理問題。如果我們要快取的資料,並不是完全無需處理直接讀寫的,而是需要讀入記憶體後,以某種語言的結構體或者物件來處理的,這就需要涉及到“序列化”和“反序列化”的問題。如果我們採用直接拷貝記憶體的方式來快取資料,當我們的這些資料需要跨程序、甚至跨語言訪問的時候,會出現那些指標、ID、控制代碼資料的失效。因為在另外一個程序空間裡,這些“標記型”的資料都是不存在的。因此我們需要更深入的對資料快取的方法,我們可能會使用所謂深拷貝的方案,也就是跟著那些指標去找出目標記憶體的資料,一併拷貝。一些更現代的做法,則是使用所謂序列化方案來解決這個問題,也就是用一些明確定義了的“拷貝方法”來定義一個結構體,然後使用者就能明確的知道這個資料會被拷貝,直接取消了指標之類的記憶體地址資料的存在。比如著名的Protocol Buffer就能很方便的進行記憶體、磁碟、網路位置的快取;現在我們常見的JSON,也被一些系統用來作為快取的資料格式。

但是我們需要注意的是,快取的資料和我們程式真正要操作的資料,往往是需要進行一些拷貝和運算的,這就是序列化和反序列化的過程,這個過程很快,也有可能很慢。所以我們在選擇資料快取結構的時候,必須要注意其轉換時間,否則你快取的效果可能被這些資料拷貝、轉換消耗去很多,嚴重的甚至比不快取更差。一般來說,快取的資料越解決使用時的記憶體結構,其轉換速度就越快,在這點上,Protocol Buffer採用TLV編碼,就比不上直接memcpy的一個C結構體,但是比編碼成純文字的XML或者JSON要來的更快。因為編解碼的過程往往要進行復雜的查表對映,列表結構等操作。