面臨的問題
對於高並發高訪問的Web應用程序來說數據庫存取瓶頸一直是個令人頭疼的問題特別當你的程序架構還是建立在單數據庫模式而一個數據池連接數峰值已經達到的時候那你的程序運行離崩潰的邊緣也不遠了很多小網站的開發人員一開始都將注意力放在了產品需求設計上缺忽視了程序整體性能可擴展性等方面的考慮結果眼看著訪問量一天天網上爬可突然發現有一天網站因為訪問量過大而崩潰了到時候哭都來不及所以我們一定要未雨綢缪在數據庫還沒罷工前想方設法給它減負這也是這篇文章的主要議題
大家都知道當有一個request過來後web服務器交給app服務器app處理並從db中存取相關數據但db存取的花費是相當高昂的特別是每次都取相同的數據等於是讓數據庫每次都在做高耗費的無用功數據庫如果會說話肯定會發牢騷你都問了這麼多遍了難道還記不住嗎?是啊如果app拿到第一次數據並存到內存裡下次讀取時直接從內存裡讀取而不用麻煩數據庫這樣不就給數據庫減負了?而且從內存取數據必然要比從數據庫媒介取快很多倍反而提升了應用程序的性能
因此我們可以在web/app層與db層之間加一層cache層主要目的 減少數據庫讀取負擔 提高數據讀取速度而且cache存取的媒介是內存而一台服務器的內存容量一般都是有限制的不像硬盤容量可以做到TB級別所以可以考慮采用分布式的cache層這樣更易於破除內存容量的限制同時又增加了靈活性
Memcached 介紹
Memcached是開源的分布式cache系統現在很多的大型web應用程序包括facebookyoutubewikipediayahoo等等都在使用memcached來支持他們每天數億級的頁面訪問通過把cache層與他們的web架構集成他們的應用程序在提高了性能的同時還大大降低了數據庫的負載
具體的memcached資料大家可以直接從它的官方網站[]上得到這裡我就簡單給大家介紹一下memcached的工作原理
Memcached處理的原子是每一個(keyvalue)對(以下簡稱kv對)key會通過一個hash算法轉化成hashkey便於查找對比以及做到盡可能的散列同時memcached用的是一個二級散列通過一張大hash表來維護
Memcached有兩個核心組件組成服務端(ms)和客戶端(mc)在一個memcached的查詢中mc先通過計算key的hash值來確定kv對所處在的ms位置當ms確定後客戶端就會發送一個查詢請求給對應的ms讓它來查找確切的數據因為這之間沒有交互以及多播協議所以memcached交互帶給網絡的影響是最小化的
舉例說明考慮以下這個場景有三個mc分別是XYZ還有三個ms分別是ABC
設置kv對
X想設置key=foovalue=seattle
X拿到ms列表並對key做hash轉化根據hash值確定kv對所存的ms位置
B被選中了
X連接上BB收到請求把(key=foovalue=seattle)存了起來
獲取kv對
Z想得到key=foo的value
Z用相同的hash算法算出hash值並確定key=foo的值存在B上
Z連接上B並從B那邊得到value=seattle
其他任何從XYZ的想得到key=foo的值的請求都會發向B
Memcached服務器(ms)
內存分配
默認情況下ms是用一個內置的叫塊分配器的組件來分配內存的捨棄c++標准的malloc/free的內存分配而采用塊分配器的主要目的是為了避免內存碎片否則操作系統要花費更多時間來查找這些邏輯上連續的內存塊(實際上是斷開的)用了塊分配器ms會輪流的對內存進行大塊的分配並不斷重用當然由於塊的大小各不相同當數據大小和塊大小不太相符的情況下還是有可能導致內存的浪費
同時ms對key和data都有相應的限制key的長度不能超過字節data也不能超過塊大小的限制 MB
因為mc所使用的hash算法並不會考慮到每個ms的內存大小理論上mc會分配概率上等量的kv對給每個ms這樣如果每個ms的內存都不太一樣那可能會導致內存使用率的降低所以一種替代的解決方案是根據每個ms的內存大小找出他們的最大公約數然後在每個ms上開n個容量=最大公約數的instance這樣就等於擁有了多個容量大小一樣的子ms從而提供整體的內存使用率
緩存策略
當ms的hash表滿了之後新的插入數據會替代老的數據更新的策略是LRU(最近最少使用)以及每個kv對的有效時限Kv對存儲有效時限是在mc端由app設置並作為參數傳給ms的
同時ms采用是偷懶替代法ms不會開額外的進程來實時監測過時的kv對並刪除而是當且僅當新來一個插入的數據而此時又沒有多余的空間放了才會進行清除動作
緩存數據庫查詢
現在memcached最流行的一種使用方式是緩存數據庫查詢下面舉一個簡單例子說明
App需要得到userid=xxx的用戶信息對應的查詢語句類似
SELECT * FROM users WHERE userid = xxx
App先去問cache有沒有user:userid(key定義可預先定義約束好)的數據如果有返回數據如果沒有App會從數據庫中讀取數據並調用cache的add函數把數據加入cache中
當取的數據需要更新app會調用cache的update函數來保持數據庫與cache的數據同步
從上面的例子我們也可以發現一旦數據庫的數據發現變化我們一定要及時更新cache中的數據來保證app讀到的是同步的正確數據當然我們可以通過定時器方式記錄下cache中數據的失效時間時間一過就會激發事件對cache進行更新但這之間總會有時間上的延遲導致app可能從cache讀到髒數據這也被稱為狗洞問題(以後我會專門描述研究這個問題)
數據冗余與故障預防
從設計角度上memcached是沒有數據冗余環節的它本身就是一個大規模的高性能cache層加入數據冗余所能帶來的只有設計的復雜性和提高系統的開支
當一個ms上丟失了數據之後app還是可以從數據庫中取得數據不過更謹慎的做法是在某些ms不能正常工作時提供額外的ms來支持cache這樣就不會因為app從cache中取不到數據而一下子給數據庫帶來過大的負載
同時為了減少某台ms故障所帶來的影響可以使用熱備份方案就是用一台新的ms來取代有問題的ms當然新的ms還是要用原來ms的IP地址大不了數據重新裝載一遍
另外一種方式就是提高你ms的節點數然後mc會實時偵查每個節點的狀態如果發現某個節點長時間沒有響應就會從mc的可用server列表裡刪除並對server節點進行重新hash定位當然這樣也會造成的問題是原本key存儲在B上變成存儲在C上了所以此方案本身也有其弱點最好能和熱備份方案結合使用就可以使故障造成的影響最小化
Memcached客戶端(mc)
Memcached客戶端有各種語言的版本供大家使用包括javac等等具體可參見memcached api page[]
大家可以根據自己項目的需要選擇合適的客戶端來集成
緩存式的Web應用程序架構
有了緩存的支持我們可以在傳統的app層和db層之間加入cache層每個app服務器都可以綁定一個mc每次數據的讀取都可以從ms中取得如果沒有再從db層讀取而當數據要進行更新時除了要發送update的sql給db層同時也要將更新的數據發給mc讓mc去更新ms中的數據
假設今後我們的數據庫可以和ms進行通訊了那可以將更新的任務統一交給db層每次數據庫更新數據的同時會自動去更新ms中的數據這樣就可以進一步減少app層的邏輯復雜度如下圖
不過每次我們如果沒有從cache讀到數據都不得不麻煩數據庫為了最小化數據庫的負載壓力我們可以部署數據庫復寫用slave數據庫來完成讀取操作而master數據庫永遠只負責三件事更新數據同步slave數據庫更新cache如下圖
以上這些緩存式web架構在實際應用中被證明是能有效並能極大地降低數據庫的負載同時又能提高web的運行性能當然這些架構還可以根據具體的應用環境進行變種以達到不同硬件條件下性能的最優化
未來的憧憬
Memcached的出現可以說是革命性的第一次讓我們意識到可以用內存作為存儲媒介來大規模的緩存數據以提高程序的性能不過它畢竟還是比較新的東西還需要很多有待優化和改進的地方例如
如何利用memcached實現cache數據庫讓數據庫跑在內存上這方面tangent software 開發的memcached_engine[]已經做了不少工作不過現在的版本還只是處於實驗室階段
如何能方便有效的進行批量key清理因為現在key是散列在不同的server上的所以對某類key進行大批量清理是很麻煩的因為memcached本身是一個大hash表是不具備key的檢索功能的所以memcached是壓根不知道某一類的key到底存了多少個都存在哪些server上而這類功能在實際應用中卻是經常用到
From:http://tw.wingwit.com/Article/program/net/201311/12995.html