天天看點

高性能伺服器架構(二):緩存清理政策

  雖然使用緩存思想似乎是一個很簡單的事情,但是緩存機制卻有一個核心的難點,就是——緩存清理。我們所說的緩存,都是儲存一些資料,但是這些資料往往是會變化的,我們要針對這些變化,清理掉儲存的“髒”資料,卻可能不是那麼容易。

  首先我們來看看最簡單的緩存資料——靜态資料。這種資料往往在程式的運作時是不會變化的,比如Web伺服器記憶體中緩存的HTML檔案資料,就是這種。事實上,所有的不是由外部使用者上傳的資料,都屬于這種“運作時靜态資料”。一般來說,我們對這種資料,可以采用兩種建立緩存的方法:一是程式一啟動,就一股腦把所有的靜态資料從檔案或者資料庫讀入記憶體;二就是程式啟動的時候并不加載靜态資料,而是等有使用者通路相關資料的時候,才去加載,這也就是所謂lazy load的做法。第一種方法程式設計比較簡單,程式的記憶體啟動後就穩定了,不太容易出現記憶體漏洞(如果加載的緩存太多,程式在啟動後立刻會因記憶體不足而退出,比較容易發現問題);第二種方法程式啟動很快,但要對緩存占用的空間有所限制或者規劃,否則如果要緩存的資料太多,可能會耗盡記憶體,導緻線上服務中斷。

  一般來說,靜态資料是不會“髒”的,因為沒有使用者會去寫緩存中的資料。但是在實際工作中,我們的線上服務往往會需要“立刻”變更一些緩存資料。比如在門戶網站上釋出了一條新聞,我們會希望立刻讓所有通路的使用者都看到。按最簡單的做法,我們一般隻要重新開機一下伺服器程序,記憶體中的緩存就會消失了。對于靜态緩存的變化頻率非常低的業務,這樣是可以的,但是如果是新聞網站,就不能每隔幾分鐘就重新開機一下WEB伺服器程序,這樣會影響大量線上使用者的通路。常見的解決這類問題有兩種處理政策:

  第一種是使用控制指令。簡單來說,就是在伺服器程序上,開通一個實時的指令端口,我們可以通過網絡資料包(如UDP包),或者Linux系統信号(如kill SIGUSR2程序号)之類的手段,發送一個指令消息給伺服器程序,讓程序開始清理緩存。這種清理可能執行的是最簡單的“全部清理”,也有的可以細緻一點的,讓指令消息中帶有“想清理的資料ID”這樣的資訊,比如我們發送給WEB伺服器的清理消息網絡包中會帶一個字元串URL,表示要清理哪一個HTML檔案的緩存。這種做法的好處是清理的操作很精準,可以明确的控制清理的時間和資料。但是缺點就是比較繁瑣,手工去編寫發送這種指令很煩人,是以一般我們會把清理緩存指令的工作,編寫到上傳靜态資料的工具當中,比如結合到網站的内容釋出系統中,一旦編輯送出了一篇新的新聞,釋出系統的程式就自動的發送一個清理消息給WEB伺服器。

  第二種是使用字段判斷邏輯。也就是伺服器程序,會在每次讀取緩存前,根據一些特征資料,快速的判斷記憶體中的緩存和源資料内容,是否有不一緻(是否髒)的地方,如果有不一緻的地方,就自動清理這條資料的緩存。這種做法會消耗一部分CPU,但是就不需要人工去處理清理緩存的事情,自動化程度很高。現在我們的浏覽器和WEB伺服器之間,就有用這種機制:檢查檔案MD5;或者檢查檔案最後更新時間。具體的做法,就是每次浏覽器發起對WEB伺服器的請求時,除了發送URL給伺服器外,還會發送一個緩存了此URL對應的檔案内容的MD5校驗串、或者是此檔案在伺服器上的“最後更新時間”(這個校驗串和“最後更新時間”是第一次獲的檔案時一并從伺服器獲得的);伺服器收到之後,就會把MD5校驗串或者最後更新時間,和磁盤上的目标檔案進行對比,如果是一緻的,說明這個檔案沒有被修改過(緩存不是“髒”的),可以直接使用緩存。否則就會讀取目标檔案傳回新的内容給浏覽器。這種做法對于伺服器性能是有一定消耗的,是以如果往往我們還會搭配其他的緩存清理機制來用,比如我們會在設定一個“逾時檢查”的機制:就是對于所有的緩存清理檢查,我們都簡單的看看緩存存在的時間是否“逾時”了,如果超過了,才進行下一步的檢查,這樣就不用每次請求都去算MD5或者看最後更新時間了。但是這樣就存在“逾時”時間内緩存變髒的可能性。

高性能伺服器架構(二):緩存清理政策

  上面說了運作時靜态的緩存清理,現在說說運作時變化的緩存資料。在伺服器程式運作期間,如果使用者和伺服器之間的互動,導緻了緩存的資料産生了變化,就是所謂“運作時變化緩存”。比如我們玩網絡遊戲,登入之後的角色資料就會從資料庫裡讀出來,進入伺服器的緩存(可能是堆記憶體或者memcached、共享記憶體),在我們不斷進行遊戲操作的時候,對應的角色資料就會産生修改的操作,這種緩存資料就是“運作時變化的緩存”。這種運作時變化的資料,有讀和寫兩個方面的清理問題:由于緩存的資料會變化,如果另外一個程序從資料庫讀你的角色資料,就會發現和目前遊戲裡的資料不一緻;如果伺服器程序突然結束了,你在遊戲裡更新,或者撿道具的資料可能會從記憶體緩存中消失,導緻你白忙活了半天,這就是沒有回寫(緩存寫操作的清理)導緻的問題。這種情況在電子商務領域也很常見,最典型的就是火車票網上購買的系統,火車票資料緩存在記憶體必須有合适的清理機制,否則讓兩個買了同一張票就麻煩了,但如果不緩存,大量使用者同時搶票,伺服器也應對不過來。是以在運作時變化的資料緩存,應該有一些特别的緩存清理政策。

  在實際運作業務中,運作變化的資料往往是根據使用使用者的增多而增多的,是以首先要考慮的問題,就是緩存空間不夠的可能性。我們不太可能把全部資料都放到緩存的空間裡,也不可能清理緩存的時候就全部資料一起清理,是以我們一般要對資料進行分割,這種分割的政策常見的有兩種:一種是按重要級來分割,一種是按使用部分分割。

  先舉例說說“按重要級分割”,在網絡遊戲中,同樣是角色的資料,有些資料的變化可能會每次修改都立刻回寫到資料庫(清理寫緩存),其他一些資料的變化會延遲一段時間,甚至有些資料直到角色退出遊戲才回寫,如玩家的等級變化(更新了),武器裝備的獲得和消耗,這些玩家非常看重的資料,基本上會立刻回寫,這些就是所謂最重要的緩存資料。而玩家的經驗值變化、目前HP、MP的變化,就會延遲一段時間才寫,因為就算丢失了緩存,玩家也不會太過關注。最後有些比如玩家在房間(地區)裡的X/Y坐标,對話聊天的記錄,可能會退出時回寫,甚至不回寫。這個例子說的是“寫緩存”的清理,下面說說“讀緩存”的按重要級分割清理。

高性能伺服器架構(二):緩存清理政策

   假如我們寫一個網店系統,裡面容納了很多産品,這些産品有一些會被使用者頻繁檢索到,比較熱銷,而另外一些商品則沒那麼熱銷。熱銷的商品的餘額、銷量、評價都會比較頻繁的變化,而滞銷的商品則變化很少。是以我們在設計的時候,就應該按照不同商品的通路頻繁程度,來決定緩存哪些商品的資料。我們在設計緩存的結構時,就應該建構一個可以統計緩存讀寫次數的名額,如果有些資料的讀寫頻率過低,或者空閑(沒有人讀、寫緩存)時間超長,緩存應該主動清理掉這些資料,以便其他新的資料能進入緩存。這種政策也叫做“冷熱交換”政策。實作“冷熱交換”的政策時,關鍵是要定義一個合理的冷熱統計算法。一些固定的名額和算法,往往并不能很好的應對不同硬體、不同網絡情況下的變化,是以現在人們普遍會用一些動态的算法,如Redis就采用了5種,他們是:

1.根據過期時間,清理最長時間沒用過的

2.根據過期時間,清理即将過期的

3.根據過期時間,任意清理一個

4. 無論是否過期,随機清理

5.無論是否過期,根據LRU原則清理:所謂LRU,就是Least Recently Used,最近最久未使用過。這個原則的思想是:如果一個資料在最近一段時間沒有被通路到,那麼在将來他被通路的可能性也很小。LRU是在作業系統中很常見的一種原則,比如記憶體的頁面置換算法(也包括FIFO,LFU等),對于LRU的實作,還是非常有技巧的,但是本文就不詳細去說明如何實作,留待大家上網搜尋“LRU”關鍵字學習。

高性能伺服器架構(二):緩存清理政策

  資料緩存的清理政策其實遠不止上面所說的這些,要用好緩存這個武器,就要仔細研究需要緩存的資料特征,他們的讀寫分布,資料之中的差别。然後最大化的利用業務領域的知識,來設計最合理的緩存清理政策。這個世界上不存在萬能的優化緩存清理政策,隻存在針對業務領域最優化的政策,這需要我們程式員深入了解業務領域,去發現資料背後的規律。