1. 程式人生 > >高性能服務器架構(一):緩沖策略

高性能服務器架構(一):緩沖策略

lin 特點 領域 思路 不能 查表 edi 操作 帶寬

原文鏈接:https://mp.weixin.qq.com/s?__biz=MzA5ODExMTkwMA==&mid=402675187&idx=1&sn=d240f6d1430b86bc007c8e79d3200e08&scene=4#wechat_redirect

  在服務器端程序開發領域,性能問題一直是備受關註的重點。業界有大量的框架、組件、類庫都是以性能為賣點而廣為人知。然而,服務器端程序在性能問題上應該有何種基本思路,這個卻很少被這些項目的文檔提及。本文正式希望介紹服務器端解決性能問題的基本策略和經典實踐 —— 由韓大分享。

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

  1. 緩存策略的概念和實例

  2. 緩存策略的難點:不同特點的緩存數據的清理機制

  3. 分布策略的概念和實例

  4. 分布策略的難點:共享數據安全性與代碼復雜度的平衡

緩存策略的概念

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

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

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

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

技術分享

[方便面預先緩存了大量的烹飪操作]

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

[拼裝家具很省運輸空間,但是安裝很費時]

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

  另外,我們的Web服務器,在發送HTML文件內容的時候,往往也會先用ZIP壓縮,然後發送給瀏覽器,瀏覽器收到後要先解壓,然後才能顯示,這個也是用服務器和客戶端的CPU時間,來換取網絡帶寬的空間。技術分享

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

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

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

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

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

高性能服務器架構(一):緩沖策略