>

简介

memcached 有以下特性:

  • 协议简单(ascii 或 binary 协议)
  • 基于 libevent 事件处理
  • 内置的内存存储方式
  • 不互相通信的分布式(由客户端程序负责分布式)
  • 基于 LRU(least recently used)算法自动删除不使用的缓存

memcached 事件模型

memcached 使用 libevent 作为其底层的网络库。

memcached 使用 master-worker 的方式, 以多线程对外提供服务, 主线程监听端口建立连接, 然后顺序分配给各个工作线程(对应工具的 -t 选项). 每个线程都有一个 event loop, 它们可以服务于不同的客户端. master 线程和 worker 线程之间使用管道通信, 每个工作的线程都会创建一个管道来保存写端和读端, 并将读端加入 event loop, 监听可读事件。多线程的模型可以充分发挥多核主机的优势.

memcached 内存分配

memcached 默认情况下采用了 Slab Allocator 的机制分配和管理内存. 在该机制出现之前内存分配简单的通过 malloc 和 free 来管理所有的记录,旧的方式会导致产生很多内存碎片, 加重机器管理内存的负担, 甚至有可能导致操作系统比 memcached 进程本身还慢, Slab Allocator 则解决了该问题.

Slab 的基本原理是按照预先规定的大小, 将分配的内存分割成特定长度的块(chunk), 以解决内存碎片的问题。这也意味着存取记录的时候可以减少内存分配的次数, 有点类似线程池/内存池的感觉。当往 memcache 中存储对象时,memcache 并不是简单的按对象大小来分配内存,因为这种情况下会导致内存碎片,加重操作系统内存管理器的负担。memcache 采用的是slab allocation 的内存分配策略,如下图所示:

这张图片里面涉及了slab_class、slab、page、chunk四个概念,它们之间的关系是:

1、Memcache 将内存空间分为一组slab
2、每个 slab 下又有若干个 page,每个 page 默认是 1M,如果一个 slab 占用 100M 内存的话,那么这个 slab 下应该有 100个page
3、每个 page 里面包含一组 chunk,chunk 是真正存放数据的地方,同一个 slab 里面的 chunk 的大小是固定的
4、有相同大小 chunk 的 slab 被组织在一起,称为 slab_class

Slaballocation的基本原理是按照预先规定的大小,将分配的内存以page为单位进行分割,默认情况下一个page是1M,可以通过-I参数在启动时指定,分割成各种尺寸的块(chunk),并把尺寸相同的块分成组(chunk的集合),如果需要申请内存时,memcached会划分出一个新的page并分配给需要的slab区域。page一旦被分配在重启前不会被回收或者重新分配,以解决内存碎片问题。

Slab 的原理也比较简单, 是将分配的内存分割成各种尺寸的块(chunk), 且把尺寸相同的 chunk 分成组(chunk 集合), 一个组称为 slab class.
Slab class 的主要术语包括以下:
page: 分配给 Slab 的内存空间, 默认是 1MB, 分配给 slab 之后根据 slab 大小分成 chunk.
chunk: 用于缓存记录的内存空间.
slab class: 特定大小的 chunk 的组.

由此可以看出, 三者之间在内存分配上的关系为 slab class -> page -> chunk, 比如以下信息:
$ memcached-tool 127.0.0.1:11211 display

Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM

1 96B 0s 1 0 no 0 0 0
2 120B 2417392s 30 257107 no 0 0 0

该 memcached 内存中, slab class 1 的 chunk 大小为 96 字节, 只分配了一个 page(1MB), 里面的记录数为 0, slab class 2 的 chunk 大小为 120 字节, 一共 30 个 page(约 30MB), 里面的记录数为 257107 个. 我们通过简单计算就可以得知, 30MB ~= 257107 * 120B. 在达到 30M 上线的时候, 如果再增加 key-value 值, 则直接分配一个 page 给 slab class 2, 这就是预分配的功能. 所以 Slab Allocation 的构造图大致如下:
slab class 1 slab class 2
+————-+ +—————+
| 96B | 96B | | 120B | 120B |
+——+——+ +——–+——+
| 96B |more..| | 120B |more..|
+————-+ +—————+

 slab class 3         slab class 4
+-------------+     +---------------+
| 152B | 152B |     | 192B  | 192B  |
+------+------+     +-------+-------+
| 152B |more..|     | 192B  |more.. |
+-------------+     +---------------+
.....

每个 slab class 的大小按照增长因子(-f 选项, 默认 1.25)增加,比如上述的 slab 1 默认为 96 字节, 那么 slab 2 则为 96*1.25 = 120 字节, 以此类推. 另外 slab allocator 还有重复使用已分配内存的目的. 也就是说, 分配的到内存不会释放, 而是重复利用.

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的内存分配策略,首先通过telnet命令连接上mencached服务器,通过stats slabs命令我们可以看到memcache的slab信息,从中里我们可以看到,一共有7个在使用中的slab,各个字段的含义如下:
chunk_size:slab中每个chunk的大小,这也是该slab能存储的最大item size上限

chunks_per_page:每个page中chunk数量 chuans_per_size = page大小/chunk_size

total_pages:该slab中的page数量

total_chunks:该slab中的chunk总数

used_chunks:已经使用过的chunk数量

free_chunks:未使用的chunk数量

total_malloced:实际已经分配的总内存数,这个数值决定memcache还能申请多少内存

通过stats命令可以查看memcache能申请的总内存大小。

limit_maxbytes表示memcache能申请的最大内存数,单位为byte,默认为64m。

slab 缓存记录的原理

memcached 会根据收到数据的大小选择合适的 slab class, slab 内保存着空闲的 chunk 列表, memcached 根据这些列表选择合适的 chunk, 然后将数据缓存到该 chunk 中. 如下图所示, 比如需要缓存 100 字节的记录, 则可以选择大小为 120B 的 chunk, 最后存到 slab class 2 中.

     Slab classes
+----------------+
| class 1 (96B)  |
+----------------+

+———-+ +–| class 2 (120B) |
| 100 byte | / +—————-+
+———-+ | class 3 (152B) |
+—————-+
| …… |
+—————-+

slab allocator 的缺点

尽管 slab 很好的解决了内存碎片的问题, 但该机制也给 memcached 带来了新的问题. 比如上述的缓存记录原理一节中, 由于分配的 chunk 都是固定的长度 120 字节, 将 100 字节存到改 chunk 中之后, 剩余的 20 字节就会被浪费, 如下:
| 120 bytes chunk |
+————-+——-+
| 100 bytes | … |
+————-+——-+

该问题还没有比较完美的解决方案, 不过应用程序如果能预先知道数据的大小, 只要尽力选择合适的 chunk, 就可以减少内存浪费, 比如 110 字节的数据都保存到 120字节的 chunk 中. 下面小节介绍的增长因子则能较好的减少内存浪费.

使用增长因子(growth factor)调优

上述小节中提到了增长因子(-f 选项), 此功能可以用来调节不同 slab class 之间 chunk 的大小差别, 默认为 1.25. 比如以下结果:

$ memcached-tool 127.0.0.1:11212 display

Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM

1 96B 2327514s 1 2 no 0 0 0
2 120B 2071695s 3 6981 no 0 0 0
3 152B 1038377s 23 90100 no 0 0 0
4 192B 0s 1 0 no 0 0 0
5 240B 2327378s 1 28 no 0 0 0
6 304B 2313765s 1 1 no 0 0 0
7 384B 2326507s 1 3 no 0 0 0
8 480B 2310076s 1 1 no 0 0 0
9 600B 2327669s 1 8 no 0 0 0
10 752B 2327425s 1 47 no 0 0 0

从 class 1 的 chunk 大小 96 字节开始, 增长因子为 1.25,后续的 class 2 的 chunk 大小即为 961.25 = 120, class 3 的即为 1201.25 = 152 等等. 从示例中我们看到, memcached 中的记录都保存在于 class 2 和 class 3 中. 这意味着大部分数据在 96 ~ 152 字节之间. 假如这里有很多 100 字节和 125 字节的记录的话, 可想而知会有很多的内存被浪费掉, 这个时候, 如果我们调节增长因子为 1.1, 则会减少很多内存的浪费, 如下:

Item_Size

1 96B
2 112B
3 128B

调整为 1.1 的因子后, 一个 100 字节的记录比调整前少浪费了 8 字节, 一个 125 字节的记录比调整前少浪费了 24 字节. 由此可见在内存使用很紧张的情况下, 调整增长因子也能节省相当多的内存.

如何处理连接过多的问题

这个问题在一些短连接的应用中较为突出, 比如使用短连接连接 memcached 的 php 应用, 每一次请求 php 就会新起一个进程连接 memcached,而每起一个进程则消耗一个本地端口, 因为本地端口为两个字节所以最大就是 65535, 如果在并发较大的情况下, 很容易发生本地端口不够用的情况。这个时候程序可以考虑通过 memcached socket 连接 memcached, 但是这种解决方案只适用于 memcached 和应用在一台机器上,如果不在一台机器上则可以考虑官方文档中介绍的使用 udp 协议连接, 而不是 tcp 协议连接, 不过在数据包较大的情况下会引起 udp 连接丢包的现象,这需要应用程序做一个很好的把控, 最好上线前做好测试。

查看 slabs 的使用状况

可以使用 memcached 作者编写的 memcahced-tool 工具查看 slabs 的使用状况, 比如上述示例中介绍的命令:
$ memcached-tool 127.0.0.1:11211 display

Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM

1 96B 0s 1 0 no 0 0 0
2 120B 2417392s 30 257107 no 0 0 0

各列的含义如下:

列 含义

slab class 编号

Item_Size chunk 大小
Max_age LRU 内最旧的记录的生存时间
Pages slab 中 page 的个数, 一个 page 默认大小为 1MB 大小
Count slab 内的记录数
Full slab 内是否含有空闲的 chunk
Evicted slab 中未过期的条目被 LRU 移除的个数
Evict_Time slab 中最近被移除的条目最后一次访问经过的秒数
OOM slab 中存储新条目失败的次数

memcached 删除机制

所有的数据都缓存到 memcached 中, 所以数据不会永久保存到磁盘上. 上述小节中也介绍过 memcahced 通过 slab 机制不会释放已分配的内存, 记录超时或删除后不会释放内存, 而是重复利用. 另外 memcached 内部不会监视过期的条目, 而是在 get 时查看时间戳, 检查记录是否过期. 这种特性成为 lazy expiration.

LRU: 删除数据的原理

memcached 优先使用超时的记录的内存空间, 但这也会发生增加新条目时空间不足的情况, memcached 采用 LRU(Least Recently Used) 机制解决内存不足的情况. LRU 即表示删除最近最少使用的意思, 很多数据库工具软件都采用 LRU 算法解决类似的问题, 比如 MySQL 的 InnoDB buffer 也采用该机制解决内存不足的问题; 在 memcached 可用内存不足的情况下, 就从最近未被使用的记录中进行搜索, 并将其分配给新的条目记录.

如果不想采用 LRU 机制可以使用 -M 选项启动 memcached, 该选项在内存用尽时直接返回错误.

memcached 二进制协议

老版本中仅支持 ascii 文本协议, 较新的版本都支持新的二进制协议, -B 选项可以选择要使用的协议, 默认情况自动选择. 使用了二进制协议后程序和 memcached 的交互不再需要文本的解析处理, 同样也能减少文本协议相关的漏洞, 所以理论上使用二进制协议效果更好.

memcached 源代码文件 memcached-master/doc/protocol-binary.xml 对二进制协议做了很详尽的描述, 一个 memcached 协议报文由两部分组成: header 和 memcached 命令. 这两部分组成一个通用格式的 memcached 协议报文, 如下:

General format of a packet:

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 the 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

可以看到一个memcached 的数据报文, 其中包括24字节的头部(header), 8 字节的 Extras 信息(包括 4 字节的 flag 和 4 字节的 expiration 字段), 以及相应的 key 和 value 长度字节, 这两个长度可以 header 头部中查找到.

另外 header 头部则可以分为请求 header 和应答 header,如下所示:

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 |
| |
+—————+—————+—————+—————+
Total 24 bytes

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 | Reserved |
+—————+—————+—————+—————+
8| Total body length |
+—————+—————+—————+—————+
12| Opaque |
+—————+—————+—————+—————+
16| CAS |
| |
+—————+—————+—————+—————+
Total 24 bytes

可以看到请求头部和应答头部的格式相同, 我们同样查看文档 doc/protocol-binary.xml, 各 header 字段的含义如下:

Magic: Magic number.
Opcode: Command code.
Key length: Length in bytes of the text key that follows the command extras.
Status: Status of the response (non-zero on error).
Extras length: Length in bytes of the command extras.
Data type: Reserved for future use (Sean is using this soon).
Reserved: Really reserved for future use (up for grabs).
Total body length: Length in bytes of extra + key + value.
Opaque: Will be copied back to you in the response.
CAS: Data version check.

抓包分析 memcached 二进制协议

我们使用以下 perl 程序以二进制方式连接测试 memcached, 再抓包分析进行说明, 如下程序:

#!/usr/bin/env perl
use strict;
use Data::Dumper;
use Cache::Memcached::libmemcached;

my $memd = Cache::Memcached::libmemcached->new({
‘servers’ => [ “127.0.0.1:11211”],
‘debug’ => 0,
‘compress_threshold’ => 10_000,
});
$memd->set_binary_protocol(1); # 使用二进制协议
print Dumper($memd);
$memd->set(“foo10”, “Some value”); # set 值

my $val = $memd->get(“foo10”);
if ($val) {
print Dumper($val);
}

抓包后, 使用 wireshark 分析, 我们以 set request 包分析如下, 下半部分为16进制数据:

Memcache Protocol, Set Request
Magic: Request (128)
Opcode: Set (1)
Key Length: 5
Extras length: 8
Data type: Raw bytes (0)
Reserved: 0
[Value length: 10]
Total body length: 23
Opaque: 65536
CAS: 0
Extras
Key: foo10
Value: Some value

0000 80 01 00 05 08 00 00 00 00 00 00 17 00 01 00 00 …………….
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….
0020 66 6f 6f 31 30 53 6f 6d 65 20 76 61 6c 75 65 foo10Some value

对比上一小节的 header 头部可以分析如下, memcached 的数据(memcached 通用报文)大小总共为 47 字节:
Magic: 80
Opcode: 01
Key Length: 00 05
Extras length: 08
Data type: 00
Status: 00 00
Total body length: 00 00 00 17
Opaque: 00 01 00 00
CAS: 00 00 00 00 00 00 00 00
Extras: 00 00 00 00 00 00 00 00 (4 字节 Flags, 4 字节 Expiration)
Key: 66 6f 6f 31 30
Value: 53 6f 6d 65 20 76 61 6c 75 65

key length 为两个字节, 这意味着 key 的最大长度可以到 65535 字节, 比起老的版本确实是很大的进步.

总结

综上介绍, 我们可以很好的理解 memcached slab 内存分配的机制, 也能理解通过增长因子就可以达到一定的性能优化, 在一些场景下可能会节省较多的内存消耗.最后通过测试详细说明了二进制协议在 memcached 中的报文格式, 对二进制格式有了较为深入的了解, 值得一提的是大部分的编程语言驱动包还只是支持文本协议, 要使用二进制协议需要安装非通用的驱动包。更多关于Memcached内部协议解析,请参考此处。其特性大致可以总结归纳为如下几点:
1、MemCache中可以保存的item数据量是没有限制的,只要内存足够

2、MemCache单进程在32位机中最大使用内存为2G,这个已经提了多次了,64位机则没有限制

3、Key最大为250个字节,超过该长度无法存储

4、单个item最大数据是1MB,超过1MB的数据不予存储

5、MemCache服务端是不安全的,比如已知某个MemCache节点,可以直接telnet过去,并通过flush_all让已经存在的键值对立即失效

6、不能够遍历MemCache中所有的item,因为这个操作的速度相对缓慢且会阻塞其他的操作

来源:
http://www.freeoa.net/osuport/servap/memcached-mem-alloca-optimiz_3180.html