Memcache 記憶體分配策略和效能(使用)狀態檢查

jyzhou發表於2016-06-03

前言

一直在使用Memcache,但是對其內部的問題,如它記憶體是怎麼樣被使用的,使用一段時間後想看看一些狀態怎麼樣?一直都不清楚,查了又忘記,現在整理出該篇文章,方便自己查閱。本文不涉及安裝、操作。有興趣的同學可以檢視之前寫的文章和Google。

1.引數

memcached -h  
memcached 1.4.14
-p <num>           TCP埠,預設為11211,可以不設定
-U <num>           UDP埠,預設為11211,0為關閉
-s <file>          UNIX socket
-a <mask>          access mask for UNIX socket, in octal (default: 0700)
-l <addr>          監聽的 IP 地址,本機可以不設定此引數
-d                 以守護程式(daemon)方式執行
-u                 指定使用者,如果當前為 root ,需要使用此引數指定使用者
-m <num>           最大記憶體使用,單位MB。預設64MB
-M                 禁止LRU策略,記憶體耗盡時返回錯誤,而不是刪除項
-c <num>           最大同時連線數,預設是1024
-v                 verbose (print errors/warnings while in event loop)
-vv                very verbose (also print client commands/reponses)
-vvv               extremely verbose (also print internal state transitions)
-h                 幫助資訊
-i                 print memcached and libevent license
-P <file>          儲存PID到指定檔案
-f <factor>        增長因子,預設1.25
-n <bytes>         初始chunk=key+suffix+value+32結構體,預設48位元組
-L                 啟用大記憶體頁,可以降低記憶體浪費,改進效能
-t <num>           執行緒數,預設4。由於memcached採用NIO,所以更多執行緒沒有太多作用
-R                 每個event連線最大併發數,預設20
-C                 禁用CAS命令(可以禁止版本計數,減少開銷)
-b                 Set the backlog queue limit (default: 1024)
-B                 Binding protocol-one of ascii, binary or auto (default)
-I                 調整分配slab頁的大小,預設1M,最小1k到128M

上面加粗的引數,需要重點關注,正常啟動的例子:

啟動: /usr/bin/memcached -m 64 -p 11212 -u nobody -c 2048 -f 1.1 -I 1024 -d -l 10.211.55.9 連線:
telnet 10.211.55.9 11212
Trying 10.211.55.9...
Connected to 10.211.55.9.
Escape character is '^]'.

可以通過命令檢視所有引數:stats settings

2.理解memcached的記憶體儲存機制

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

Slab Allocator的基本原理是按照預先規定的大小,將分配的記憶體以page為單位,預設情況下一個page是1M,可以通過-I引數在啟動時指定,分割成各種尺寸的塊(chunk), 並把尺寸相同的塊分成組(chunk的集合),如果需要申請記憶體時,memcached會劃分出一個新的page並分配給需要的slab區域。page一旦被分配在重啟前不會被回收或者重新分配,以解決記憶體碎片問題。

Page

分配給Slab的記憶體空間,預設是1MB。分配給Slab之後根據slab的大小切分成chunk。

Chunk

用於快取記錄的記憶體空間。

Slab Class

特定大小的chunk的組。

Memcached並不是將所有大小的資料都放在一起的,而是預先將資料空間劃分為一系列slabs,每個slab只負責一定範圍內的資料儲存。memcached根據收到的資料的大小,選擇最適合資料大小的slab。memcached中儲存著slab內空閒chunk的列表,根據該列表選擇chunk,然後將資料快取於其中。

如圖所示,每個slab只儲存大於其上一個slab的size並小於或者等於自己最大size的資料。例如:100位元組大小的字串會被存到slab2(88-112)中,每個slab負責的空間是不等的,memcached預設情況下下一個slab的最大值為前一個的1.25倍,這個可以通過修改-f引數來修改增長比例。

Slab Allocator解決了當初的記憶體碎片問題,但新的機制也給memcached帶來了新的問題。chunk是memcached實際存放快取資料的地方,這個大小就是管理它的slab的最大存放大小。每個slab中的chunk大小是一樣的,如上圖所示slab1的chunk大小是88位元組,slab2是112位元組。由於分配的是特定長度的記憶體,因此無法有效利用分配的記憶體。例如,將100位元組的資料快取到128位元組的chunk中,剩餘的28位元組就浪費了。這裡需要注意的是chunk中不僅僅存放快取物件的value而且儲存了快取物件的key,expire time, flag等詳細資訊。所以當set 1位元組的item,需要遠遠大於1位元組的空間存放。

memcached在啟動時指定 Growth Factor因子(通過-f選項), 就可以在某種程度上控制slab之間的差異。預設值為1.25。

slab的記憶體分配具體過程如下:

Memcached在啟動時通過-m引數指定最大使用記憶體,但是這個不會一啟動就佔用完,而是逐步分配給各slab的。如果一個新的資料要被存放,首先選擇一個合適的slab,然後檢視該slab是否還有空閒的chunk,如果有則直接存放進去;如果沒有則要進行申請,slab申請記憶體時以page為單位,無論大小為多少,都會有1M大小的page被分配給該slab(該page不會被回收或者重新分配,永遠都屬於該slab)。申請到page後,slab會將這個page的記憶體按chunk的大小進行切分,這樣就變成了一個chunk的陣列,再從這個chunk陣列中選擇一個用於儲存資料。若沒有空閒的page的時候,則會對改slab進行LRU,而不是對整個memcache進行LRU。

以上大致講解了memcache的記憶體分配策略,下面來說明如何檢視memcache的使用狀況。

3.memcache狀態和效能檢視

 命中率 :stats命令

按照下面的圖來解讀分析

get_hits表示讀取cache命中的次數,get_misses是讀取失敗的次數,即嘗試讀取不存在的快取資料。即:

命中率=get_hits / (get_hits + get_misses) 

命中率越高說明cache起到的快取作用越大。但是在實際使用中,這個命中率不是有效資料的命中率,有些時候get操作可能只是檢查一個key存在不存在,這個時候miss也是正確的,這個命中率是從memcached啟動開始所有的請求的綜合值,不能反映一個時間段內的情況,所以要排查memcached的效能問題,還需要更詳細的數值。但是高的命中率還是能夠反映出memcached良好的使用情況,突然下跌的命中率能夠反映大量cache丟失的發生。

② 觀察各slab的items的情況:Stats items命令

主要引數說明:

outofmemory slab class為新item分配空間失敗的次數。這意味著你執行時帶上了-M或者移除操作失敗
number 存放的資料總數
age 存放的資料中存放時間最久的資料已經存在的時間,以秒為單位
evicted 不得不從LRU中移除未過期item的次數
evicted_time 自最後一次清除過期item起所經歷的秒數,即最後被移除快取的時間,0表示當前就有被移除,用這個來判斷資料被移除的最近時間
evicted_nonzero 沒有設定過期時間(預設30天),但不得不從LRU中稱除該未過期的item的次數

因為memcached的記憶體分配策略導致一旦memcached的總記憶體達到了設定的最大記憶體表示所有的slab能夠使用的page都已經固定,這時如果還有資料放入,將導致memcached使用LRU策略剔除資料。而LRU策略不是針對所有的slabs,而是隻針對新資料應該被放入的slab,例如有一個新的資料要被放入slab 3,則LRU只對slab 3進行,通過stats items就可以觀察到這些剔除的情況。

注意evicted_time:並不是發生了LRU就程式碼memcached負載過載了,因為有些時候在使用cache時會設定過期時間為0,這樣快取將被存放30天,如果記憶體滿了還持續放入資料,而這些為過期的資料很久沒有被使用,則可能被剔除。把evicted_time換算成標準時間看下是否已經達到了你可以接受的時間,例如:你認為資料被快取了2天是你可以接受的,而最後被剔除的資料已經存放了3天以上,則可以認為這個slab的壓力其實可以接受的;但是如果最後被剔除的資料只被快取了20秒,不用考慮,這個slab已經負載過重了。

通過上面的說明可以看到當前的memcache的slab1的狀態:

items有305816個,有效時間最久的是21529秒,通過LRU移除未過期的items有95336839個,通過LRU移除沒有設定過期時間的未過期items有95312220個,當前就有被清除的items,啟動時沒有帶-M引數。

③ 觀察各slabs的情況:stats slabs命令

從Stats items中如果發現有異常的slab,則可以通過stats slabs檢視下該slab是不是記憶體分配的確有問題。

主要引數說明:

屬性名稱 屬性說明
chunk_size 當前slab每個chunk的大小
chunk_per_page 每個page能夠存放的chunk數
total_pages 分配給當前slab的page總數,預設1個page大小1M,可以計算出該slab的大小
total_chunks 當前slab最多能夠存放的chunk數,應該等於chunck_per_page * total_page
used_chunks 已經被佔用的chunks總數
free_chunks 過期資料空出的chunk但還沒有被使用的chunk數
free_chunks_end 新分配的但是還沒有被使用的chunk數

這裡需要注意total_pages 這個是當前slab總共分配大的page總數,如果沒有修改page的預設大小的情況下,這個數值就是當前slab能夠快取的資料的總大小(單位為M)。如果這個slab的剔除非常嚴重,一定要注意這個slab的page數是不是太少了。還有一個公式:

total_chunks = used_chunks + free_chunks + free_chunks_end

例外stats slabs還有2個屬性:

屬性名稱 屬性說明
active_slabs 活動的slab總數
total_malloced 實際已經分配的總記憶體數,單位為byte,這個數值決定了memcached實際還能申請多少記憶體,如果這個值已經達到設定的上限(和stats settings中的maxbytes對比),則不會有新的page被分配。

④ 物件數量的統計:stats sizes

注意:該命令會鎖定服務,暫停處理請求。該命令展示了固定chunk大小中的items的數量。也可以看出slab1(96byte)中有多少個chunks。

⑤ 檢視、匯出所有key:stats cachedump

在進入memcache中,大家都想檢視cache裡的key,類似redis中的keys *命令,在memcache裡也可以檢視,但是需要2步完成。

一是先列出items:

stats items --命令
...
...
STAT items:29:number 228
STAT items:29:age 34935
...
END

二是通過itemid取key,上面的id是29,再加上一個引數:為列出的長度,0為全部列出。

stats cachedump 29 0 --命令
ITEM 26457202 [49440 b; 1467262309 s]
...
ITEM 30017977 [45992 b; 1467425702 s]
ITEM 26634739 [48405 b; 1467437677 s]
END  --總共228個key

get 26634739  取value

如何匯出key呢?這裡就需要通過 echo … nc 來完成了

echo "stats cachedump 29 0" | nc 10.211.55.9 11212 >/home/zhoujy/memcache.log

在匯出的時候需要注意的是:cachedump命令每次返回的資料大小隻有2M,這個是memcached的程式碼中寫死的一個數值,除非在編譯前修改。

⑥ 另一個監控工具memcached-tool,一個perl寫的工具:memcache_tool.pl

#!/usr/bin/perl
#
# memcached-tool:
#   stats/management tool for memcached.
#
# Author:
#   Brad Fitzpatrick <brad@danga.com>
#
# Contributor:
#   Andrey Niakhaichyk <andrey@niakhaichyk.org>
#
# License:
#   public domain.  I give up all rights to this
#   tool.  modify and copy at will.
#

use strict;
use IO::Socket::INET;

my $addr = shift;
my $mode = shift || "display";
my ($from, $to);

if ($mode eq "display") {
    undef $mode if @ARGV;
} elsif ($mode eq "move") {
    $from = shift;
    $to = shift;
    undef $mode if $from < 6 || $from > 17;
    undef $mode if $to   < 6 || $to   > 17;
    print STDERR "ERROR: parameters out of range\n\n" unless $mode;
} elsif ($mode eq 'dump') {
    ;
} elsif ($mode eq 'stats') {
    ;
} elsif ($mode eq 'settings') {
    ;
} elsif ($mode eq 'sizes') {
    ;
} else {
    undef $mode;
}

undef $mode if @ARGV;

die
    "Usage: memcached-tool <host[:port] | /path/to/socket> [mode]\n
       memcached-tool 10.0.0.5:11211 display    # shows slabs
       memcached-tool 10.0.0.5:11211            # same.  (default is display)
       memcached-tool 10.0.0.5:11211 stats      # shows general stats
       memcached-tool 10.0.0.5:11211 settings   # shows settings stats
       memcached-tool 10.0.0.5:11211 sizes      # shows sizes stats
       memcached-tool 10.0.0.5:11211 dump       # dumps keys and values
WARNING! sizes is a development command.
As of 1.4 it is still the only command which will lock your memcached instance for some time.
If you have many millions of stored items, it can become unresponsive for several minutes.
Run this at your own risk. It is roadmapped to either make this feature optional
or at least speed it up.
" unless $addr && $mode;

my $sock;
if ($addr =~ m:/:) {
    $sock = IO::Socket::UNIX->new(
        Peer => $addr,
    );
}
else {
    $addr .= ':11211' unless $addr =~ /:\d+$/;

    $sock = IO::Socket::INET->new(
        PeerAddr => $addr,
        Proto    => 'tcp',
    );
}
die "Couldn't connect to $addr\n" unless $sock;

if ($mode eq 'dump') {
    my %items;
    my $totalitems;

    print $sock "stats items\r\n";

    while (<$sock>) {
        last if /^END/;
        if (/^STAT items:(\d*):number (\d*)/) {
            $items{$1} = $2;
            $totalitems += $2;
        }
    }
    print STDERR "Dumping memcache contents\n";
    print STDERR "  Number of buckets: " . scalar(keys(%items)) . "\n";
    print STDERR "  Number of items  : $totalitems\n";

    foreach my $bucket (sort(keys(%items))) {
        print STDERR "Dumping bucket $bucket - " . $items{$bucket} . " total items\n";
        print $sock "stats cachedump $bucket $items{$bucket}\r\n";
        my %keyexp;
        while (<$sock>) {
            last if /^END/;
            # return format looks like this
            # ITEM foo [6 b; 1176415152 s]
            if (/^ITEM (\S+) \[.* (\d+) s\]/) {
                $keyexp{$1} = $2;
            }
        }

        foreach my $k (keys(%keyexp)) {
            print $sock "get $k\r\n";
            my $response = <$sock>;
            if ($response =~ /VALUE (\S+) (\d+) (\d+)/) {
                my $flags = $2;
                my $len = $3;
                my $val;
                read $sock, $val, $len;
                print "add $k $flags $keyexp{$k} $len\r\n$val\r\n";
                # get the END
                $_ = <$sock>;
                $_ = <$sock>;
            }
        }
    }
    exit;
}

if ($mode eq 'stats') {
    my %items;

    print $sock "stats\r\n";

    while (<$sock>) {
        last if /^END/;
        chomp;
        if (/^STAT\s+(\S*)\s+(.*)/) {
            $items{$1} = $2;
        }
    }
    printf ("#%-17s %5s %11s\n", $addr, "Field", "Value");
    foreach my $name (sort(keys(%items))) {
        printf ("%24s %12s\n", $name, $items{$name});

    }
    exit;
}

if ($mode eq 'settings') {
    my %items;

    print $sock "stats settings\r\n";

    while (<$sock>) {
        last if /^END/;
        chomp;
        if (/^STAT\s+(\S*)\s+(.*)/) {
            $items{$1} = $2;
        }
    }
    printf ("#%-17s %5s %11s\n", $addr, "Field", "Value");
    foreach my $name (sort(keys(%items))) {
        printf ("%24s %12s\n", $name, $items{$name});
    }
    exit;
}

if ($mode eq 'sizes') {
    my %items;

    print $sock "stats sizes\r\n";

    while (<$sock>) {
        last if /^END/;
        chomp;
        if (/^STAT\s+(\S*)\s+(.*)/) {
            $items{$1} = $2;
        }
    }
    printf ("#%-17s %5s %11s\n", $addr, "Size", "Count");
    foreach my $name (sort(keys(%items))) {
        printf ("%24s %12s\n", $name, $items{$name});
    }
    exit;
}

# display mode:

my %items;  # class -> { number, age, chunk_size, chunks_per_page,
#            total_pages, total_chunks, used_chunks,
#            free_chunks, free_chunks_end }

print $sock "stats items\r\n";
my $max = 0;
while (<$sock>) {
    last if /^END/;
    if (/^STAT items:(\d+):(\w+) (\d+)/) {
        $items{$1}{$2} = $3;
    }
}

print $sock "stats slabs\r\n";
while (<$sock>) {
    last if /^END/;
    if (/^STAT (\d+):(\w+) (\d+)/) {
        $items{$1}{$2} = $3;
        $max = $1;
    }
}

print "  #  Item_Size  Max_age   Pages   Count   Full?  Evicted Evict_Time OOM\n";
foreach my $n (1..$max) {
    my $it = $items{$n};
    next if (0 == $it->{total_pages});
    my $size = $it->{chunk_size} < 1024 ?
        "$it->{chunk_size}B" :
        sprintf("%.1fK", $it->{chunk_size} / 1024.0);
    my $full = $it->{free_chunks_end} == 0 ? "yes" : " no";
    printf("%3d %8s %9ds %7d %7d %7s %8d %8d %4d\n",
           $n, $size, $it->{age}, $it->{total_pages},
           $it->{number}, $full, $it->{evicted},
           $it->{evicted_time}, $it->{outofmemory});
}
./memcached-tool 10.211.55.9:11212 --執行
  #  Item_Size  Max_age   Pages   Count   Full?  Evicted Evict_Time OOM
  1      96B     20157s      28  305816     yes 95431913        0    0
  2     120B     16049s      40  349520     yes 117041737        0    0
  3     152B     17574s      39  269022     yes 92679465        0    0
  4     192B     18157s      43  234823     yes 78892650        0    0
  5     240B     18722s      52  227188     yes 72908841        0    0
  6     304B     17971s      73  251777     yes 85556469        0    0
  7     384B     17881s      81  221130     yes 75596858        0    0
  8     480B     17760s      70  152880     yes 53553607        0    0
  9     600B     18167s      58  101326     yes 34647962        0    0
 10     752B     18518s      52   72488     yes 24813707        0    0
 11     944B     18903s      52   57720     yes 16707430        0    0
 12     1.2K     20475s      44   38940     yes 11592923        0    0
 13     1.4K     21220s      36   25488     yes  8232326        0    0
 14     1.8K     22710s      35   19740     yes  6232766        0    0
 15     2.3K     22027s      33   14883     yes  4952017        0    0
 16     2.8K     23139s      33   11913     yes  3822663        0    0
 17     3.5K     23495s      31    8928     yes  2817520        0    0
 18     4.4K     22611s      29    6670     yes  2168871        0    0
 19     5.5K     23652s      29    5336     yes  1636656        0    0
 20     6.9K     21245s      26    3822     yes  1334189        0    0
 21     8.7K     22794s      22    2596     yes   783620        0    0
 22    10.8K     22443s      19    1786     yes   514953        0    0
 23    13.6K     21385s      18    1350     yes   368016        0    0
 24    16.9K     23782s      16     960     yes   254782        0    0
 25    21.2K     23897s      14     672     yes   183793        0    0
 26    26.5K     27847s      13     494     yes   117535        0    0
 27    33.1K     27497s      14     420     yes    83966        0    0
 28    41.4K     28246s      14     336     yes    63703        0    0
 29    51.7K     33636s      12     228     yes    24239        0    0

解釋:

含義
# slab class編號
Item_Size chunk大小
Max_age LRU內最舊的記錄的生存時間
pages 分配給Slab的頁數
count Slab內的記錄數、chunks數、items數、keys數
Full? Slab內是否含有空閒chunk
Evicted 從LRU中移除未過期item的次數
Evict_Time 最後被移除快取的時間,0表示當前就有被移除
OOM -M引數?

4.總結

實際應用Memcached時,我們遇到的很多問題都是因為不瞭解其記憶體分配機制所致,希望本文能讓大家初步瞭解Memcached在記憶體方便的分配機制,雖然redis等一些nosql的資料庫產品在很多產品中替換了memcache,但是memcache還有很多專案會依賴它,所以還得學習來解決問題,後續出現新內容會不定時更新。

相關文章