原文地址:http://zhihuzeye.com/archives/2361
1、memcached 介紹
1.1 memcached 是什么?
memcached 是以LiveJournal旗下Danga Interactive 公司的Brad Fitzpatric 為首開發的一款軟件。現在已成為mixi、hatena、Facebook、Vox、LiveJournal 等眾多服務中提高Web應用擴展性的重要因素。許多Web 應用都將數據保存到RDBMS 中,應用服務器從中讀取數據並在瀏覽器中顯示。但隨着數據量的增大、訪問的集中,就會出現RDBMS 的負擔加重、數據庫響應惡化、網站顯示延遲等重大影響。這時就該memcached 大顯身手了。memcached 是高性能的分布式內存緩存服務器。一般的使用目的是,通過緩存數據庫查詢結果,減少數據庫訪問次數,以提高動態Web 應用的速度、提高可擴展性。

內置內存存儲方式
研究memcached這個產品,首先從它的內存模型開始:我們知道c++里分配內存有兩種方式,預先分配和動態分配,顯然,預先分配內存會使程序比較快,但是它的缺點是不能有效利用內存,而動態分配可以有效利用內存,但是會使程序運行效率下降,memcached的內存分配就是基於以上原理,顯然為了獲得更快的速度,有時候我們不得不以空間換時間。
Memcached的高性能源於兩階段哈希(two-stage hash)結構。Memcached就像一個巨大的、存儲了很多<key,value>對的哈希表。通過key,可以存儲或查詢任意的數據。 客戶端可以把數據存儲在多台memcached上。當查詢數據時,客戶端首先參考節點列表計算出key的哈希值(階段一哈希),進而選中一個節點;客戶端將請求發送給選中的節點,然后
memcached節點通過一個內部的哈希算法(階段二哈希),查找真正的數據(item)並返回給客戶端。從實現的角度看,memcached是一個非阻塞的、基於事件的服務器程序。
為了提高性能,memcached 中保存的數據都存儲在memcached 內置的內存存儲空間中。由於數據僅存在於內存中,因此重啟memcached、重啟操作系統會導致全部數據消失。另外,內容容量達到指定值之后,就基於LRU(Least Recently Used)算法自動刪除不使用的緩存。memcached 本身是為緩存而設計的服務器,因此並沒有過多考慮數據的永久性問題
memcached 不互相通信的分布式
memcached 盡管是“分布式”緩存服務器,但服務器端並沒有分布式功能。各個
memcached 不會互相通信以共享信息。那么,怎樣進行分布式呢?這完全取決於客戶端的實現。

1.2 memcached啟動
memcached 啟動的命令在安裝目錄的bin 二級目錄下,如/home/test/app/memcahced-1.4.2/bin/memcached -p 11222 -m 128–d
常用的一些啟動選項介紹選項說明
-p 偵聽的端口,默認為11211
-m 使用內存大小,默認的64m
-d 作為daemon 在后台啟動
-vv 用very vrebose 模式啟動,調試信息和錯誤輸出到控制台
-l 偵聽的地址,默認為所有可以訪問的地址
-M 用於在內存溢出的時候,返回一個錯誤,禁止自動的移出數
據,替代的是返回一個error
-P Pid 文件存在的路徑,僅限加上-d 參數是用
-c 最大同時的連接數,默認為1024
其它的一些選項,可以通過–h 命令來進行查看
1.3 命令行訪問memcached
下面假設memcached 啟動時的-p 參數為11311,命令操作在啟動memcached
本機首先telnet 連接到memcached 服務器
telnet 127.0.0.1 11311
telnet 成功之后,大概會顯示下面的信息
Trying 127.0.0.1...
Connected to localhost.localdomain (127.0.0.1).
Escape character is '^]'.
各種狀態(stats)
STAT <name> <value>\r\n
如:stats命令,則返回以下信息:
stats STAT pid 26804 STAT uptime 182783 STAT time 1404973716 STAT version 1.4.13 STAT libevent 2.0.11-stable STAT pointer_size 64 STAT rusage_user 2.320647 STAT rusage_system 5.411177 STAT curr_connections 34 STAT total_connections 558 STAT connection_structures 37 STAT reserved_fds 20 STAT cmd_get 127292 STAT cmd_set 60056 STAT cmd_flush 145 STAT cmd_touch 0 STAT get_hits 83811 STAT get_misses 43481 STAT delete_misses 15970 STAT delete_hits 11992 STAT incr_misses 0 STAT incr_hits 0 STAT decr_misses 0 STAT decr_hits 0 STAT cas_misses 0 STAT cas_hits 0 STAT cas_badval 0 STAT touch_hits 0 STAT touch_misses 0 STAT auth_cmds 0 STAT auth_errors 0 STAT bytes_read 14300156 STAT bytes_written 11507140 STAT limit_maxbytes 134217728 # 分配給memcache的內存大小(字節) STAT accepting_conns 1 STAT listen_disabled_num 0 STAT threads 4 STAT conn_yields 0 STAT hash_power_level 16 STAT hash_bytes 524288 STAT hash_is_expanding 0 STAT expired_unfetched 16884 STAT evicted_unfetched 0 STAT bytes 609350 # 當前服務器存儲items占用的字節數 STAT curr_items 4668 # 服務器當前存儲的items數量 STAT total_items 60056 STAT evictions 0 # 分配給memcache的空間用滿后需要刪除舊的items數,踢出。 STAT reclaimed 27160 #回收再利用,已過期的數據條目來存儲新數據。 END
memcache中指令
(1)插入指令,add,replace,set
“set”表示按照相應的<key>存儲該數據,沒有的時候增加,有的覆蓋。
“add”表示按照相應的<key>添加該數據,但是如果該<key>已經存在則會操作失敗,返回false。
“replace”表示按照相應的<key>替換數據,但是如果該<key>不存在則操作失敗,返回false。
(2)讀取指令
get <key>*\r\n
其中,<key>* 表示一個或者多個key(以空格分開); “\r\n” 命令頭的結束。
(3)刪除指令
delete <key> <time>\r\n
其中, <key> 需要被刪除數據的key; <time> 客戶端希望服務器將該數據刪除的時間(unix時間或者從現在開始的秒數); “\r\n” 命令頭的結束。
(4)退出指令,quit
2、理解memcached 的內存存儲
Memcache使用了Slab Allocator的內存分配機制:按照預先規定的大小,將分配的內存分割成特定長度的塊,以完全解決內存碎片問題。
Memcache的存儲涉及到slab,page,chunk三個概念
1.Chunk為固定大小的內存空間,默認為48Byte。
2.page對應實際的物理空間,1個page為1M。
3.同樣大小的chunk又稱為slab。
2.1、Slab Allocation 機制:整理內存以便重復使用
最近的memcached 默認情況下采用了名為Slab Allocator 的機制分配、管理內存。在該機制出現以前,內存的分配是通過對所有記錄簡單地進行malloc和free 來進行的。但是,這種方式會導致內存碎片,加重操作系統內存管理器的負擔,最壞的情況下,會導致操作系統比memcached 進程本身還慢。Slab Allocator 就是為解決該問題而誕生的Slab Allocation 的原理相當簡單。將分配的內存分割成各種尺寸的塊(chunk),並把尺寸相同的塊分成組(chunk 的集合)。

而且,slab allocator 還有重復使用已分配的內存的目的。也就是說,分配到的內存不會釋放,而是重復利用。
Slab Allocation 的主要術語
Page:分配給Slab 的內存空間,默認是1MB。分配給Slab 之后根據slab 的大小切分成chunk。
Chunk:用於緩存記錄的內存空間。
Slab Class:特定大小的chunk 的組
2.2 在Slab中緩存記錄的原理
memcached 根據收到的數據的大小,選擇最適合數據大小的slab,memcached 中保存着slab 內空閑chunk 的列表,根據該列表選擇chunk,然
后將數據緩存於其中

2.3 Slab Allocator的缺點
由於分配的是特定長度的內存,因此無法有效利用分配的內存。例如,將100 字節的數據緩存到128 字節的chunk 中,剩余的28字節就浪費了

對於該問題目前還沒有完美的解決方案,但在文檔中記載了比較有效的解決方案。就是說,如果預先知道客戶端發送的數據的公用大小,或者僅緩存大小相同的數據的情況下,只要使用適合數據大小的組的列表,就可以減少浪費。但是很遺憾,現在還不能進行任何調優,只能期待以后的版本了。但是,我們可以調節slab class 的大小的差別。接下來說明growth factor 選項。
2.4 使用Growth Factor進行調優
memcached 在啟動時指定Growth Factor 因子(通過f 選項),就可以在某種程度上控制slab 之間的差異。默認值為1.25。但是,在該選項出現之前,這個因子曾經固定為2,稱為“powers of 2”策略。
下面是啟動后的verbose 輸出:
slab class 1: chunk size 128 perslab 8192 slab class 2: chunk size 256 perslab 4096 slab class 3: chunk size 512 perslab 2048 slab class 4: chunk size 1024 perslab 1024 slab class 5: chunk size 2048 perslab 512 slab class 6: chunk size 4096 perslab 256 slab class 7: chunk size 8192 perslab 128 slab class 8: chunk size 16384 perslab 64 slab class 9: chunk size 32768 perslab 32 slab class 10: chunk size 65536 perslab 16 slab class 11: chunk size 131072 perslab 8 slab class 12: chunk size 262144 perslab 4 slab class 13: chunk size 524288 perslab 2
可見,從128 字節的組開始,組的大小依次增大為原來的2 倍。這樣設置的問題是,slab 之間的差別比較大,有些情況下就相當浪費內存。因此,為盡量減少內存浪費,兩年前追加了growth factor 這個選項來看看現在的默認設置(f=1.25)時的輸出(篇幅所限,這里只寫到第10 組):
slab class 1: chunk size 88 perslab 11915 slab class 2: chunk size 112 perslab 9362 slab class 3: chunk size 144 perslab 7281 slab class 4: chunk size 184 perslab 5698 slab class 5: chunk size 232 perslab 4519 slab class 6: chunk size 296 perslab 3542 slab class 7: chunk size 376 perslab 2788 slab class 8: chunk size 472 perslab 2221 slab class 9: chunk size 592 perslab 1771 slab class 10: chunk size 744 perslab 1409
可見,組間差距比因子為2 時小得多,更適合緩存幾百字節的記錄。從上面的輸出結果來看,可能會覺得有些計算誤差,這些誤差是為了保持字節數的對齊而故意設置的。將memcached 引入產品,或是直接使用默認值進行部署時,最好是重新計算一下數據的預期平均長度,調整growth factor,以獲得最恰當的設置。內存是珍貴的資源,浪費就太可惜了。
item占用空間計算
*nsuffix = (uint8_t) snprintf(suffix, 40, " %d %d\r\n", flags, nbytes – 2);
return sizeof(item) + nkey + *nsuffix + nbytes;
*nsuffix=" %d %d\r\n”的長度
如果ITEM_CAS標志設置時,這里有8字節的數據
完整的item長度是鍵長+值長+后綴長+item結構大小(48字節) + 8
item.length=56+key.lenght+value.length+后綴長
32位機器 item結構是32字節
64位機器 itme結構是48字節
memcache存儲的時候對key的長度有限制,php和C的最大長度都是250
3、memcached 刪除機制
memcached 是緩存,不需要永久的保存到服務器上,本章介紹memcache 的刪除機制
3.1 memcached 在數據刪除方面有效的利用資源
Memcached 不會釋放已經分配的內存,記錄過期之后,客戶端無法再看到這一條記錄,其存儲空間就可以利用。
Lazy Expiration
memcached 內部不會監視記錄是否過期,而是在get 時查看記錄的時間戳,檢查記錄是否過期。這種技術被稱為lazy(惰性)expiration。因此,memcached不會在過期監視上耗費CPU 時間
3.2 LRU:從緩存中有效刪除數據的原理
1.search->refcount == 0 && 已經過期的 刪除
2.tries = 50; // 最多嘗試50次 LRU隊列tail 查找 search->refcount == 0 第一個 刪除
3. tries = 50; // 最多嘗試50次 LRU隊列tail 查找search->refcount != 0 查詢時間(超過3小時)的item 第一個 刪除
memcached 會優先使用已超時的記錄的空間,但即使如此,也會發生追加新記錄時空間不足的情況,此時就要使用名為Least Recently Used(LRU)機制來分配空間。顧名思義,這是刪除“最近最少使用”的記錄的機制。因此,當memcached 的內存空間不足時(無法從slab class 獲取到新的空間時),就從最近未被使用的記錄中搜索,並將其空間分配給新的記錄。從緩存的實用角度來看,該模型十分理想。不過,有些情況下LRU 機制反倒會造成麻煩。memcached 啟動時通過“M”參數可以禁止LRU,如下所示:
$ memcached -M –m 1024
啟動時必須注意的是,小寫的“m”選項是用來指定最大內存大小的。不指定具體數值則使用默認值64MB。
指定“M”參數啟動后,內存用盡時memcached 會返回錯誤。話說回來,memcached 畢竟不是存儲器,而是緩存,所以推薦使用LRU
4、memcached 的分布式算法
4.1memcached的分布式
memcached 雖然稱為“分布式”緩存服務器,但服務器端並沒有“分布式”功能。memcached 的分布式,則是完全由客戶端程序庫實現的。這種分布式是memcached 的最大特點
memcached的分布式是什么意思?
下面假設memcached 服務器有node1~node3 三台,應用程序要保存鍵名為“tokyo”、“kanagawa”、“chiba”、“saitama”、“gunma”的數據

首先向memcached 中添加“tokyo”。將“tokyo”傳給客戶端程序庫后,客戶端實現的算法就會根據“鍵”來決定保存數據的memcached 服務器。服務器選定后,即命令它保存“tokyo”及其值
同樣,“kanagawa”、“chiba”、“saitama”、“gunma”都是先選擇服務器再保接下來獲取保存的數據。獲取時也要將要獲取的鍵“tokyo”傳遞給函數庫。函數庫通過與數據保存時相同的算法,根據“鍵”選擇服務器。使用的算法相同,就能選中與保存時相同的服務器,然后發送get 命令。只要數據沒有因為某些原因被刪除,就能獲得保存的值。
這樣,將不同的鍵保存到不同的服務器上,就實現了memcached 的分布式。memcached 服務器增多后,鍵就會分散,即使一台memcached 服務器發生故障無法連接,也不會影響其他的緩存,系統依然能繼續運行
4.2 余數分布式算法
就是“根據服務器台數的余數進行分散”。求得鍵的整數哈希值,再除以服務器台數,根據其余數來選擇服務器。
余數算法的缺點
余數計算的方法簡單,數據的分散性也相當優秀,但也有其缺點。那就是當添加或移除服務器時,緩存重組的代價相當巨大。添加服務器后,余數就會產生巨變,這樣就無法獲取與保存時相同的服務器,從而影響緩存的命中。
4.3Consistent Hashing(一致哈希)
知識補充:哈希算法,即散列函數。將任意長度的二進制值映射為較短的固定長度的二進制值,這個小的二進制值稱為哈希值。哈希值是一段數據唯一且極其緊湊的數值表示形式。如果散列一段明文而且哪怕只更改該段落的一個字母,隨后的哈希都將產生不同的值。要找到散列為同一個值的兩個不同的輸入,在計算上是不可能的,所以數據的哈希值可以檢驗數據的完整性。一般用於快速查找和加密算法。(常見的有MD5,SHA-1)
Consistent Hashing的簡單說明
Consistent Hashing 如下所示:首先求出memcached 服務器(節點)的哈希值,並將其配置到0~232 的圓(continuum)上。然后用同樣的方法求出存儲數據的鍵的哈希值,並映射到圓上。然后從數據映射到的位置開始順時針查找,將數據保存到找到的第一個服務器上。如果超過232 仍然找不到服務器,就會保存到第一台memcached 服務器上。

從上圖的狀態中添加一台memcached 服務器。余數分布式算法由於保存鍵的服務器會發生巨大變化,而影響緩存的命中率,但Consistent Hashing中,只有在continuum 上增加服務器的地點逆時針方向的第一台服務器上的鍵會受到影響。

Consistent Hashing:添加服務器
因此,Consistent Hashing 最大限度地抑制了鍵的重新分布。而且,有的Consistent Hashing 的實現方法還采用了虛擬節點的思想。使用一般的hash函數的話,服務器的映射地點的分布非常不均勻。因此,使用虛擬節點的思想,為每個物理節點(服務器)在continuum上分配100~200 個點。這樣就能抑制分布不均勻,最大限度地減小服務器增減時的緩存重新分布。
通過上文中介紹的使用Consistent Hashing 算法的memcached 客戶端函數庫進行測試的結果是,由服務器台數(n)和增加的服務器台數(m)計算增加服務器后的命中率計算公式如下:
(1 n/(n+m)) * 100
存儲命令
<command name> <key> <flags> <exptime> <bytes>\r\n
- <command name> 是set, add,或者repalce
- <key> 是接下來的客戶端所要求儲存的數據的鍵值
- <flags> 是在取回內容時,與數據和發送塊一同保存服務器上的任意16位無符號整形(用十進制來書寫)。客戶端可以用它作為“位域”來存儲一些特定的信息;它對服務器是不透明的。
- <exptime> 是終止時間。如果為0,該項永不過期(雖然它可能被刪除,以便為其他緩存項目騰出位置)。如果非0(Unix 時間戳或當前時刻的秒偏移),到達終止時間后,客戶端無法再獲得這項內容。
- <bytes> 是隨后的數據區塊的字節長度,不包括用於分野的“\r\n”。它可以是0(這時后面跟隨一個空的數據區塊)。
- <data block> 是大段的8位數據,其長度由前面的命令行中的<bytes>指定。
• set 意思是“儲存此數據”
• add 意思是“儲存此數據,只在服務器*未*保留此鍵值的數據時”
• replace 意思是“儲存此數據,只在服務器*曾*保留此鍵值的數據時”
發送命令行和數據區塊以后,客戶端等待回復,可能的回復如下:
- "STORED\r\n"表明成功.
- "NOT_STORED\r\n"表明數據沒有被存儲,但不是因為發生錯誤。這通常意味着add或replace 命令的條件不成立,或者,項目已經位列刪除隊列(參考后文的“delete”命令)。
取回命令
get <key>*\r\n
- <key>* 表示一個或多個鍵值,由空格隔開的字串這行命令以后,客戶端的等待0個或多個項目,每項都會收到一行文本,然后跟着數據區塊。所有項目傳送完畢后,服務器發送以下字串:"END\r\n"來指示回應完畢,服務器用以下形式發送每項內容:
VALUE <key> <flags> <bytes>\r\n
<data block>\r\n
- <key> 是所發送的鍵名
- <flags> 是存儲命令所設置的記號
- <bytes> 是隨后數據塊的長度,*不包括* 它的界定符“\r\n”
- <data block> 是發送的數據
如果在取回請求中發送了一些鍵名,而服務器沒有送回項目列表,這意味着服務器沒這些鍵名(可能因為它們從未被存儲,或者為給其他內容騰出空間而被刪除,或者到期,或者被已客戶端刪除)。
刪除
delete <key> <time>\r\n
- <key> 是客戶端希望服務器刪除的內容的鍵名
- <time> 是一個單位為秒的時間(或代表直到某一刻的Unix時間),在該時間內服務器會拒絕對於此鍵名的“add”和“replace”命令。此時內容被放入delete隊列,無法再通過“get”得到該內容,也無法是用“add”和“replace”命令(但是“set”命令可用)。直到指定時間,這些內容被最終從服務器的內存中徹底清除。<time>參數是可選的,缺省為0(表示內容會立刻清除,並且隨后的存儲命令均可用)。
此命令有一行回應:- "DELETED\r\n"表示執行成功
- "NOT_FOUND\r\n"表示沒有找到這項內容
增加/減少
命令“incr”和“decr”被用來修改數據,當一些內容需要替換、增加或減少時。這些數據必須是十進制的32位無符號整新。如果不是,則當作0 來處理。修改的內容必須存在,當使用“incr”/“decr”命令修改不存在的內容時,不會被當作0處理,而是操作失敗。
客戶端發送命令行:
incr <key> <value>\r\n或decr <key> <value>\r\n
- <key> 是客戶端希望修改的內容的建名
- <value> 是客戶端要增加/減少的總數。
回復為以下集中情形:
- "NOT_FOUND\r\n"指示該項內容的值,不存在。
- <value>\r\n ,<value>是增加/減少。
注意"decr"命令發生下溢:如果客戶端嘗試減少的結果小於0 時,結果會是0。"incr" 命令不會發生溢出。
狀態
命令"stats" 被用於查詢服務器的運行狀態和其他內部數據。有兩種格式。不帶參數的:
stats\r\n
這會在隨后輸出各項狀態、設定值和文檔。另一種格式帶有一些參數:
stats <args>\r\n
通過<args>,服務器傳回各種內部數據。因為隨時可能發生變動,本文不提供參數的種類及其傳回數據。
各種狀態
受到無參數的"stats"命令后,服務器發送多行內容,如下:
STAT <name> <value>\r\n
服務器用以下一行來終止這個清單:END\r\n,在每行狀態中,<name> 是狀態的名字,<value>使狀態的數據。以下清單,是所有的狀態名稱,數據類型,和數據代表的含義。
在“類型”一列中,"32u"表示32 位無符號整型,"64u"表示64 位無符號整型,"32u:32u"表示用冒號隔開的兩個32 位無符號整型。
| 名稱 |
類型 |
含義 |
| pid |
32u |
服務器進程ID |
| uptime |
32u |
服務器運行時間,單位秒 |
| time |
32u |
服務器當前的UNIX時間 |
| version |
string |
服務器的版本號 |
| rusage_user |
32u |
該進程累計的用戶時間(秒:微妙) |
| rusage_system |
32u |
該進程累計的系統時間(秒:微妙) |
| curr_items |
32u |
服務器當前存儲的內容數量 |
| total_items |
32u |
服務器啟動以來存儲過的內容總數 |
| bytes |
64u |
服務器當前存儲內容所占用的字節數 |
| curr_connections |
32u |
連接數 |
| total_connections |
32u |
服務器運行以來接受的連接總數 |
| connection_structures |
32u |
服務器分配的連接結構的數量 |
| cmd_get |
32u |
取回請求總數 |
| cmd_set |
32u |
存儲請求總數 |
| get_hits |
32u |
請求成功的總次數 |
| get_misses |
32u |
請求失敗的總次數 |
| bytes_read |
64u |
服務器從網絡讀取到的總字節數 |
| bytes_written |
64u |
服務器向網絡發送的總字節數 |
| limit_maxbytes |
32u |
服務器在存儲時被允許使用的字節總數 |
如果不想每次通過輸入stats來查看memcache狀態,可以通過echo "stats" |nc ip port 來查看,例如:echo "stats" | nc 127.0.0.1 9023。
5.Memcache 命中率
緩存命中率 = get_hits/cmd_get * 100% (總命中次數/總請求次數)
要提高memcached的命中率,預估我們的value大小並且適當的調整內存頁大小和增長因子是必須的。
命中率的提升可以通過多種方案實現.
其一,提高服務獲取的內存總量
其二,提高空間利用率,這實際上也是另一種方式的增加內存總量
其三,應用一級別上再來一次LRU
其四,對於整體命中率,可以采取有效的冗余策略,減少分布式服務時某個server發生服務抖動的情況
6.一些注意
1. memcache已經分配的內存不會再主動清理。
2. memcache分配給某個slab的內存頁不能再分配給其他slab。
3. flush_all不能重置memcache分配內存頁的格局,只是給所有的item置為過期。
4. memcache最大存儲的item(key+value)大小限制為1M,這由page大小1M限制
5.由於memcache的分布式是客戶端程序通過hash算法得到的key取模來實現,不同的語言可能會采用不同的hash算法,同樣的客戶端程序也有可能使用相異的方法,因此在多語言、多模塊共用同一組memcached服務時,一定要注意在客戶端選擇相同的hash算法
6.啟動memcached時可以通過-M參數禁止LRU替換,在內存用盡時add和set會返回失敗
7.memcached啟動時指定的是數據存儲量,沒有包括本身占用的內存、以及為了保存數據而設置的管理空間。因此它占用的內存量會多於啟動時指定的內存分配量,這點需要注意。
8.memcache存儲的時候對key的長度有限制,php和C的最大長度都是250
