2021-10-18 16:46:58

memcached使用介绍

[TOC]

memcached 有2个版本。需要注意。

介绍

特点:

  1. 协议简单
  2. 基于libevent的时间处理。
  3. 内置内存存储方式;
  4. 采用互不通信的分布式。(这里的分布式只是为了扩大能存多少的问题,而不是稳定性。)

从上述特点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

  1. connect(string $host, int $port, int $timeout): 直接链接,适合单机
  2. addServer($host, $port, $persistent, $weight, $timeout, $retry_intval, $status, $failure_callback):分布式使用。按某算法选择服务器,见后文。
    1. $persistent 是否为长连接。
    2. $retry_intval 链接失败时重试频率,默认15秒。-1将禁止重试。
    3. retry_intval为-1。失败的服务器自动进入不可用池子里。再次请求直接失败。
  3. add($k, $v, $flag, $expire): 添加。已存在时失败。最大过期时间30天。$v最大1M。 $flag可使用MEMCACHE_COMPRESSED压缩。大于一定大小才会压缩。
  4. replace($k, $v, $flag, $expire): 替换
  5. set($k, $v, $flag, $expire)): 基本用这个。是addreplace的结合体。
  6. get($k, $flag): 支持单个取,也支持批量取。
  7. delete($k, $timeout): 删除。$timeout可设置延迟删除。
  8. flush: 请空缓存。只是打标记,并不立即清除。
  9. getServerStatus($host, int $port) 获取服务器状态。需要先连服务器才能调用。
  10. getStats ($type = null, $slabid = null, $limit = 100) 获取统计信息
    1. $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里。

image-20210108204700325

如上图所示,一系列slab,分别管理128B,256B,512B…的chunk内存单元。

将上图中管理128B的slab0放大:

image-20210108204747030

能够发现slab中的一些核心数据结构是:

  • chunk_size:该slab管理的是128B的chunk
  • free_chunk_list:用于快速找到空闲的chunk
  • chunk[]:已经预分配好,用于存放用户item数据的实际chunk空间

画外音:其实还有lru_list。

假如用户要存储一个100B的item,是如何找到对应的可用chunk的呢?

image-20210108205506264

会从最接近item大小的slab的chunk[]中,通过free_chunk_list快速找到对应的chunk,如上图所示,与item大小最接近的chunk是128B。

为什么不会出现内存碎片呢?

image-20210108205555481

拿到一个128B的chunk,去存储一个100B的item,余下的28B不会再被其他的item所使用,即:实际上浪费了存储空间,来减少内存碎片,保证访问的速度。

画外音:理论上,内存碎片几乎不存在。

memcache通过slab,chunk,free_chunk_list来快速分配内存,存储用户的item,那它又是如何快速实现key的查找的呢?

没有什么特别算法:

image-20210108205642212

  • 通过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

  1. 当某个值过期后,并没有从内存删除,因此stats统计时,curr_item有信息。
  2. 如果没有get取其值时,将不会自动删除。
  3. 当get取其值时,如果过期,返回空,并且清空,所以curr_item就减少了。
  4. 当内存不够用时,会按LRU算法删除。

分布式

普通hash

就是把$key按某种哈希算法返回一个整数n, 然后n%服务器个数取模得到指定的服务器。
也就是当少/多了服务器后,之前的缓存其实都会失效。

一致性hash

按同样的hash算法把$key$ip(服务器ip) hash到一个圆环上。
$key沿着顺时针方向旋转,遇到$ip时,就存到这个服务器上。

这样当少/多服务器后,影响的只是一台服务器的数据。其他服务器保持不变。
这种得自己实现,还得重新实现连接池。

具体参考一致性hash算法

memcache状态和性能查看

命中率 : stats命令

img

按照下面的图来解读分析

img

get_hits表示读取cache命中的次数,get_misses是读取失败的次数,即尝试读取不存在的缓存数据。即:

命中率=get_hits / (get_hits + get_misses)

命中率越高说明cache起到的缓存作用越大。但是在实际使用中,这个命中率不是有效数据的命中率,有些时候get操作可能只是检查一个key存在不存在,这个时候miss也是正确的,这个命中率是从memcached启动开始所有的请求的综合值,不能反映一个时间段内的情况,所以要排查memcached的性能问题,还需要更详细的数值。但是高的命中率还是能够反映出memcached良好的使用情况,突然下跌的命中率能够反映大量cache丢失的发生。

观察各slab的items的情况:Stats items命令

img

主要参数说明:

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是不是内存分配的确有问题。

img

主要参数说明:

属性名称 属性说明
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个属性:

img

属性名称 属性说明
active_slabs 活动的slab总数
total_malloced 实际已经分配的总内存数,单位为byte,这个数值决定了memcached实际还能申请多少内存,如果这个值已经达到设定的上限(和stats settings中的maxbytes对比),则不会有新的page被分配。

对象数量的统计:stats sizes

img

注意:该命令会锁定服务,暂停处理请求。该命令展示了固定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参数?

参考

memcacheq

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

本文链接:http://blog.go2live.cn/post/memcached.html

-- EOF --