天天看點

memcache核心,一文搞定!面試再也不怕了!!!(值得收藏)

memcache是網際網路分層架構中,使用最多的的KV緩存。面試的過程中,memcache相關的問題幾乎是必問的,關于memcache的面試提問,你能回答到哪一個層次呢?

畫外音:很可能關乎,你拿到offer的薪酬檔位。

第一類問題:知道不知道

這一類問題,考察用沒用過,知不知道,相對比較好回答。

關于memcache一些基礎特性,使用過的小夥伴基本都能回答出來:

(1)mc的核心職能是KV記憶體管理,value存儲最大為1M,它不支援複雜資料結構(哈希、清單、集合、有序集合等);

(2)mc不支援持久化;

(3)mc支援key過期;

(4)mc持續運作很少會出現記憶體碎片,速度不會随着服務運作時間降低;

(5)mc使用非阻塞IO複用網絡模型,使用監聽線程/工作線程的多線程模型;

面對這類封閉性的問題,一定要斬釘截鐵,毫無猶豫的給出回答。

第二類問題:為什麼(why),什麼(what)

這一類問題,考察對于一個工具,隻停留在使用層面,還是有原理性的思考。

memcache為什麼不支援複雜資料結構?為什麼不支援持久化?

業務決定技術方案,mc的誕生,以“以服務的方式,而不是庫的方式管理KV記憶體”為設計目标,它颠覆的是,KV記憶體管理元件庫,複雜資料結構與持久化并不是它的初衷。

當然,用“颠覆”這個詞未必不合适,庫和服務各有使用場景,隻是在分布式的環境下,服務的使用範圍更廣。設計目标,誕生背景很重要,這一定程度上決定了實作方案,就如redis的出現,是為了有一個更好用,更多功能的緩存服務。

畫外音:我很喜歡問這個問題,大部分候選人面對這個沒有标準答案的問題,狀态可能是蒙圈。

memcache是用什麼技術實作key過期的?

懶淘汰(lazy expiration)。

memcache為什麼能保證運作性能,且很少會出現記憶體碎片?

提前配置設定記憶體。

memcache為什麼要使用非阻塞IO複用網絡模型,使用監聽線程/工作線程的多線程模型,有什麼優缺點?

目的是提高吞吐量。

多線程能夠充分的利用多核,但會帶來一些鎖沖突。

面對這類半開放的問題,有些并沒有标準答案,一定要回答出自己的思考和見解。

第三類問題:怎麼做(how) | 文本剛開始

這一類問題,探測候選人了解得有多透,掌握得有多細,對技術有多刨根究底。

畫外音:所謂“好奇心”,真的很重要,隻想要“一份工作”的技術人很難有這種好奇心。

memcache是什麼實作記憶體管理,以減小記憶體碎片,是怎麼實作配置設定記憶體的?

開講之前,先解釋幾個非常重要的概念:

chunk:它是将記憶體配置設定給使用者使用的最小單元。

item:使用者要存儲的資料,包含key和value,最終都存儲在chunk裡。

slab:它會管理一個固定chunk size的若幹個chunk,而mc的記憶體管理,由若幹個slab組成。

畫外音:為了避免複雜性,本文先不引入page的概念了。

memcache核心,一文搞定!面試再也不怕了!!!(值得收藏)

如上圖所示,一系列slab,分别管理128B,256B,512B…的chunk記憶體單元。

将上圖中管理128B的slab0放大:

memcache核心,一文搞定!面試再也不怕了!!!(值得收藏)
能夠發現slab中的一些核心資料結構是:

  • chunk_size:該slab管理的是128B的chunk
  • free_chunk_list:用于快速找到空閑的chunk
  • chunk[]:已經預配置設定好,用于存放使用者item資料的實際chunk空間

畫外音:其實還有lru_list。

假如使用者要存儲一個100B的item,是如何找到對應的可用chunk的呢?

memcache核心,一文搞定!面試再也不怕了!!!(值得收藏)

會從最接近item大小的slab的chunk[]中,通過free_chunk_list快速找到對應的chunk,如上圖所示,與item大小最接近的chunk是128B。

為什麼不會出現記憶體碎片呢?

memcache核心,一文搞定!面試再也不怕了!!!(值得收藏)

拿到一個128B的chunk,去存儲一個100B的item,餘下的28B不會再被其他的item所使用,即:實際上浪費了存儲空間,來減少記憶體碎片,保證通路的速度。

畫外音:理論上,記憶體碎片幾乎不存在。

memcache通過slab,chunk,free_chunk_list來快速配置設定記憶體,存儲使用者的item,那它又是如何快速實作key的查找的呢?

沒有什麼特别算法:

memcache核心,一文搞定!面試再也不怕了!!!(值得收藏)
  • 通過hash表實作快速查找
  • 通過連結清單來解決沖突

用最樸素的方式,實作key的快速查找。

随着item的個數不斷增多,hash沖突越來越大,hash表如何保證查詢效率呢?

當item總數達到hash表長度的1.5倍時,hash表會動态擴容,rehash将資料重新分布,以保證查找效率不會不斷降低。

擴充hash表之後,同一個key在新舊hash表内的位置會發生變化,如何保證資料的一緻性,以及如何保證遷移過程服務的可用性呢(肯定不能加一把大鎖,遷移完成資料,再重新服務吧)?

哈希表擴充,資料遷移是一個耗時的操作,會有一個專門的線程來實施,為了避免大鎖,采用的是“分段遷移”的政策。

當item數量達到門檻值時,遷移線程會分段遷移,對hash表中的一部分桶進行加鎖,遷移資料,解鎖:

  • 一來,保證不會有長時間的阻塞,影響服務的可用性
  • 二來,保證item不會在新舊hash表裡不一緻

新的問題來了,對于已經存在與舊hash表中的item,可以通過上述方式遷移,那麼在item遷移的過程中,如果有新的item插入,是應該插入舊hash表還是新hash表呢?

memcache的做法是,判斷舊hash表中,item應該插入的桶,是否已經遷移至新表中:

  • 如果已經遷移,則item直接插入新hash表
  • 如果還沒有被遷移,則直接插入舊hash表,未來等待遷移線程來遷移至新hash表

為什麼要這麼做呢,不能直接插入新hash表嗎?

memcache沒有給出官方的解釋,樓主揣測,這種方法能夠保證一個桶内的資料,隻在一個hash表中(要麼新表,要麼舊表),任何場景下都不會出現,舊表新表查詢兩次,以提升查詢速度。

memcache是怎麼實作key過期的,懶淘汰(lazy expiration)具體是怎麼玩的?

實作“逾時”和“過期”,最常見的兩種方法是:

  • 啟動一個逾時線程,對所有item進行掃描,如果發現逾時,則進行逾時回調處理
  • 每個item設定一個逾時信号通知,通知觸發逾時回調處理

這兩種方法,都需要有額外的資源消耗。

mc的查詢業務非常簡單,隻會傳回cache hit與cache miss兩種結果,這種場景下,非常适合使用懶淘汰的方式。

懶淘汰的核心是:

  • item不會被主動淘汰,即沒有逾時線程,也沒有信号通知來主動檢查
  • item每次會查詢(get)時,檢查一下時間戳,如果已經過期,被動淘汰,并傳回cache miss

舉個例子,假如set了一個key,有效期100s:

  • 在第50s的時候,有使用者查詢(get)了這個key,判斷未過期,傳回對應的value值
  • 在第200s的時候,又有使用者查詢(get)了這個key,判斷已過期,将item所在的chunk釋放,傳回cache miss

這種方式的實作代價很小,消耗資源非常低:

  • 在item裡,加入一個過期時間屬性
  • 在get時,加入一個時間判斷

記憶體總是有限的,chunk數量有限的情況下,能夠存儲的item個數是有限的,假如chunk被用完了,該怎麼辦?

仍然是上面的例子,假如128B的chunk都用完了,使用者又set了一個100B的item,要不要擠掉已有的item?

要。

這裡的啟示是:

(1)即使item的有效期設定為“永久”,也可能被淘汰;

(2)如果要做全量資料緩存,一定要仔細評估,cache的記憶體大小,必須大于,全量資料的總大小,否則很容易采坑;

擠掉哪一個item?怎麼擠?

這裡涉及LRU淘汰機制。

如果作業系統的記憶體管理,最常見的淘汰算法是FIFO和LRU:

  • FIFO(first in first out):最先被set的item,最先被淘汰
  • LRU(least recently used):最近最少被使用(get/set)的item,最先被淘汰

使用LRU算法擠掉item,需要增加兩個屬性:

  • 最近item通路計數
  • 最近item通路時間

并增加一個LRU連結清單,就能夠快速實作。

畫外音:是以,管理chunk的每個slab,除了free_chunk_list,還有lru_list。

思路比結論重要。

本文轉自“架構師之路”公衆号,58沈劍提供。

繼續閱讀