memcached全面剖析

1. memcached 的介紹

memcached是什麼?

memcached 是以LiveJournal 旗下Danga Interactive 公司的Brad Fitzpatric 爲首開發的一款軟件。現在已成爲 mixi、 hatena、 Facebook、 Vox、LiveJournal等衆多服務中 提高Web應用擴展性的重要因素。

許多Web應用都將數據保存到RDBMS中,應用服務器從中讀取數據並在瀏覽器中顯示。 但隨着數據量的增大、訪問的集中,就會出現RDBMS的負擔加重、數據庫響應惡化、 網站顯示延遲等重大影響。

這時就該memcached大顯身手了。memcached是高性能的分佈式內存緩存服務器。 一般的使用目的是,通過緩存數據庫查詢結果,減少數據庫訪問次數,以提高動態Web應用的速度、 提高可擴展性。


圖1 一般情況下memcached的用途

memcached的特徵

memcached作爲高速運行的分佈式緩存服務器,具有以下的特點。

  • 協議簡單
  • 基於libevent的事件處理
  • 內置內存存儲方式
  • memcached不互相通信的分佈式

協議簡單

memcached的服務器客戶端通信並不使用複雜的XML等格式, 而使用簡單的基於文本行的協議。因此,通過telnet 也能在memcached上保存數據、取得數據。下面是例子。

$ telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.localdomain (127.0.0.1).
Escape character is '^]'.
set foo 0 0 3 (保存命令)
bar (數據)
STORED (結果)
get foo (取得命令)
VALUE foo 0 3 (數據)
bar (數據)

協議文檔位於memcached的源代碼內,也可以參考以下的URL。

基於libevent的事件處理

libevent是個程序庫,它將Linux的epoll、BSD類操作系統的kqueue等事件處理功能 封裝成統一的接口。即使對服務器的連接數增加,也能發揮O(1)的性能。 memcached使用這個libevent庫,因此能在Linux、BSD、Solaris等操作系統上發揮其高性能。 關於事件處理這裏就不再詳細介紹,可以參考Dan Kegel的The C10K Problem。

內置內存存儲方式

爲了提高性能,memcached中保存的數據都存儲在memcached內置的內存存儲空間中。 由於數據僅存在於內存中,因此重啓memcached、重啓操作系統會導致全部數據消失。 另外,內容容量達到指定值之後,就基於LRU(Least Recently Used)算法自動刪除不使用的緩存。 memcached本身是爲緩存而設計的服務器,因此並沒有過多考慮數據的永久性問題。 關於內存存儲的詳細信息,本連載的第二講以後前阪會進行介紹,請屆時參考。

memcached不互相通信的分佈式

memcached儘管是“分佈式”緩存服務器,但服務器端並沒有分佈式功能。 各個memcached不會互相通信以共享信息。那麼,怎樣進行分佈式呢? 這完全取決於客戶端的實現。本連載也將介紹memcached的分佈式。


圖2 memcached的分佈式

接下來簡單介紹一下memcached的使用方法。

安裝memcached

memcached的安裝比較簡單,這裏稍加說明。

memcached支持許多平臺。

  • Linux
  • FreeBSD
  • Solaris (memcached 1.2.5以上版本)
  • Mac OS X

另外也能安裝在Windows上。這裏使用Fedora Core 8進行說明。

memcached的安裝

運行memcached需要本文開頭介紹的libevent庫。Fedora 8中有現成的rpm包, 通過yum命令安裝即可。

$ sudo yum install libevent libevent-devel

memcached的源代碼可以從memcached網站上下載。本文執筆時的最新版本爲1.2.5。 Fedora 8雖然也包含了memcached的rpm,但版本比較老。因爲源代碼安裝並不困難, 這裏就不使用rpm了。

memcached安裝與一般應用程序相同,configure、make、make install就行了。

$ wget http://www.danga.com/memcached/dist/memcached-1.2.5.tar.gz
$ tar zxf memcached-1.2.5.tar.gz
$ cd memcached-1.2.5
$ ./configure
$ make
$ sudo make install

默認情況下memcached安裝到/usr/local/bin下。

memcached的啓動

從終端輸入以下命令,啓動memcached。

$ /usr/local/bin/memcached -p 11211 -m 64m -vv
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 38: chunk size 391224 perslab 2
slab class 39: chunk size 489032 perslab 2
<23 server listening
<24 send buffer was 110592, now 268435456
<24 server listening (udp)
<24 server listening (udp)
<24 server listening (udp)
<24 server listening (udp)

這裏顯示了調試信息。這樣就在前臺啓動了memcached,監聽TCP端口11211 最大內存使用量爲64M。調試信息的內容大部分是關於存儲的信息, 下次連載時具體說明。

作爲daemon後臺啓動時,只需

$ /usr/local/bin/memcached -p 11211 -m 64m -d

這裏使用的memcached啓動選項的內容如下。

選項 說明
-p 使用的TCP端口。默認爲11211
-m 最大內存大小。默認爲64M
-vv 用very vrebose模式啓動,調試信息和錯誤輸出到控制檯
-d 作爲daemon在後臺啓動

上面四個是常用的啓動選項,其他還有很多,通過

$ /usr/local/bin/memcached -h

命令可以顯示。許多選項可以改變memcached的各種行爲, 推薦讀一讀。

用客戶端連接

許多語言都實現了連接memcached的客戶端,其中以Perl、PHP爲主。 僅僅memcached網站上列出的語言就有

  • Perl
  • PHP
  • Python
  • Ruby
  • C#
  • C/C++
  • Lua

等等。

這裏介紹通過mixi正在使用的Perl庫鏈接memcached的方法。

使用Cache::Memcached

Perl的memcached客戶端有

  • Cache::Memcached
  • Cache::Memcached::Fast
  • Cache::Memcached::libmemcached

等幾個CPAN模塊。這裏介紹的Cache::Memcached是memcached的作者Brad Fitzpatric的作品, 應該算是memcached的客戶端中應用最爲廣泛的模塊了。

使用Cache::Memcached連接memcached

下面的源代碼爲通過Cache::Memcached連接剛纔啓動的memcached的例子。

#!/usr/bin/perl

use strict;
use warnings;
use Cache::Memcached;

my $key = "foo";
my $value = "bar";
my $expires = 3600; # 1 hour
my $memcached = Cache::Memcached->new({
servers => ["127.0.0.1:11211"],
compress_threshold => 10_000
});

$memcached->add($key, $value, $expires);
my $ret = $memcached->get($key);
print "$ret\n";

在這裏,爲Cache::Memcached指定了memcached服務器的IP地址和一個選項,以生成實例。 Cache::Memcached常用的選項如下所示。

選項 說明
servers 用數組指定memcached服務器和端口
compress_threshold 數據壓縮時使用的值
namespace 指定添加到鍵的前綴

另外,Cache::Memcached通過Storable模塊可以將Perl的複雜數據序列化之後再保存, 因此散列、數組、對象等都可以直接保存到memcached中。

保存數據

向memcached保存數據的方法有

  • add
  • replace
  • set

它們的使用方法都相同:

my $add = $memcached->add( '鍵', '值', '期限' );
my $replace = $memcached->replace( '鍵', '值', '期限' );
my $set = $memcached->set( '鍵', '值', '期限' );

向memcached保存數據時可以指定期限(秒)。不指定期限時,memcached按照LRU算法保存數據。 這三個方法的區別如下:

選項 說明
add 僅當存儲空間中不存在鍵相同的數據時才保存
replace 僅當存儲空間中存在鍵相同的數據時才保存
set 與add和replace不同,無論何時都保存

獲取數據

獲取數據可以使用get和get_multi方法。

my $val = $memcached->get('鍵');
my $val = $memcached->get_multi('鍵1', '鍵2', '鍵3', '鍵4', '鍵5');

一次取得多條數據時使用get_multi。get_multi可以非同步地同時取得多個鍵值, 其速度要比循環調用get快數十倍。

刪除數據

刪除數據使用delete方法,不過它有個獨特的功能。

$memcached->delete('鍵', '阻塞時間(秒)');

刪除第一個參數指定的鍵的數據。第二個參數指定一個時間值,可以禁止使用同樣的鍵保存新數據。 此功能可以用於防止緩存數據的不完整。但是要注意,set函數忽視該阻塞,照常保存數據

增一和減一操作

可以將memcached上特定的鍵值作爲計數器使用。

my $ret = $memcached->incr('鍵');
$memcached->add('鍵', 0) unless defined $ret;

增一和減一是原子操作,但未設置初始值時,不會自動賦成0。因此, 應當進行錯誤檢查,必要時加入初始化操作。而且,服務器端也不會對 超過2<sup>32</sup>時的行爲進行檢查。

2. 理解memcached的內存存儲原理

Slab Allocation機制:整理內存以便重複使用

最近的memcached默認情況下采用了名爲Slab Allocator的機制分配、管理內存。 在該機制出現以前,內存的分配是通過對所有記錄簡單地進行malloc和free來進行的。 但是,這種方式會導致內存碎片,加重操作系統內存管理器的負擔,最壞的情況下, 會導致操作系統比memcached進程本身還慢。Slab Allocator就是爲解決該問題而誕生的。

下面來看看Slab Allocator的原理。下面是memcached文檔中的slab allocator的目標:

the primary goal of the slabs subsystem in memcached was to eliminate memory fragmentation issues totally by using fixed-size memory chunks coming from a few predetermined size classes.

也就是說,Slab Allocator的基本原理是按照預先規定的大小,將分配的內存分割成特定長度的塊, 以完全解決內存碎片問題。

Slab Allocation的原理相當簡單。 將分配的內存分割成各種尺寸的塊(chunk), 並把尺寸相同的塊分成組(chunk的集合)(圖1)。

圖1 Slab Allocation的構造圖

而且,slab allocator還有重複使用已分配的內存的目的。 也就是說,分配到的內存不會釋放,而是重複利用。

Slab Allocation的主要術語

Page

分配給Slab的內存空間,默認是1MB。分配給Slab之後根據slab的大小切分成chunk。

Chunk

用於緩存記錄的內存空間。

Slab Class

特定大小的chunk的組。

在Slab中緩存記錄的原理

下面說明memcached如何針對客戶端發送的數據選擇slab並緩存到chunk中。

memcached根據收到的數據的大小,選擇最適合數據大小的slab(圖2)。 memcached中保存着slab內空閒chunk的列表,根據該列表選擇chunk, 然後將數據緩存於其中。

圖2 選擇存儲記錄的組的方法

實際上,Slab Allocator也是有利也有弊。下面介紹一下它的缺點。

Slab Allocator的缺點

Slab Allocator解決了當初的內存碎片問題,但新的機制也給memcached帶來了新的問題。

這個問題就是,由於分配的是特定長度的內存,因此無法有效利用分配的內存。 例如,將100字節的數據緩存到128字節的chunk中,剩餘的28字節就浪費了(圖3)。

圖3 chunk空間的使用

對於該問題目前還沒有完美的解決方案,但在文檔中記載了比較有效的解決方案。

The most efficient way to reduce the waste is to use a list of size classes that closely matches (if that's at all possible) common sizes of objects that the clients of this particular installation of memcached are likely to store.

就是說,如果預先知道客戶端發送的數據的公用大小,或者僅緩存大小相同的數據的情況下, 只要使用適合數據大小的組的列表,就可以減少浪費。

但是很遺憾,現在還不能進行任何調優,只能期待以後的版本了。 但是,我們可以調節slab class的大小的差別。 接下來說明growth factor選項。

使用Growth Factor進行調優

memcached在啓動時指定 Growth Factor因子(通過-f選項), 就可以在某種程度上控制slab之間的差異。默認值爲1.25。 但是,在該選項出現之前,這個因子曾經固定爲2,稱爲“powers of 2”策略。

讓我們用以前的設置,以verbose模式啓動memcached試試看:

$ memcached -f 2 -vv

下面是啓動後的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, 以獲得最恰當的設置。內存是珍貴的資源,浪費就太可惜了。

接下來介紹一下如何使用memcached的stats命令查看slabs的利用率等各種各樣的信息。

查看memcached的內部狀態

memcached有個名爲stats的命令,使用它可以獲得各種各樣的信息。 執行命令的方法很多,用telnet最爲簡單:

$ telnet 主機名 端口號

連接到memcached之後,輸入stats再按回車,即可獲得包括資源利用率在內的各種信息。 此外,輸入"stats slabs"或"stats items"還可以獲得關於緩存記錄的信息。 結束程序請輸入quit。

這些命令的詳細信息可以參考memcached軟件包內的protocol.txt文檔。

$ telnet localhost 11211
Trying ::1...
Connected to localhost.
Escape character is '^]'.
stats
STAT pid 481
STAT uptime 16574
STAT time 1213687612
STAT version 1.2.5
STAT pointer_size 32
STAT rusage_user 0.102297
STAT rusage_system 0.214317
STAT curr_items 0
STAT total_items 0
STAT bytes 0
STAT curr_connections 6
STAT total_connections 8
STAT connection_structures 7
STAT cmd_get 0
STAT cmd_set 0
STAT get_hits 0
STAT get_misses 0
STAT evictions 0
STAT bytes_read 20
STAT bytes_written 465
STAT limit_maxbytes 67108864
STAT threads 4
END
quit

另外,如果安裝了libmemcached這個面向C/C++語言的客戶端庫,就會安裝 memstat 這個命令。 使用方法很簡單,可以用更少的步驟獲得與telnet相同的信息,還能一次性從多臺服務器獲得信息。

$ memstat --servers=server1,server2,server3,...

libmemcached可以從下面的地址獲得:

查看slabs的使用狀況

使用memcached的創造着Brad寫的名爲memcached-tool的Perl腳本,可以方便地獲得slab的使用情況 (它將memcached的返回值整理成容易閱讀的格式)。可以從下面的地址獲得腳本:

使用方法也極其簡單:

$ memcached-tool 主機名:端口 選項

查看slabs使用狀況時無需指定選項,因此用下面的命令即可:

$ memcached-tool 主機名:端口

獲得的信息如下所示:

 #  Item_Size   Max_age  1MB_pages Count   Full?
1 104 B 1394292 s 1215 12249628 yes
2 136 B 1456795 s 52 400919 yes
3 176 B 1339587 s 33 196567 yes
4 224 B 1360926 s 109 510221 yes
5 280 B 1570071 s 49 183452 yes
6 352 B 1592051 s 77 229197 yes
7 440 B 1517732 s 66 157183 yes
8 552 B 1460821 s 62 117697 yes
9 696 B 1521917 s 143 215308 yes
10 872 B 1695035 s 205 246162 yes
11 1.1 kB 1681650 s 233 221968 yes
12 1.3 kB 1603363 s 241 183621 yes
13 1.7 kB 1634218 s 94 57197 yes
14 2.1 kB 1695038 s 75 36488 yes
15 2.6 kB 1747075 s 65 25203 yes
16 3.3 kB 1760661 s 78 24167 yes

各列的含義爲:

含義
# slab class編號
Item_Size Chunk大小
Max_age LRU內最舊的記錄的生存時間
1MB_pages 分配給Slab的頁數
Count Slab內的記錄數
Full? Slab內是否含有空閒chunk

從這個腳本獲得的信息對於調優非常方便,強烈推薦使用。

內存存儲的總結

本次簡單說明了memcached的緩存機制和調優方法。 希望讀者能理解memcached的內存管理原理及其優缺點。

下次將繼續說明LRU和Expire等原理,以及memcached的最新發展方向—— 可擴充體系(pluggable architecher))。

3. memcached的數據刪除機制及發展方向

memcached在數據刪除方面有效利用資源

數據不會真正從memcached中消失

上次介紹過, memcached不會釋放已分配的內存。記錄超時後,客戶端就無法再看見該記錄(invisible,透明), 其存儲空間即可重複使用。

Lazy Expiration

memcached內部不會監視記錄是否過期,而是在get時查看記錄的時間戳,檢查記錄是否過期。 這種技術被稱爲lazy(惰性)expiration。因此,memcached不會在過期監視上耗費CPU時間。

LRU:從緩存中有效刪除數據的原理

memcached會優先使用已超時的記錄的空間,但即使如此,也會發生追加新記錄時空間不足的情況, 此時就要使用名爲 Least Recently Used(LRU)機制來分配空間。 顧名思義,這是刪除“最近最少使用”的記錄的機制。 因此,當memcached的內存空間不足時(無法從slab class 獲取到新的空間時),就從最近未被使用的記錄中搜索,並將其空間分配給新的記錄。 從緩存的實用角度來看,該模型十分理想。

不過,有些情況下LRU機制反倒會造成麻煩。memcached啓動時通過“-M”參數可以禁止LRU,如下所示:

$ memcached -M -m 1024

啓動時必須注意的是,小寫的“-m”選項是用來指定最大內存大小的。不指定具體數值則使用默認值64MB。

指定“-M”參數啓動後,內存用盡時memcached會返回錯誤。 話說回來,memcached畢竟不是存儲器,而是緩存,所以推薦使用LRU。

memcached的最新發展方向

memcached的roadmap上有兩個大的目標。一個是二進制協議的策劃和實現,另一個是外部引擎的加載功能。

關於二進制協議

使用二進制協議的理由是它不需要文本協議的解析處理,使得原本高速的memcached的性能更上一層樓, 還能減少文本協議的漏洞。目前已大部分實現,開發用的代碼庫中已包含了該功能。 memcached的下載頁面上有代碼庫的鏈接。

二進制協議的格式

協議的包爲24字節的幀,其後面是鍵和無結構數據(Unstructured Data)。 實際的格式如下(引自協議文檔):

 Byte/     0       |       1       |       2       |       3       |   
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0/ HEADER /
/ /
/ /
/ /
+---------------+---------------+---------------+---------------+
24/ COMMAND-SPECIFIC EXTRAS (as needed) /
+/ (note length in th extras length header field) /
+---------------+---------------+---------------+---------------+
m/ Key (as needed) /
+/ (note length in key length header field) /
+---------------+---------------+---------------+---------------+
n/ Value (as needed) /
+/ (note length is total body length header field, minus /
+/ sum of the extras and key length body fields) /
+---------------+---------------+---------------+---------------+
Total 24 bytes

如上所示,包格式十分簡單。需要注意的是,佔據了16字節的頭部(HEADER)分爲 請求頭(Request Header)和響應頭(Response Header)兩種。 頭部中包含了表示包的有效性的Magic字節、命令種類、鍵長度、值長度等信息,格式如下:

Request Header

Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| Magic | Opcode | Key length |
+---------------+---------------+---------------+---------------+
4| Extras length | Data type | Reserved |
+---------------+---------------+---------------+---------------+
8| Total body length |
+---------------+---------------+---------------+---------------+
12| Opaque |
+---------------+---------------+---------------+---------------+
16| CAS |
| |
+---------------+---------------+---------------+---------------+
Response Header

Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+---------------+---------------+---------------+---------------+
0| Magic | Opcode | Key Length |
+---------------+---------------+---------------+---------------+
4| Extras length | Data type | Status |
+---------------+---------------+---------------+---------------+
8| Total body length |
+---------------+---------------+---------------+---------------+
12| Opaque |
+---------------+---------------+---------------+---------------+
16| CAS |
| |
+---------------+---------------+---------------+---------------+

如希望瞭解各個部分的詳細內容,可以checkout出memcached的二進制協議的代碼樹, 參考其中的docs文件夾中的protocol_binary.txt文檔。

HEADER中引人注目的地方

看到HEADER格式後我的感想是,鍵的上限太大了!現在的memcached規格中,鍵長度最大爲250字節, 但二進制協議中鍵的大小用2字節表示。因此,理論上最大可使用65536字節(2<sup>16</sup>)長的鍵。 儘管250字節以上的鍵並不會太常用,二進制協議發佈之後就可以使用巨大的鍵了。

二進制協議從下一版本1.3系列開始支持。

外部引擎支持

我去年曾經試驗性地將memcached的存儲層改造成了可擴展的(pluggable)。

MySQL的Brian Aker看到這個改造之後,就將代碼發到了memcached的郵件列表。 memcached的開發者也十分感興趣,就放到了roadmap中。現在由我和 memcached的開發者Trond Norbye協同開發(規格設計、實現和測試)。 和國外協同開發時時差是個大問題,但抱着相同的願景, 最後終於可以將可擴展架構的原型公佈了。 代碼庫可以從memcached的下載頁面 上訪問。

外部引擎支持的必要性

世界上有許多memcached的派生軟件,其理由是希望永久保存數據、實現數據冗餘等, 即使犧牲一些性能也在所不惜。我在開發memcached之前,在mixi的研發部也曾經 考慮過重新發明memcached。

外部引擎的加載機制能封裝memcached的網絡功能、事件處理等複雜的處理。 因此,現階段通過強制手段或重新設計等方式使memcached和存儲引擎合作的困難 就會煙消雲散,嘗試各種引擎就會變得輕而易舉了。

簡單API設計的成功的關鍵

該項目中我們最重視的是API設計。函數過多,會使引擎開發者感到麻煩; 過於複雜,實現引擎的門檻就會過高。因此,最初版本的接口函數只有13個。 具體內容限於篇幅,這裏就省略了,僅說明一下引擎應當完成的操作:

  • 引擎信息(版本等)
  • 引擎初始化
  • 引擎關閉
  • 引擎的統計信息
  • 在容量方面,測試給定記錄能否保存
  • 爲item(記錄)結構分配內存
  • 釋放item(記錄)的內存
  • 刪除記錄
  • 保存記錄
  • 回收記錄
  • 更新記錄的時間戳
  • 數學運算處理
  • 數據的flush

對詳細規格有興趣的讀者,可以checkout engine項目的代碼,閱讀器中的engine.h。

重新審視現在的體系

memcached支持外部存儲的難點是,網絡和事件處理相關的代碼(核心服務器)與 內存存儲的代碼緊密關聯。這種現象也稱爲tightly coupled(緊密耦合)。 必須將內存存儲的代碼從核心服務器中獨立出來,才能靈活地支持外部引擎。 因此,基於我們設計的API,memcached被重構成下面的樣子:


重構之後,我們與1.2.5版、二進制協議支持版等進行了性能對比,證實了它不會造成性能影響。

在考慮如何支持外部引擎加載時,讓memcached進行並行控制(concurrency control)的方案是最爲容易的, 但是對於引擎而言,並行控制正是性能的真諦,因此我們採用了將多線程支持完全交給引擎的設計方案。

以後的改進,會使得memcached的應用範圍更爲廣泛。

4. memcached中的分佈式算法

正如第1次中介紹的那樣, memcached雖然稱爲“分佈式”緩存服務器,但服務器端並沒有“分佈式”功能。 服務器端僅包括 第2次、 第3次 前阪介紹的內存存儲功能,其實現非常簡單。 至於memcached的分佈式,則是完全由客戶端程序庫實現的。 這種分佈式是memcached的最大特點。

memcached的分佈式是什麼意思?

這裏多次使用了“分佈式”這個詞,但並未做詳細解釋。 現在開始簡單地介紹一下其原理,各個客戶端的實現基本相同。

下面假設memcached服務器有node1~node3三臺, 應用程序要保存鍵名爲“tokyo”“kanagawa”“chiba”“saitama”“gunma” 的數據。


圖1 分佈式簡介:準備

首先向memcached中添加“tokyo”。將“tokyo”傳給客戶端程序庫後, 客戶端實現的算法就會根據“鍵”來決定保存數據的memcached服務器。 服務器選定後,即命令它保存“tokyo”及其值。


圖2 分佈式簡介:添加時

同樣,“kanagawa”“chiba”“saitama”“gunma”都是先選擇服務器再保存。

接下來獲取保存的數據。獲取時也要將要獲取的鍵“tokyo”傳遞給函數庫。 函數庫通過與數據保存時相同的算法,根據“鍵”選擇服務器。 使用的算法相同,就能選中與保存時相同的服務器,然後發送get命令。 只要數據沒有因爲某些原因被刪除,就能獲得保存的值。


圖3 分佈式簡介:獲取時

這樣,將不同的鍵保存到不同的服務器上,就實現了memcached的分佈式。 memcached服務器增多後,鍵就會分散,即使一臺memcached服務器發生故障 無法連接,也不會影響其他的緩存,系統依然能繼續運行。

接下來介紹第1次 中提到的Perl客戶端函數庫Cache::Memcached實現的分佈式方法。

Cache::Memcached的分佈式方法

Perl的memcached客戶端函數庫Cache::Memcached是 memcached的作者Brad Fitzpatrick的作品,可以說是原裝的函數庫了。

該函數庫實現了分佈式功能,是memcached標準的分佈式方法。

根據餘數計算分散

Cache::Memcached的分佈式方法簡單來說,就是“根據服務器臺數的餘數進行分散”。 求得鍵的整數哈希值,再除以服務器臺數,根據其餘數來選擇服務器。

下面將Cache::Memcached簡化成以下的Perl腳本來進行說明。

use strict;
use warnings;
use String::CRC32;

my @nodes = ('node1','node2','node3');
my @keys = ('tokyo', 'kanagawa', 'chiba', 'saitama', 'gunma');

foreach my $key (@keys) {
my $crc = crc32($key); # CRC値
my $mod = $crc % ( $#nodes + 1 );
my $server = $nodes[ $mod ]; # 根據餘數選擇服務器
printf "%s => %s\n", $key, $server;
}

Cache::Memcached在求哈希值時使用了CRC。

首先求得字符串的CRC值,根據該值除以服務器節點數目得到的餘數決定服務器。 上面的代碼執行後輸入以下結果:

tokyo       => node2
kanagawa => node3
chiba => node2
saitama => node1
gunma => node1

根據該結果,“tokyo”分散到node2,“kanagawa”分散到node3等。 多說一句,當選擇的服務器無法連接時,Cache::Memcached會將連接次數 添加到鍵之後,再次計算哈希值並嘗試連接。這個動作稱爲rehash。 不希望rehash時可以在生成Cache::Memcached對象時指定“rehash => 0”選項。

根據餘數計算分散的缺點

餘數計算的方法簡單,數據的分散性也相當優秀,但也有其缺點。 那就是當添加或移除服務器時,緩存重組的代價相當巨大。 添加服務器後,餘數就會產生鉅變,這樣就無法獲取與保存時相同的服務器, 從而影響緩存的命中率。用Perl寫段代碼來驗證其代價。

use strict;
use warnings;
use String::CRC32;

my @nodes = @ARGV;
my @keys = ('a'..'z');
my %nodes;

foreach my $key ( @keys ) {
my $hash = crc32($key);
my $mod = $hash % ( $#nodes + 1 );
my $server = $nodes[ $mod ];
push @{ $nodes{ $server } }, $key;
}

foreach my $node ( sort keys %nodes ) {
printf "%s: %s\n", $node, join ",", @{ $nodes{$node} };
}

這段Perl腳本演示了將“a”到“z”的鍵保存到memcached並訪問的情況。 將其保存爲mod.pl並執行。

首先,當服務器只有三臺時:

$ mod.pl node1 node2 nod3
node1: a,c,d,e,h,j,n,u,w,x
node2: g,i,k,l,p,r,s,y
node3: b,f,m,o,q,t,v,z

結果如上,node1保存a、c、d、e……,node2保存g、i、k……, 每臺服務器都保存了8個到10個數據。

接下來增加一臺memcached服務器。

$ mod.pl node1 node2 node3 node4
node1: d,f,m,o,t,v
node2: b,i,k,p,r,y
node3: e,g,l,n,u,w
node4: a,c,h,j,q,s,x,z

添加了node4。可見,只有d、i、k、p、r、y命中了。像這樣,添加節點後 鍵分散到的服務器會發生巨大變化。26個鍵中只有六個在訪問原來的服務器, 其他的全都移到了其他服務器。命中率降低到23%。在Web應用程序中使用memcached時, 在添加memcached服務器的瞬間緩存效率會大幅度下降,負載會集中到數據庫服務器上, 有可能會發生無法提供正常服務的情況。

mixi的Web應用程序運用中也有這個問題,導致無法添加memcached服務器。 但由於使用了新的分佈式方法,現在可以輕而易舉地添加memcached服務器了。 這種分佈式方法稱爲 Consistent Hashing。

Consistent Hashing

關於Consistent Hashing的思想,mixi株式會社的開發blog等許多地方都介紹過, 這裏只簡單地說明一下。

Consistent Hashing的簡單說明

Consistent Hashing如下所示:首先求出memcached服務器(節點)的哈希值, 並將其配置到0~232的圓(continuum)上。 然後用同樣的方法求出存儲數據的鍵的哈希值,並映射到圓上。 然後從數據映射到的位置開始順時針查找,將數據保存到找到的第一個服務器上。 如果超過232仍然找不到服務器,就會保存到第一臺memcached服務器上。


圖4 Consistent Hashing:基本原理

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


圖5 Consistent Hashing:添加服務器

因此,Consistent Hashing最大限度地抑制了鍵的重新分佈。 而且,有的Consistent Hashing的實現方法還採用了虛擬節點的思想。 使用一般的hash函數的話,服務器的映射地點的分佈非常不均勻。 因此,使用虛擬節點的思想,爲每個物理節點(服務器) 在continuum上分配100~200個點。這樣就能抑制分佈不均勻, 最大限度地減小服務器增減時的緩存重新分佈。

通過下文中介紹的使用Consistent Hashing算法的memcached客戶端函數庫進行測試的結果是, 由服務器臺數(n)和增加的服務器臺數(m)計算增加服務器後的命中率計算公式如下:

(1 - n/(n+m)) * 100

支持Consistent Hashing的函數庫

本連載中多次介紹的Cache::Memcached雖然不支持Consistent Hashing, 但已有幾個客戶端函數庫支持了這種新的分佈式算法。 第一個支持Consistent Hashing和虛擬節點的memcached客戶端函數庫是 名爲libketama的PHP庫,由last.fm開發。

至於Perl客戶端,連載的第1次 中介紹過的Cache::Memcached::Fast和Cache::Memcached::libmemcached支持 Consistent Hashing。

兩者的接口都與Cache::Memcached幾乎相同,如果正在使用Cache::Memcached, 那麼就可以方便地替換過來。Cache::Memcached::Fast重新實現了libketama, 使用Consistent Hashing創建對象時可以指定ketama_points選項。

my $memcached = Cache::Memcached::Fast->new({
servers => ["192.168.0.1:11211","192.168.0.2:11211"],
ketama_points => 150
});

另外,Cache::Memcached::libmemcached 是一個使用了Brain Aker開發的C函數庫libmemcached的Perl模塊。 libmemcached本身支持幾種分佈式算法,也支持Consistent Hashing, 其Perl綁定也支持Consistent Hashing。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章