在服務器端程序開發(fā)領域咐刨,性能問題一直是備受關注的重點码俩。業(yè)界有大量的框架度帮、組件、類庫都是以性能為賣點而廣為人知稿存。然而笨篷,服務器端程序在性能問題上應該有何種基本思路,這個卻很少被這些項目的文檔提及瓣履。本文正式希望介紹服務器端解決性能問題的基本策略和經(jīng)典實踐率翅,并分為幾個部分來說明:
緩存策略的概念和實例
緩存策略的難點:不同特點的緩存數(shù)據(jù)的清理機制
分布策略的概念和實例
分布策略的難點:共享數(shù)據(jù)安全性與代碼復雜度的平衡
緩存策略的概念
? ? ? ? 我們提到服務器端性能問題的時候,往往會混淆不清拂苹。因為當我們訪問一個服務器時安聘,出現(xiàn)服務卡住不能得到數(shù)據(jù),就會認為是“性能問題”瓢棒。但是實際上這個性能問題可能是有不同的原因浴韭,表現(xiàn)出來都是針對客戶請求的延遲很長甚至中斷。我們來看看這些原因有哪些:第一個是所謂并發(fā)數(shù)不足脯宿,也就是同時請求的客戶過多念颈,導致超過容納能力的客戶被拒絕服務,這種情況往往會因為服務器內(nèi)存耗盡而導致的连霉;第二個是處理延遲過長榴芳,也就是有一些客戶的請求處理時間已經(jīng)超過用戶可以忍受的長度嗡靡,這種情況常常表現(xiàn)為CPU占用滿額100%。
? ? ? ? 我們在服務器開發(fā)的時候窟感,最常用到的有下面這幾種硬件:CPU讨彼、內(nèi)存、磁盤柿祈、網(wǎng)卡哈误。其中CPU是代表計算機處理時間的,硬盤的空間一般很大躏嚎,主要是讀寫磁盤會帶來比較大的處理延遲蜜自,而內(nèi)存、網(wǎng)卡則是受存儲卢佣、帶寬的容量限制的重荠。所以當我們的服務器出現(xiàn)性能問題的時候,就是這幾個硬件某一個甚至幾個都出現(xiàn)負荷占滿的情況虚茶。這四個硬件的資源一般可以抽象成兩類:一類是時間資源戈鲁,比如CPU和磁盤讀寫;一類是空間資源媳危,比如內(nèi)存和網(wǎng)卡帶寬荞彼。所以當我們的服務器出現(xiàn)性能問題,有一個最基本的思路待笑,就是——時間空間轉(zhuǎn)換鸣皂。我們可以舉幾個例子來說明這個問題。
[水壩就是用水庫空間來換流量時間的例子]
? ? ? ? 當我們訪問一個WEB的網(wǎng)站的時候暮蹂,輸入的URL地址會被服務器變成對磁盤上某個文件的讀取寞缝。如果有大量的用戶訪問這個網(wǎng)站,每次的請求都會造成對磁盤的讀操作仰泻,可能會讓磁盤不堪重負荆陆,導致無法即時讀取到文件內(nèi)容。但是如果我們寫的程序集侯,會把讀取過一次的文件內(nèi)容被啼,長時間的保存在內(nèi)存中,當有另外一個對同樣文件的讀取時棠枉,就直接從內(nèi)存中把數(shù)據(jù)返回給客戶端浓体,就無需去讓磁盤讀取了。由于用戶訪問的文件往往很集中辈讶,所以大量的請求可能都能從內(nèi)存中找到保存的副本命浴,這樣就能大大提高服務器能承載的訪問量了。這種做法,就是用內(nèi)存的空間生闲,換取了磁盤的讀寫時間媳溺,屬于用空間換時間的策略。
[方便面預先緩存了大量的烹飪操作]
? ? ? ? 舉另外一個例子:我們寫一個網(wǎng)絡游戲的服務器端程序碍讯,通過讀寫數(shù)據(jù)庫來提供玩家資料存檔悬蔽。如果有大量玩家進入這個服務器,必定有很多玩家的數(shù)據(jù)資料變化捉兴,比如升級屯阀、獲得武器等等,這些通過讀寫數(shù)據(jù)庫來實現(xiàn)的操作轴术,可能會讓數(shù)據(jù)庫進程負荷過重,導致玩家無法即時完成游戲操作钦无。我們會發(fā)現(xiàn)游戲中的讀操作逗栽,大部分都是針是對一些靜態(tài)數(shù)據(jù)的,比如游戲中的關卡數(shù)據(jù)失暂、武器道具的具體信息彼宠;而很多寫操作,實際上是會覆蓋的弟塞,比如我的經(jīng)驗值凭峡,可能每打一個怪都會增加幾十點,但是最后記錄的只是最終的一個經(jīng)驗值决记,而不會記錄下打怪的每個過程摧冀。所以我們也可以使用時空轉(zhuǎn)換的策略來提供性能:我們可以用內(nèi)存,把那些游戲中的靜態(tài)數(shù)據(jù)系宫,都一次性讀取并保存起來索昂,這樣每次讀這些數(shù)據(jù),都和數(shù)據(jù)庫無關了扩借;而玩家的資料數(shù)據(jù)椒惨,則不是每次變化都去寫數(shù)據(jù)庫,而是先在內(nèi)存中保持一個玩家數(shù)據(jù)的副本潮罪,所有的寫操作都先去寫內(nèi)存中的結構康谆,然后定期再由服務器主動寫回到數(shù)據(jù)庫中,這樣可以把多次的寫數(shù)據(jù)庫操作變成一次寫操作嫉到,也能節(jié)省很多寫數(shù)據(jù)庫的消耗沃暗。這種做法也是用空間換時間的策略。
[拼裝家具很省運輸空間屯碴,但是安裝很費時]
? ? ? ? 最后說說用時間換空間的例子:假設我們要開發(fā)一個企業(yè)通訊錄的數(shù)據(jù)存儲系統(tǒng)描睦,客戶要求我們能保存下通訊錄的每次新增、修改导而、刪除操作忱叭,也就是這個數(shù)據(jù)的所有變更歷史隔崎,以便可以讓數(shù)據(jù)回退到任何一個過去的時間點。那么我們最簡單的做法韵丑,就是這個數(shù)據(jù)在任何變化的時候爵卒,都拷貝一份副本。但是這樣會非常的浪費磁盤空間撵彻,因為這個數(shù)據(jù)本身變化的部分可能只有很小一部分钓株,但是要拷貝的副本可能很大。這種情況下陌僵,我們就可以在每次數(shù)據(jù)變化的時候轴合,都記下一條記錄,內(nèi)容就是數(shù)據(jù)變化的情況:插入了一條內(nèi)容是某某的聯(lián)系方法碗短、刪除了一條某某的聯(lián)系方法……受葛,這樣我們記錄的數(shù)據(jù),僅僅就是變化的部分偎谁,而不需要拷貝很多份副本总滩。當我們需要恢復到任何一個時間點的時候,只需要按這些記錄依次對數(shù)據(jù)修改一遍巡雨,直到指定的時間點的記錄即可闰渔。這個恢復的時間可能會有點長,但是卻可以大大節(jié)省存儲空間。這就是用CPU的時間來換磁盤的存儲空間的策略。我們現(xiàn)在常見的MySQLInnoDB日志型數(shù)據(jù)表叹卷,以及SVN源代碼存儲,都是使用這種策略的炕舵。
另外,我們的Web服務器跟畅,在發(fā)送HTML文件內(nèi)容的時候咽筋,往往也會先用ZIP壓縮,然后發(fā)送給瀏覽器徊件,瀏覽器收到后要先解壓奸攻,然后才能顯示,這個也是用服務器和客戶端的CPU時間虱痕,來換取網(wǎng)絡帶寬的空間睹耐。
在我們的計算機體系中,緩存的思路幾乎無處不在部翘,比如我們的CPU里面就有1級緩存硝训、2級緩存,他們就是為了用這些快速的存儲空間,換取對內(nèi)存這種相對比較慢的存儲空間的等待時間窖梁。我們的顯示卡里面也帶有大容量的緩存赘风,他們是用來存儲顯示圖形的運算結果的。
[通往大空間的郊區(qū)路上容易交通堵塞]
緩存的本質(zhì)纵刘,除了讓“已經(jīng)處理過的數(shù)據(jù)邀窃,不需要重復處理”以外,還有“以快速的數(shù)據(jù)存儲讀寫假哎,代替較慢速的存儲讀寫”的策略瞬捕。我們在選擇緩存策略進行時空轉(zhuǎn)換的時候,必須明確我們要轉(zhuǎn)換的時間和空間是否合理舵抹,是否能達到效果肪虎。比如早期有一些人會把WEB文件緩存在分布式磁盤上(例如NFS),但是由于通過網(wǎng)絡訪問磁盤本身就是一個比較慢的操作惧蛹,而且還會占用可能就不充裕的網(wǎng)絡帶寬空間笋轨,導致性能可能變得更慢。
在設計緩存機制的時候赊淑,我們還容易碰到另外一個風險,就是對緩存數(shù)據(jù)的編程處理問題仅讽。如果我們要緩存的數(shù)據(jù)陶缺,并不是完全無需處理直接讀寫的,而是需要讀入內(nèi)存后洁灵,以某種語言的結構體或者對象來處理的饱岸,這就需要涉及到“序列化”和“反序列化”的問題。如果我們采用直接拷貝內(nèi)存的方式來緩存數(shù)據(jù)徽千,當我們的這些數(shù)據(jù)需要跨進程苫费、甚至跨語言訪問的時候,會出現(xiàn)那些指針双抽、ID百框、句柄數(shù)據(jù)的失效。因為在另外一個進程空間里牍汹,這些“標記型”的數(shù)據(jù)都是不存在的铐维。因此我們需要更深入的對數(shù)據(jù)緩存的方法,我們可能會使用所謂深拷貝的方案慎菲,也就是跟著那些指針去找出目標內(nèi)存的數(shù)據(jù)嫁蛇,一并拷貝。一些更現(xiàn)代的做法露该,則是使用所謂序列化方案來解決這個問題睬棚,也就是用一些明確定義了的“拷貝方法”來定義一個結構體,然后用戶就能明確的知道這個數(shù)據(jù)會被拷貝,直接取消了指針之類的內(nèi)存地址數(shù)據(jù)的存在抑党。比如著名的Protocol Buffer就能很方便的進行內(nèi)存包警、磁盤、網(wǎng)絡位置的緩存新荤;現(xiàn)在我們常見的JSON揽趾,也被一些系統(tǒng)用來作為緩存的數(shù)據(jù)格式。
但是我們需要注意的是苛骨,緩存的數(shù)據(jù)和我們程序真正要操作的數(shù)據(jù)篱瞎,往往是需要進行一些拷貝和運算的,這就是序列化和反序列化的過程痒芝,這個過程很快俐筋,也有可能很慢。所以我們在選擇數(shù)據(jù)緩存結構的時候严衬,必須要注意其轉(zhuǎn)換時間澄者,否則你緩存的效果可能被這些數(shù)據(jù)拷貝、轉(zhuǎn)換消耗去很多请琳,嚴重的甚至比不緩存更差粱挡。一般來說,緩存的數(shù)據(jù)越解決使用時的內(nèi)存結構俄精,其轉(zhuǎn)換速度就越快询筏,在這點上,Protocol Buffer采用TLV編碼竖慧,就比不上直接memcpy的一個C結構體嫌套,但是比編碼成純文本的XML或者JSON要來的更快。因為編解碼的過程往往要進行復雜的查表映射圾旨,列表結構等操作踱讨。