Memcached學習總結

一. 為什么使用緩存

如圖1,為了快速應對早期的業務快速發展,我們架設一個超級簡單的Web服務,只有一臺應用服務器和DB,這種架構簡單,便于快速開發和部署。但隨著應用服務器的QPS不斷增長,水漲船高,DB的QPS也逐漸提升,對DB的響應時間也有很高要求,單DB已無法快速滿足業務發展。這時候可以考慮對DB進行分庫分表,或者讀寫分離等方式以滿足業務的發展。但是這些方式仍然有很多問題:

  • 性能提升有限,很難達到數量級上的提升,至于原因,我將再寫一篇文章論述。如果業務發展迅速,訪問量將會指數上升,僅僅依賴DB對于響應時間的降低幫助不大。
  • 成本高昂,為了應對N倍訪問量,需要增加N倍DB服務器,成本難以接受。
  • DB為了數據高可靠,犧牲性能,具體表現為數據是存儲到硬盤中,而不是內存,這樣即使DB重啟保證數據不會丟失。所以訪問DB常常要從硬盤讀取數據,從圖2可以看出硬盤的讀取時間遠遠大于內存的。


    圖1 最簡單的Web架構
圖2 服務器不同級別的訪問時間

在大部分業務場景下,數據的讀取符合二八原則,即80%的訪問量是落到20%的熱點數據上,假設我們把這20%熱點數據通過內存保存起來,在訪問這20%數據時先訪問保存的內存地方,這就是緩存。這將極大提升數據讀取速度,最終降低請求的響應時間,提高QPS。為了解決上面提出的問題,引入緩存組件,將熱點數據緩存到內存中,架構圖如圖3所示。應用服務器讀取數據的順序為:

  • 第一步,從緩存讀取數據,如果有數據,直接返回數據;
  • 第二步,從DB讀取數據,保存到緩存,返回數據。

因此,Web架構引入緩存后:

  • 提升數據讀取速度,降低請求響應時間,承受更多的請求量;
  • 提升系統擴展能力,緩存的內存空間不足,可以橫向擴展緩存組件,提升系統承載能力;
  • 降低存儲成本,Cache+DB承受更多請求量,大大減少原有需要承受這么多請求量的DB服務器。


    圖3 引入緩存的簡單Web架構

二. 緩存分類

選擇緩存作為Web架構的數據讀取組件后,下一步根據業務場景選擇具體的緩存組件,從緩存數據分布的特點對比表如下。

  • 本地緩存如果使用在數據變化率高的場景,即無明顯高頻次的數據讀取,緩存數據使用內存越多,挪用Web應用的內存資源就越多,反而降低應用的性能;
  • 伸縮性差表現在無法橫向擴展本地緩存,并且如果要清除緩存內容,只能逐臺服務器清除,達不到分布式緩存的一次性清除緩存的效果。
使用場景 優點 缺點 代表例子
本地緩存 數據變化率較低 存儲任意數據,無網絡存取延遲,數據查詢更快 占用應用系統資源,容量和伸縮性差 HashMap、GuavaCache、OSCache、JCache、Ehcache
分布式緩存 數據變化率較高 容量和伸縮性好 網絡開銷大,響應時間長 MC、Redis、Tair

從數據存儲方式看,對比表如下。

  • 內存模式要考慮重啟機器內存數據丟失,運維復雜度也很高,但是相對于磁盤持久化模式而言,少了維護數據持久化到硬盤的功能,所以運維復雜度相對較低。
特點 運維復雜度 代表例子
內存模式 所有數據內存Cache 相對較低 Memcached
磁盤持久化模式 熱點數據內存Cache,非熱點數據磁盤存儲 Redis、Tair

三. Memcached特性

Memcached作為一款經典的緩存組件,具備極高的讀取性能,下面將從四個特性分析Memcached的高性能。

  • 協議簡單
    • Memcached支持文本和二進制協議;
    • 文本協議調試簡單,內容可視化;
    • 二進制性能高效,且相對文本協議安全性高。
  • 基于libevent的事件處理
    • 使用IO多路復用的IO模型,Linux系統下使用epoll處理數據讀寫,具備極高的IO性能。
  • 內置內存存儲方式
    • 所有數據存放在內存,相對于Linux提供的malloc/free產生的內存碎片,Memcached獨特的內存存儲方式可以避免內存碎片,提高內存利用率和性能。
    • 因為數據存儲在內存中,所以重啟Memcached和操作系統,數據將全部丟失。
  • Memcached互不通信的分布式
    • Memcached實際上不是一個真正的分布式服務器,集群的各個Memcached服務器不互相通信以共享數據,分布式特性通過客戶端實現。實際上這也避免分布式集群特有的問題:腦裂。

Memcached協議和基于libevent的事件處理都不是Memcached特有的特性,所以本文重點分享Memcached內置內存存儲方式和不互相通信的分布式的特性。

四. Slab Allocation

傳統的內存分配是通過malloc/free實現,易產生內存碎片,加重操作系統內存管理器的負擔。Memcached使用Slab Allocation機制高效管理內存,Slab Allocation機制按照一個特定的增長比例,將分配的內存分割成特定長度的塊,完全解決內存碎片問題。
Slab Allocation將內存分割成不同Slab Class,把相同尺寸的塊(Chunk)組成組(Slab),如圖4所示。Slab Allocation重復使用已分配的內存塊,覆蓋原有內存塊的數據。


圖4 Slab Class

首先介紹Slab Allocation的概念:

  • Page:操作系統的內存頁,分配給Slab的內存空間,默認1MB;
  • Chunk:用于緩存記錄的內存空間,不同Slab的Chunk大小通過一個特定增長比例逐漸增大;
  • Slab:特定大小的Chunk的組,同一個Page分割成相同大小的Chunk,組成一個Slab。

Memcached根據數據大小選擇最合適的Slab,如圖5所示,100 bytes items選擇112 bytes的Slab Chunk存儲數據,內存空間利用率最高,其中items包含緩存數據的key/value等,具體請查看拙作[Memcached] Slab Allocation的MC項占用空間分析及實踐

圖5 數據選擇最合適的Slab

圖5引出Slab Allocation的一個缺點:內存空間有浪費。112的Chunk存放100 bytes數據,有12 bytes空間浪費。通過下面公式可以計算圖5的期望內存利用率,對我們啟示作用請查看拙作[Memcached] 為什么MC達到90%的內存利用率時開始踢出數據?

(88+112)/2/112=89%   112 bytes的Chunk存放的期望大小是(88+112)/2,所以期望內存利用率是89%,其他同理。
(112+144)/2/144=89%
(144+184)/2/184=89%
...

不同Chunk size遞增通過Growth Factor控制,Memcached啟動可以指定Growth Factor,默認是1.25。圖6和圖7的Growth Factor分別是2和1.25,有趣的是兩者的起始Chunk size都是96B,原因請查看拙作[Memcached] 初始Chunk size計算
如下,根據不同Growth Factor推出期望內存利用率,當真實內存利用率達到期望內存利用率,警惕Memcached踢出數據,例子[Memcached] 為什么MC達到90%的內存利用率時開始踢出數據?

Growth Factor=2 : (n+2*n)/2/2n=75%
Growth Factor=1.25 : (n+1.25*n)/2/1.25n=90%
Growth Factor=gf : (n+gf*n)/2/(gf*n)=(1+gf)/2gf
圖6 Growth Factor=2的Slab Alloaction
圖7 Growth Factor=1.25的Slab Alloaction

五. Memcached刪除機制

  • 數據不會真正從Memcached消失
    • Memcached不會主動釋放已分配的內存,記錄超時后,客戶端get該記錄,Memcached不會返回該記錄,并標志該記錄過期失效,此時內存空間可重復使用。
  • Lazy Expiration
    • Memcached內部不會監視記錄是否過期,而是客戶端get時查看記錄的時間戳,檢查記錄是否過期,如果過期,標志為過期,下次存放新記錄優先使用過期記錄占用的內存,這種技術就是Lazy Expiration,Memcached不會在過期監控上耗費CPU資源。
  • LRU: Least Recently Used
    • 從緩存中有效刪除數據原理。Memcached優先使用記錄已超時和未使用的內存空間,但是在追加新記錄時如果沒有記錄已超時和未使用的內存空間,此時使用Least Recently Used(LRU)機制分配空間。顧名思義,就是刪除”最近最少使用“的記錄的機制。當Memcached內存空間不足(無法從Slab獲取Chunk)時,就刪除”最近最少使用“的記錄,將其內存空間分配給新記錄存放。從緩存使用角度,該模型非常理想。如果想關閉LRU機制,啟動Memcached指定-M參數即可。
  • Slab 鈣化

六. Memcached保存記錄過程

圖8為Memcached保存item流程圖,具體步驟為:

  • 第一步,從LRU隊列尋找過期item,這里的LRU隊列是相同Slab一個隊列,而不是全局統一,過期item標記方法通過Lazy Expiration實現,如果有過期的item,使用新item替換過期item,結束;
  • 第二步,如果沒有過期item,查看是否有合適空閑的Chunk,如果有,保存新item到空閑Chunk,結束;
  • 第三步,如果沒有合適空閑的Chunk,嘗試初始化一個新同等Chunk size的Slab,檢查內存是否足夠,如果夠,分配內存創建Slab和Chunk,并使用Chunk存放新item,結束;
  • 第四步,如果內存不夠,從LRU隊列淘汰最近最少使用的item,然后用這個Chunk存放新item,結束。注意這一步將導致非過期LRU數據丟失。
圖8 Memcached保存記錄過程

七. Memcached內存儲存學習啟示

  • 盡量避免緩存大對象,大對象降低內存利用率和命中率
  • 緩存/節點失效后大量請求涌向數據庫,容易造成雪崩
  • 利用組件在預警時間之后失效時間之間訪問緩存,主動刷新緩存
  • 關鍵業務數據不要放MC,LRU機制導致緩存數據刪除,影響業務

八. Memcached不互相通信的分布式特征

memcached盡管是“分布式”緩存服務器,但服務器端并沒有分布式功能,各個memcached不會互相通信以共享信息,由客戶端的實現訪問Memcached集群。如圖9,應用程序通過MC客戶端程序庫訪問MC集群,MC客戶端程序庫根據Hash算法從服務器列表選擇一臺MC服務器存放數據,各個MC之間不共享數據,也就沒有腦裂問題。


圖9 應用程序通過MC客戶端程序庫訪問MC集群

九. 一致性Hash算法

MC客戶端程序庫根據普通的Hash取余算法選擇MC服務器存放數據,如果移除或者新增MC服務器,MC客戶端程序庫要根據服務器列表總數重新取余,就會選擇一臺其他的MC服務器存儲數據,而該臺服務器沒有緩存上一臺服務器的數據,所以導致大量數據發起大量請求訪問DB獲取數據,容易造成雪崩問題。
為了解決Hash取余算法的固有缺點,MC引入一致性Hash算法,如果圖10所示,首先求出MC服務器(節點)的哈希值,將其分配到0~232的環上。然后用同樣的方法求出存放數據的哈希值,映射到環上,并從數據映射的位置開始順時針查找,將數據存放到最近的一個MC節點。如果超過232仍然找不到服務器,就會保存到第一臺MC節點上。獲取數據的查找方式也是如此。

圖10 一致性Hash基本原理

從圖10的狀態中添加一臺MC服務器節點5,變成圖11,只有環上增加節點5到逆時針方向的第一臺節點(節點2)之間的鍵受影響,本應映射到節點4而映射到節點5。因此,一致性Hash算法最大程度抑制鍵的重新分布。


圖11 一致性Hash:添加服務器

圖12的哈希表或許更加直觀,md5根據key值摘要一個128bit的哈希值(校驗和),一般表示為32位的16進制數,我們取哈希值的第一位范圍將key映射到不同節點,如圖12左側表格所示。當新增一個節點5,把原本映射到節點4的一半數據映射到節點5,其他三個節點不受影響。但是引出數據在MC節點上分布不均勻的問題,原本左側表格每個節點映射的數據量一樣,但是右側表格的節點4/5只有其他三個節點的一半數據量,導致節點4/5的帶寬和內存使用率一直不飽滿。


圖12 一致性Hash:哈希表方式

為此,引入虛擬節點,如圖13所示,左側表格中依md5值劃分為16個虛擬節點,每四個虛擬節點映射到一個物理節點。當增加物理節點5時,就從節點1/2/3各拿一個虛擬節點映射到物理節點5,這樣每個物理節點基本有3到4個虛擬節點的映射,緩存數據分布相對圖12右側表格均衡很多。


圖13 一致性Hash:引入虛擬節點
一致性Hash算法啟示
  • 副本存儲到多個節點,避免單點故障或者數據失效大量請求涌向DB
  • 節點故障后,有快速預熱新節點應急手段,或者使用冷節點備份

十. Memcached一些疑難問題

1. 為什么不能用緩存保存session?

當MC內存空間不足,并且沒有過期數據,MC用新數據覆蓋LRU的數據,導致部分session信息被清除,用戶重新登錄才能獲取到session,用戶體驗差。實際上,可以把session持久化到DB,MC緩存一份session,待MC查詢不到session信息,再到DB查詢,并更新MC,既能保證數據不丟失,也保證session查詢速度快。

2. 為什么同一個MC的數據,有的緩存(值較大的舊數據)要2天才被置換出,有的緩存(值較小的新數據)幾分鐘就被置換出?

值較大的數據占用Slab Class的大Chunk size,值較小的數據占用Slab Class的小Chunk size,根據Slab鈣化問題,當值較小的數據占用Slab Class空間不夠用時,并且沒有多余的內存和過期的數據,不會挪用值較大的數據占用Slab Class空間,只會復用原有值較小的數據占用Slab Class空間,根據LRU算法置換出同一種Slab的Chunk數據。
Slab鈣化問題請查看拙作[Memcached] Slab鈣化問題

3. Cache失效后的擁堵問題

通常我們會為兩種數據做Cache,一種是熱數據,也就是說短時間內有很多人訪問的數據;另一種是高成本的數據,也就說查詢很耗時的數據。當這些數據過期的瞬間,如果大量請求同時到達,那么它們會一起請求后端重建Cache,造成擁堵問題。
一般有如下幾種解決思路可供選擇:
首先,通過定時任務主動更新Cache;
其次,我們可以加分布式鎖,保證只有一個請求訪問數據庫更新緩存。

4. Multiget的無底洞問題

出于效率的考慮,很多Memcached應用都以Multiget操作為主,隨著訪問量的增加,系統負載捉襟見肘,遇到此類問題,直覺通常都是增加服務器來提升系統性能,但是在實際操作中卻發現問題并不簡單,新加的服務器好像被扔到了無底洞里一樣毫無效果。Multiget根據key請求多臺服務器,但這并不是問題的癥結,真正的原因在于客戶端在請求多臺服務器時是并行的還是串行的!問題是很多客戶端,在處理Multiget多服務器請求時,使用的是串行的方式!也就是說,先請求一臺服務器,然后等待響應結果,接著請求另一臺,結果導致客戶端操作時間累加,請求堆積,性能下降。

5. 緩存命中率下降,但是內存的利用率很高時,我們需要如何進行處理?

內存空間不足,導致緩存失效移除,命中率下降,既然內存利用率高,擴容MC服務器即可。

6. 緩存命中率下降,內存的利用率也在下降時,我們需要如何進行處理?

跟問題2類似,也是Slab鈣化問題。空間利用率高的Slab Class不會使用空間利用率低的其他的Slab Class,導致空間利用率高的Slab Class不斷因為LRU踢出數據,總體而言,緩存命中率下降,內存的利用率也會下降。
Slab鈣化降低內存使用率,如果發生Slab鈣化,有三種解決方案:

  1. 重啟Memcached實例,簡單粗暴,啟動后重新分配Slab class,但是如果是單點可能造成大量請求訪問數據庫,出現雪崩現象,沖跨數據庫。
  2. 隨機過期:過期淘汰策略也支持淘汰其他slab class的數據,twitter工程師采用隨機選擇一個Slab,釋放該Slab的所有緩存數據,然后重新建立一個合適的Slab。
  3. 通過slab_reassign、slab_authmove。

7. 通常情況下,緩存的粒度越小,命中率會越高。

舉個實際的例子說明:當緩存單個對象的時候(例如:單個用戶信息),只有當該對象對應的數據發生變化時,我們才需要更新緩存或者移除緩存。而當緩存一個集合的時候(例如:所有用戶數據),其中任何一個對象對應的數據發生變化時,都需要更新或移除緩存。
由于序列化和反序列化需要一定的資源開銷,當處于高并發高負載的情況下,對大對象數據的頻繁讀取有可能會使得服務器的CPU崩潰。

8. 緩存被“擊穿”問題

對于一些設置了過期時間的key,如果這些key可能會在某些時間點被超高并發地訪問,是一種非常“熱點”的數據。這個時候,需要考慮另外一個問題:緩存被“擊穿”的問題。

  • 概念:緩存在某個時間點過期的時候,恰好在這個時間點對這個Key有大量的并發請求過來,這些請求發現緩存過期一般都會從后端DB加載數據并回設到緩存,這個時候大并發的請求可能會瞬間把后端DB壓垮。
  • 如何解決:業界比較常用的做法,是使用mutex。簡單地來說,就是在緩存失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作返回成功時,再進行load db的操作并回設緩存;否則,就重試整個get緩存的方法。類似下面的代碼:
public String get(key) {
      String value = redis.get(key);
      if (value == null) { //代表緩存值過期
          //設置3min的超時,防止del操作失敗的時候,下次緩存過期一直不能load db
          if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表設置成功
               value = db.get(key);
                      redis.set(key, value, expire_secs);
                      redis.del(key_mutex);
              } else {  //這個時候代表同時候的其他線程已經load db并回設到緩存了,這時候重試獲取緩存值即可
                      sleep(50);
                      get(key);  //重試
              }
          } else {
              return value;      
          }
  }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容