[開源學習_01]memcached學習

最近看nginx,有一部分緩存服務依靠memcached完成,於是學習了一下。

memcached也是開源軟件,提供網絡資源緩存服務,可集羣部署。緩存資源存儲在內存中,CPU消耗低。通過自有協議與客戶端交互,客戶端通過key-value來查詢、存儲資源。

memcached源碼下載地址:http://dl.oschina.net/soft/memcached

自己的幾點感想:

1、memcached的內存碎片管理(下面轉載文章的第二篇有介紹)、LRU(最近最少使用機制)實現不復雜,自己的項目中可以參考,值得認真看看。

2、動態擴張的hash表,根據hash衝突與裝載因子成正比的原理,memcached在(hash表項數/hash桶數)>1.5時,擴大hash桶個數,該操作由maintence線程來維護。

3、利用libev的事件調度框架來管理事件。

4、從代碼中明顯感覺鎖機制設計的比較粗,很多鎖的鎖範圍比較大,多線程下表現不佳(官方也有說明,facebook在使用memcached時專門對鎖做了優化)

5、這裏簡單說明一下memcached的線程隊列部署,如下圖:


  • 主線程創建listen socket監聽客戶端的請求,並調用accept完成建鏈。通過輪巡的方式來分發任務給各個業務線程,即將accept socket以及相關信息放入某個業務線程的隊列,並通過pipe通知該業務線程讀取任務。這裏要注意的是,圖中畫的是tcp連接的流程,採用輪巡機制分發accpet socket給各個業務線程處理沒有問題,因爲accept socket由業務線程自己來監聽。但是,如果是udp流程,這種方法必然導致同一條流(五元組相同)的報文被分到不同的業務線程處理。爲了規避這個問題,memcached採用的辦法竟然是每個業務線程都送一份。。好吧,本來還想看下大神是如何做的,沒戲了。
  • memcached在操作隊列時給隊列加了一把鎖,感覺沒有必要。從圖中可以看出,每個隊列都是1進1出,完全可以實現無鎖。
  • 業務線程一直監聽自己與主線程之間的pipe socket,當收到有新連接到達的通知時,立即讀取自己的隊列,開始處理新的連接,進入自己的狀態機。
  • 維護線程通過條件信號量maintenance_cond把自己阻塞起來,業務線程在添加hash表項時會判斷hash表的裝載因子是否達到1.5,如果到達且hash表並非處在擴張狀態,則通過操作maintenance_cond信號來喚醒維護線程,通知維護線程進行hash表擴張。


----------------------------------------------------------我是分割線^_^----------------------------------------------------------

以下內容轉自網友翻譯,原文作者爲日本mixi的幾位工程師,講的很好。

文章出處:

http://blog.charlee.li/memcached-001/

http://blog.charlee.li/memcached-002/

http://blog.charlee.li/memcached-003/

http://blog.charlee.li/memcached-004/

http://blog.charlee.li/memcached-005/



1. memcached的基礎

翻譯一篇技術評論社的文章,是講memcached的連載。fcicq同學說這個東西很有用,希望大家喜歡。

我是mixi株式會社開發部系統運營組的長野。 日常負責程序的運營。從今天開始,將分幾次針對最近在Web應用的可擴展性領域 的熱門話題memcached,與我公司開發部研究開發組的前阪一起, 說明其內部結構和使用。

memcached是什麼?

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

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

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

memcached-0001-01.png

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的分佈式。

memcached-0001-02.png

接下來簡單介紹一下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');

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

刪除數據

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

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

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

增一和減一操作

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

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

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

總結

這次簡單介紹了memcached,以及它的安裝方法、Perl客戶端Cache::Memcached的用法。 只要知道,memcached的使用方法十分簡單就足夠了。

下次由前阪來說明memcached的內部結構。瞭解memcached的內部構造, 就能知道如何使用memcached才能使Web應用的速度更上一層樓。 歡迎繼續閱讀下一章。



2.理解memcached的內存存儲

下面是《memcached全面剖析》的第二部分。

我是mixi株式會社研究開發組的前阪徹。 上次的文章介紹了memcached是分佈式的高速緩存服務器。 本次將介紹memcached的內部構造的實現方式,以及內存的管理方式。 另外,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)。

memcached-0002-01.png

而且,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, 然後將數據緩存於其中。

memcached-0002-02.png

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

Slab Allocator的缺點

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

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

memcached-0002-03.png

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

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的前提。 
本次介紹memcached的數據刪除機制,以及memcached的最新發展方向——二進制協議(Binary Protocol) 和外部引擎支持。

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字節(216)長的鍵。 儘管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被重構成下面的樣子:

memcached-0003-001.png

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

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

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

總結

本次介紹了memcached的超時原理、內部如何刪除數據等,在此之上又介紹了二進制協議和 外部引擎支持等memcached的最新發展方向。這些功能要到1.3版纔會支持,敬請期待!

這是我在本連載中的最後一篇。感謝大家閱讀我的文章!

下次由長野來介紹memcached的應用知識和應用程序兼容性等內容。



4. memcached的分佈式算法

前幾次的文章在這裏:

我是Mixi的長野。 第2次、 第3次 由前阪介紹了memcached的內部情況。本次不再介紹memcached的內部結構, 開始介紹memcached的分佈式。

memcached的分佈式

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

memcached的分佈式是什麼意思?

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

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

memcached-0004-01.png

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

memcached-0004-02.png

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

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

memcached-0004-03.png

這樣,將不同的鍵保存到不同的服務器上,就實現了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~2SUP(32)的圓(continuum)上。 然後用同樣的方法求出存儲數據的鍵的哈希值,並映射到圓上。 然後從數據映射到的位置開始順時針查找,將數據保存到找到的第一個服務器上。 如果超過2SUP(32)仍然找不到服務器,就會保存到第一臺memcached服務器上。

memcached-0004-04.png

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

memcached-0004-05.png

因此,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。

總結

本次介紹了memcached的分佈式算法,主要有memcached的分佈式是由客戶端函數庫實現, 以及高效率地分散數據的Consistent Hashing算法。下次將介紹mixi在memcached應用方面的一些經驗, 和相關的兼容應用程序。



5. memcached的應用和兼容程序

前幾次的文章在這裏:

我是Mixi的長野。memcached的連載終於要結束了。 到上次爲止, 我們介紹了與memcached直接相關的話題,本次介紹一些mixi的案例和 實際應用上的話題,並介紹一些與memcached兼容的程序。

mixi案例研究

mixi在提供服務的初期階段就使用了memcached。 
隨着網站訪問量的急劇增加,單純爲數據庫添加slave已無法滿足需要,因此引入了memcached。 此外,我們也從增加可擴展性的方面進行了驗證,證明了memcached的速度和穩定性都能滿足需要。 現在,memcached已成爲mixi服務中非常重要的組成部分。

memcached-0005-01.png

服務器配置和數量

mixi使用了許許多多服務器,如數據庫服務器、應用服務器、圖片服務器、 
反向代理服務器等。單單memcached就有將近200臺服務器在運行。 memcached服務器的典型配置如下:

  • CPU:Intel Pentium 4 2.8GHz
  • 內存:4GB
  • 硬盤:146GB SCSI
  • 操作系統:Linux(x86_64)

這些服務器以前曾用於數據庫服務器等。隨着CPU性能提升、內存價格下降, 我們積極地將數據庫服務器、應用服務器等換成了性能更強大、內存更多的服務器。 這樣,可以抑制mixi整體使用的服務器數量的急劇增加,降低管理成本。 由於memcached服務器幾乎不佔用CPU,就將換下來的服務器用作memcached服務器了。

memcached進程

每臺memcached服務器僅啓動一個memcached進程。分配給memcached的內存爲3GB, 啓動參數如下:

/usr/bin/memcached -p 11211 -u nobody -m 3000 -c 30720

由於使用了x86_64的操作系統,因此能分配2GB以上的內存。32位操作系統中, 每個進程最多隻能使用2GB內存。也曾經考慮過啓動多個分配2GB以下內存的進程, 但這樣一臺服務器上的TCP連接數就會成倍增加,管理上也變得複雜, 所以mixi就統一使用了64位操作系統。

另外,雖然服務器的內存爲4GB,卻僅分配了3GB,是因爲內存分配量超過這個值, 就有可能導致內存交換(swap)。連載的第2次中 前阪講解過了memcached的內存存儲“slab allocator”,當時說過,memcached啓動時 指定的內存分配量是memcached用於保存數據的量,沒有包括“slab allocator”本身佔用的內存、 以及爲了保存數據而設置的管理空間。因此,memcached進程的實際內存分配量要比 指定的容量要大,這一點應當注意。

mixi保存在memcached中的數據大部分都比較小。這樣,進程的大小要比 
指定的容量大很多。因此,我們反覆改變內存分配量進行驗證, 確認了3GB的大小不會引發swap,這就是現在應用的數值。

memcached使用方法和客戶端

現在,mixi的服務將200臺左右的memcached服務器作爲一個pool使用。 每臺服務器的容量爲3GB,那麼全體就有了將近600GB的巨大的內存數據庫。 客戶端程序庫使用了本連載中多次提到車的Cache::Memcached::Fast, 與服務器進行交互。當然,緩存的分佈式算法使用的是 第4次介紹過的 Consistent Hashing算法。

應用層上memcached的使用方法由開發應用程序的工程師自行決定並實現。 但是,爲了防止車輪再造、防止Cache::Memcached::Fast上的教訓再次發生, 我們提供了Cache::Memcached::Fast的wrap模塊並使用。

通過Cache::Memcached::Fast維持連接

Cache::Memcached的情況下,與memcached的連接(文件句柄)保存在Cache::Memcached包內的類變量中。 
在mod_perl和FastCGI等環境下,包內的變量不會像CGI那樣隨時重新啓動, 而是在進程中一直保持。其結果就是不會斷開與memcached的連接, 減少了TCP連接建立時的開銷,同時也能防止短時間內反覆進行TCP連接、斷開 而導致的TCP端口資源枯竭。

但是,Cache::Memcached::Fast沒有這個功能,所以需要在模塊之外 將Cache::Memcached::Fast對象保持在類變量中,以保證持久連接。

package Gihyo::Memcached;

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

my @server_list = qw/192.168.1.1:11211 192.168.1.1:11211/;
my $fast;  ## 用於保持對象

sub new {
    my $self  = bless {}, shift;
    if ( !$fast ) {
        $fast = Cache::Memcached::Fast->new({ servers => \@server_list });
    }
    $self->{_fast} = $fast;
    return $self;
}

sub get {
   my $self = shift;
   $self->{_fast}->get(@_);
}

上面的例子中,Cache::Memcached::Fast對象保存到類變量$fast中。

公共數據的處理和rehash

諸如mixi的主頁上的新聞這樣的所有用戶共享的緩存數據、設置信息等數據, 會佔用許多頁,訪問次數也非常多。在這種條件下,訪問很容易集中到某臺memcached服務器上。 訪問集中本身並不是問題,但是一旦訪問集中的那臺服務器發生故障導致memcached無法連接, 就會產生巨大的問題。

連載的第4次 中提到,Cache::Memcached擁有rehash功能,即在無法連接保存數據的服務器的情況下, 會再次計算hash值,連接其他的服務器。

但是,Cache::Memcached::Fast沒有這個功能。不過,它能夠在連接服務器失敗時, 短時間內不再連接該服務器的功能。

my $fast = Cache::Memcached::Fast->new({
    max_failures     => 3,
    failure_timeout  => 1
});

在failuretimeout秒內發生maxfailures以上次連接失敗,就不再連接該memcached服務器。 我們的設置是1秒鐘3次以上。

此外,mixi還爲所有用戶共享的緩存數據的鍵名設置命名規則, 符合命名規則的數據會自動保存到多臺memcached服務器中, 取得時從中僅選取一臺服務器。創建該函數庫後,就可以使memcached服務器故障 不再產生其他影響。

memcached應用經驗

到此爲止介紹了memcached內部構造和函數庫,接下來介紹一些其他的應用經驗。

通過daemontools啓動

通常情況下memcached運行得相當穩定,但mixi現在使用的最新版1.2.5 曾經發生過幾次memcached進程死掉的情況。架構上保證了即使有幾臺memcached故障 也不會影響服務,不過對於memcached進程死掉的服務器,只要重新啓動memcached, 就可以正常運行,所以採用了監視memcached進程並自動啓動的方法。 於是使用了daemontools。

daemontools是qmail的作者DJB開發的UNIX服務管理工具集, 
其中名爲supervise的程序可用於服務啓動、停止的服務重啓等。

這裏不介紹daemontools的安裝了。mixi使用了以下的run腳本來啓動memcached。

#!/bin/sh

if [ -f /etc/sysconfig/memcached ];then
        . /etc/sysconfig/memcached
fi

exec 2>&1
exec /usr/bin/memcached -p $PORT -u $USER  -m $CACHESIZE -c $MAXCONN $OPTIONS

監視

mixi使用了名爲“nagios”的開源監視軟件來監視memcached。

在nagios中可以簡單地開發插件,可以詳細地監視memcached的get、add等動作。 不過mixi僅通過stats命令來確認memcached的運行狀態。

define command {
command_name                   check_memcached
command_line                   $USER1$/check_tcp -H $HOSTADDRESS$ -p 11211 -t 5 -E -s 'stats\r\nquit\r\n' -e 'uptime' -M crit
}

此外,mixi將stats目錄的結果通過rrdtool轉化成圖形,進行性能監視, 並將每天的內存使用量做成報表,通過郵件與開發者共享。

memcached的性能

連載中已介紹過,memcached的性能十分優秀。我們來看看mixi的實際案例。 這裏介紹的圖表是服務所使用的訪問最爲集中的memcached服務器。

memcached-0005-02.png

圖2 請求數

memcached-0005-03.png

圖3 流量

memcached-0005-04.png

圖4 TCP連接數

從上至下依次爲請求數、流量和TCP連接數。請求數最大爲15000qps, 流量達到400Mbps,這時的連接數已超過了10000個。 該服務器沒有特別的硬件,就是開頭介紹的普通的memcached服務器。 此時的CPU利用率爲:

memcached-0005-05.png

圖5 CPU利用率

可見,仍然有idle的部分。因此,memcached的性能非常高, 可以作爲Web應用程序開發者放心地保存臨時數據或緩存數據的地方。

兼容應用程序

memcached的實現和協議都十分簡單,因此有很多與memcached兼容的實現。 
一些功能強大的擴展可以將memcached的內存數據寫到磁盤上,實現數據的持久性和冗餘。 連載第3次 介紹過,以後的memcached的存儲層將變成可擴展的(pluggable),逐漸支持這些功能。

這裏介紹幾個與memcached兼容的應用程序。

  • repcached: 爲memcached提供複製(replication)功能的patch。
  • Flared: 存儲到QDBM。同時實現了異步複製和fail over等功能。
  • memcachedb: 存儲到BerkleyDB。還實現了message queue。
  • Tokyo Tyrant: 將數據存儲到Tokyo Cabinet。不僅與memcached協議兼容,還能通過HTTP進行訪問。

Tokyo Tyrant案例

mixi使用了上述兼容應用程序中的Tokyo Tyrant。Tokyo Tyrant是平林開發的 
Tokyo Cabinet DBM的網絡接口。它有自己的協議,但也擁有memcached兼容協議, 
也可以通過HTTP進行數據交換。Tokyo Cabinet雖然是一種將數據寫到磁盤的實現,但速度相當快。

mixi並沒有將Tokyo Tyrant作爲緩存服務器,而是將它作爲保存鍵值對組合的DBMS來使用。 
主要作爲存儲用戶上次訪問時間的數據庫來使用。它與幾乎所有的mixi服務都有關, 每次用戶訪問頁面時都要更新數據,因此負荷相當高。MySQL的處理十分笨重, 單獨使用memcached保存數據又有可能會丟失數據,所以引入了Tokyo Tyrant。 但無需重新開發客戶端,只需原封不動地使用Cache::Memcached::Fast即可, 這也是優點之一。關於Tokyo Tyrant的詳細信息,請參考本公司的開發blog。

總結

到本次爲止,“memcached全面剖析”系列就結束了。我們介紹了memcached的基礎、內部結構、 分散算法和應用等內容。讀完後如果您能對memcached產生興趣,就是我們的榮幸。 關於mixi的系統、應用方面的信息,請參考本公司的開發blog。 感謝您的閱讀。

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