Memcache 内存分配策略和性能(使用)状态检查

2022-12-26,,,,

前言:

一直在使用Memcache,但是对其内部的问题,如它内存是怎么样被使用的,使用一段时间后想看看一些状态怎么样?一直都不清楚,查了又忘记,现在整理出该篇文章,方便自己查阅。本文不涉及安装、操作。有兴趣的同学可以查看之前写的文章和Google。

1:参数

memcached -h 
memcached 1.4.
-p <num> TCP端口,默认为11211,可以不设置
-U <num> UDP端口,默认为11211,0为关闭
-s <file> UNIX socket
-a <mask> access mask for UNIX socket, in octal (default: )
-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.
-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: )
-B Binding protocol-one of ascii, binary or auto (default)
-I 调整分配slab页的大小,默认1M,最小1k到128M

上面加粗的参数,需要重点关注,正常启动的例子:

启动:
/usr/bin/memcached -m -p -u nobody -c -f 1.1 -I -d -l 10.211.55.9
连接:

telnet 10.211.55.9
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  >/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 < || $from > ;
undef $mode if $to < || $to > ;
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{$} = $;
$totalitems += $;
}
}
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{$} = $;
}
} foreach my $k (keys(%keyexp)) {
print $sock "get $k\r\n";
my $response = <$sock>;
if ($response =~ /VALUE (\S+) (\d+) (\d+)/) {
my $flags = $;
my $len = $;
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{$} = $;
}
}
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{$} = $;
}
}
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{$} = $;
}
}
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 = ;
while (<$sock>) {
last if /^END/;
if (/^STAT items:(\d+):(\w+) (\d+)/) {
$items{$}{$} = $;
}
} print $sock "stats slabs\r\n";
while (<$sock>) {
last if /^END/;
if (/^STAT (\d+):(\w+) (\d+)/) {
$items{$}{$} = $;
$max = $;
}
} print " # Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM\n";
foreach my $n (..$max) {
my $it = $items{$n};
next if ( == $it->{total_pages});
my $size = $it->{chunk_size} < ?
"$it->{chunk_size}B" :
sprintf("%.1fK", $it->{chunk_size} / 1024.0);
my $full = $it->{free_chunks_end} == ? "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
96B 20157s yes
120B 16049s yes
152B 17574s yes
192B 18157s yes
240B 18722s yes
304B 17971s yes
384B 17881s yes
480B 17760s yes
600B 18167s yes
752B 18518s yes
944B 18903s yes
.2K 20475s yes
.4K 21220s yes
.8K 22710s yes
.3K 22027s yes
.8K 23139s yes
.5K 23495s yes
.4K 22611s yes
.5K 23652s yes
.9K 21245s yes
.7K 22794s yes
.8K 22443s yes
.6K 21385s yes
.9K 23782s yes
.2K 23897s yes
.5K 27847s yes
.1K 27497s yes
.4K 28246s yes
.7K 33636s yes

解释:

含义
# 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还有很多项目会依赖它,所以还得学习来解决问题,后续出现新内容会不定时更新。

5,参考文档

Memcached内存分析、调优、集群

memcache内存分配、性能检测

memcached的基础

理解memcached的内存存储

memcached的删除机制和发展方向

memcached的分布式算法

memcached的应用和兼容程序

Memcached二三事儿

Memcache 内存分配策略和性能(使用)状态检查的相关教程结束。

《Memcache 内存分配策略和性能(使用)状态检查.doc》

下载本文的Word格式文档,以方便收藏与打印。