[TOC]
memcached 有2个版本。需要注意。
介绍
特点:
- 协议简单
- 基于
libevent
的时间处理。 - 内置内存存储方式;
- 采用互不通信的分布式。(这里的分布式只是为了扩大能存多少的问题,而不是稳定性。)
从上述特点4可见,基于memcached的memcacheq
有个很大的缺点,就是做分布式其实不稳定,如果其中一个服务器down掉,就会丢失消息,这时只能通过重启机器来解决。也就是必须有个监控服务才能保证稳定性。
基本特性
(1)mc的核心职能是KV内存管理,value存储最大为1M(可修改,改源代码,编译安装),它不支持复杂数据结构(哈希、列表、集合、有序集合等);
(2)mc不支持持久化;
(3)mc支持key过期;
(4)mc持续运行很少会出现内存碎片,速度不会随着服务运行时间降低;
(5)mc使用非阻塞IO复用网络模型,使用监听线程/工作线程的多线程模型;
安装
先安装libevent
, 再安装memcached
。网上随便找个文章即可。
启动参数
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
php使用介绍
<?php
$mc = new Memcache();
$mc->connect('127.0.0.1');
$mc->set('key', 'value',MEMCACHE_COMPRESSED,1);
$val = $mc->get('key');// value
$mc->delete('key');
$mc->flush();
$mc->close();
常用api
connect(string $host, int $port, int $timeout)
: 直接链接,适合单机addServer($host, $port, $persistent, $weight, $timeout, $retry_intval, $status, $failure_callback)
:分布式使用。按某算法选择服务器,见后文。- $persistent 是否为长连接。
- $retry_intval 链接失败时重试频率,默认15秒。-1将禁止重试。
retry_intval为-1。失败的服务器自动进入不可用池子里。再次请求直接失败。
add($k, $v, $flag, $expire)
: 添加。已存在时失败。最大过期时间30天。$v最大1M。 $flag可使用MEMCACHE_COMPRESSED
压缩。大于一定大小才会压缩。replace($k, $v, $flag, $expire)
: 替换set($k, $v, $flag, $expire))
: 基本用这个。是add
和replace
的结合体。get($k, $flag)
: 支持单个取,也支持批量取。delete($k, $timeout)
: 删除。$timeout
可设置延迟删除。flush
: 请空缓存。只是打标记,并不立即清除。getServerStatus($host, int $port)
获取服务器状态。需要先连服务器才能调用。getStats ($type = null, $slabid = null, $limit = 100)
获取统计信息- $type, Valid values are {reset, malloc, maps, cachedump, slabs, items, sizes}.
感觉要保持缓存和数据库的一致性得做好ORM
。 直接hash语句,实现简单,但是没法保证数据的一致性,而且不好管理。
class User {
protected $id;
protected $name;
protected $password;
protected $gender;
protected $_cache;
protected $_changed;
protected $_prefix = 'user_';
public function __construct()
{
$mc = new Memcache();
$mc->connect('127.0.0.1');
$this->_cache = $mc;
}
function __set($name, $value)
{
if ($this->$name != $value) {
$this->$name = $value;
$this->_changed = true;
}
}
public function findById($id) {
$u = $this->_cache->get($this->_prefix.$id);
if (!$u) {
//load from db
$this->_cache->set($this->_prefix.$id, $this->serialize($u), 2, 5*86400);
}
$u = $this->unserialize($u);
return $u;
}
function serialize() {
return serialize($this);
}
function unserialize($v) {
return unserialize($v);
}
function save() {
if($this->_changed) {
//up db
$this->_cache->set($this->_prefix.$this->id, $this->serialize(), 2, 5*86400);
}
}
}
$u = new User();
$u->id = 1;
$u->name ='kitty';
$u->password = 'good';
$u->save();
var_dump($u->findById(1));
深入了解
Memcached 如何支持的高并发
使用的多路复用I/O(epoll、select等)。
Slab分配算法保存数据
几个概念:
Page:分配给Slab的内存空间,默认是1MB。分配给Slab之后根据slab的大小切分成chunk。
chunk:它是将内存分配给用户使用的最小单元。
slab:它会管理一个固定chunk size的若干个chunk,而mc的内存管理,由若干个slab组成。
item:用户要存储的数据,包含key和value,最终都存储在chunk里。
如上图所示,一系列slab,分别管理128B,256B,512B…的chunk内存单元。
将上图中管理128B的slab0放大:
能够发现slab中的一些核心数据结构是:
- chunk_size:该slab管理的是128B的chunk
- free_chunk_list:用于快速找到空闲的chunk
- chunk[]:已经预分配好,用于存放用户item数据的实际chunk空间
画外音:其实还有lru_list。
假如用户要存储一个100B的item,是如何找到对应的可用chunk的呢?
会从最接近item大小的slab的chunk[]中,通过free_chunk_list快速找到对应的chunk,如上图所示,与item大小最接近的chunk是128B。
为什么不会出现内存碎片呢?
拿到一个128B的chunk,去存储一个100B的item,余下的28B不会再被其他的item所使用,即:实际上浪费了存储空间,来减少内存碎片,保证访问的速度。
画外音:理论上,内存碎片几乎不存在。
memcache通过slab,chunk,free_chunk_list来快速分配内存,存储用户的item,那它又是如何快速实现key的查找的呢?
没有什么特别算法:
-
通过hash表实现快速查找
-
通过链表来解决冲突
用最朴素的方式,实现key的快速查找。
随着item的个数不断增多,hash冲突越来越大,hash表如何保证查询效率呢?
当item总数达到hash表长度的1.5倍时,hash表会动态扩容,rehash将数据重新分布,以保证查找效率不会不断降低。
扩展hash表之后,同一个key在新旧hash表内的位置会发生变化,如何保证数据的一致性,以及如何保证迁移过程服务的可用性呢(肯定不能加一把大锁,迁移完成数据,再重新服务吧)?
哈希表扩展,数据迁移是一个耗时的操作,会有一个专门的线程来实施,为了避免大锁,采用的是“分段迁移”的策略。
当item数量达到阈值时,迁移线程会分段迁移,对hash表中的一部分桶进行加锁,迁移数据,解锁:
- 一来,保证不会有长时间的阻塞,影响服务的可用性
- 二来,保证item不会在新旧hash表里不一致
新的问题来了,对于已经存在与旧hash表中的item,可以通过上述方式迁移,那么**在item迁移的过程中,**如果有新的item插入,是应该插入旧hash表还是新hash表呢?
memcache的做法是,判断旧hash表中,item应该插入的桶,是否已经迁移至新表中:
- 如果已经迁移,则item直接插入新hash表
- 如果还没有被迁移,则直接插入旧hash表,未来等待迁移线程来迁移至新hash表
为什么要这么做呢,不能直接插入新hash表吗?
memcache没有给出官方的解释,楼主揣测,这种方法能够保证一个桶内的数据,只在一个hash表中(要么新表,要么旧表),任何场景下都不会出现,旧表新表查询两次,以提升查询速度。
默认不能高于1M
就是这个算法原因,避免生成太多的内存碎片。
可通过修改POWER_BLOCK
改成更大的值。
slab 按 POWER_BLOCK
申请内存,然后把这块内存再分割成若干个chunk
, 存储perslab
个。
如此存储的时候,就按数据的size 找对应的slab
, 分配里面的chunk
使用。
通过memcached -vv
有个直观的了解。
删除过期item
- 当某个值过期后,并没有从内存删除,因此stats统计时,curr_item有信息。
- 如果没有get取其值时,将不会自动删除。
- 当get取其值时,如果过期,返回空,并且清空,所以curr_item就减少了。
- 当内存不够用时,会按
LRU
算法删除。
分布式
普通hash
就是把$key
按某种哈希算法返回一个整数n, 然后n%服务器个数
取模得到指定的服务器。
也就是当少/多了服务器后,之前的缓存其实都会失效。
一致性hash
按同样的hash
算法把$key
和$ip
(服务器ip) hash到一个圆环上。
$key
沿着顺时针方向旋转,遇到$ip
时,就存到这个服务器上。
这样当少/多服务器后,影响的只是一台服务器的数据。其他服务器保持不变。
这种得自己实现,还得重新实现连接池。
具体参考一致性hash算法
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-pool
memcached-tool脚本可以方便地获得slab的使用情况 (它将memcached的返回值整理成容易阅读的格式),可以从下面的地址获得脚本:
http://www.netingcn.com/demo/memcached-tool.zip
memcached-tool server_ip:port option
例子:
./memcached-tool 10.72.23.179:12001 stats ./memcached-tool 10.72.23.179:12001 display
输出示例:
./memcached-tool 192.168.10.49:11235
# Item_Size Max_age 1MB_pages Count Full?
1 96 B 53345 s 3 25095 yes
2 192 B 53345 s 3 14433 yes
3 384 B 53320 s 3 6270 yes
4 768 B 53345 s 5 5600 yes
5 1.5 kB 53341 s 11 7406 yes
6 3.0 kB 53345 s 68 23124 yes
7 6.0 kB 53336 s 66 11207 yes
8 12.0 kB 53326 s 52 4381 yes
9 24.0 kB 53342 s 20 810 yes
10 48.0 kB 53200 s 10 198 yes
11 96.0 kB 52613 s 11 102 yes
12 192.0 kB 53007 s 2 8 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参数? |