MemCached学习笔记

MemCache是什么:

      MemCache是一个自由、源码开放、高性能、分布式的分布式内存对象缓存系统,用于动态Web应用以减轻数据库的负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高了网站访问的速度。MemCaChe是一个存储键值对的HashMap,在内存中对任意的数据(比如字符串、对象等)所使用的key-value存储,数据可以来自数据库调用、API调用,或者页面渲染的结果。MemCache设计理念就是小而强大,它简单的设计促进了快速部署、易于开发并解决面对大规模的数据缓存的许多难题,而所开放的API使得MemCache能用于Java、C/C++/C#、Perl、Python、PHP、Ruby等大部分流行的程序语言。

Memcached工作原理介绍:

      Memcached是一套C/S模式架构软件,在服务端启动memcached服务守护进程。可以指定监听的IP地址、端口号、并发访问连接数以及分配多少内存处理客户端的请求参数。

          1)、Socket事件处理机制:

             Memcached软件是由C语言来实现的,全部代码仅有2000多行,采用的是异步epoll/kqueue非阻塞IO网络模型,其实现的方式是基于异步的libevent事件单进程、单线程模式,使用libevent作为事件通知机制,多个服务端协同工作,但是这些服务端之间是没有任何通讯联系,没有服务端只对自己的数据进行管理。应用程序通过指定缓存的IP地址和端口,就可以链接memcached服务互相通信。

          2)、数据存储机制:

   需要被缓存的数据以key/value键值对的形式保存在服务端预分配的内存中,每个被缓存的数据都有唯一的标识key,操作memcached中的数据通过这个唯一的key进行。缓存到memcached中的数据仅放置在memcached服务预分配的内存中,而非存储在memcached所在的磁盘上,因此存取的速度非常快。

             由于memcached服务自身没有对缓存的数据进行持久化性存储设计,因此,在服务器端的memcache服务进程重启之后,存储在内存中的这些数据就会丢失。且内存的缓存的数据容量达到启动时设定的内存值,就自动使用LRU算法删除过期的缓存数据。

            开发memcached的初衷就是仅为内存缓存而设计的,因此并没有过多的考虑数据的永久存储问题,因此,如果使用memecached作为缓存数据服务,要考虑数据丢失带来的问题,例如是否可以重新生成数据,还有在高并发场合数据丢失会不会导致网络架构雪崩。

  Memcached访问模型:

       MemCache虽然被称为"分布式缓存",但是MemCache本身完全不具备分布式的功能,MemCache集群之间不会相互通信(与之形成对比的,比如JBoss Cache,某台服务器有缓存数据更新时,会通知集群中其他机器更新缓存或清除缓存数据),所谓的"分布式",完全依赖于客户端程序的实现,如图:

  blob.png

    MemCache一次写缓存的流程:         

    1、应用程序输入需要写缓存的数据

    2、API将Key输入路由算法模块,路由算法根据Key和MemCache集群服务器列表得到一台服务器编号

    3、由服务器编号得到MemCache及其的ip地址和端口号

    4、API调用通信模块和指定编号的服务器通信,将数据写入该服务器,完成一次分布式缓存的写操作

        读缓存和写缓存一样,只要使用相同的路由算法和服务器列表,只要应用程序查询的是相同的Key,MemCache客户端总是访问相同的客户端去读取数据,只要服务器中还缓存着该数据,就能保证缓存命中。

        这种MemCache集群的方式也是从分区容错性的方面考虑的,假如Node2宕机了,那么Node2上面存储的数据都不可用了,此时由于集群中Node0和Node1还存在,下一次请求Node2中存储的Key值的时候,肯定是没有命中的,这时先从数据库中拿到要缓存的数据,然后路由算法模块根据Key值在Node0和Node1中选取一个节点,把对应的数据放进去,这样下一次就又可以走缓存了,这种集群的做法很好,但是缺点是成本比较大。

  

MemCache客户端路由算法:

    memcached的分布式主要体现在client端,对于server端,仅仅是部署多个memcached server组成集群,每个server独自维护自己的数据(互相之间没有任何通信),通过daemon监听端口等待client端的请求。

    而在client端,通过一致的hash算法,将要存储的数据分布到某个特定的server上进行存储,后续读取查询使用同样的hash算法即可定位。

      1、简单的Hash算法(余数Hash):    

           client端可以采用各种hash算法来定位server:最简单的hash算法: targetServer = serverList[hash(key) % serverList.size],这种算法不仅简单,而且具有不错的随机分布特性

比方说,字符串str对应的HashCode是50、服务器的数目是3,取余数得到2,str对应节点Node2,所以路由算法把str路由到Node2服务器上。由于HashCode随机性比较强,所以使用余数Hash路由算法就可以保证缓存数据在整个MemCache服务器集群中有比较均衡的分布。

如果不考虑服务器集群的伸缩性,那么余数Hash算法几乎可以满足绝大多数的缓存路由需求,但是当分布式缓存集群需要扩容的时候,就难办了。

就假设MemCache服务器集群由3台变为4台吧,更改服务器列表,仍然使用余数Hash,50对4的余数是2,对应Node2,但是str原来是存在Node1上的,这就导致了缓存没有命中。如果这么说不够明白,那么不妨举个例子,原来有HashCode为0~19的20个数据,那么:

    blob.png

    (2)通过模拟请求的方式逐渐预热缓存,使缓存服务器中的数据重新分布    如果我扩容到20+的台数,只有前三个HashCode对应的Key是命中的,也就是15%。当然这只是个简单例子,现实情况肯定比这个复杂得多,不过足以说明,使用余数Hash的路由算法,在扩容的时候会造成大量的数据无法正确命中(其实不仅仅是无法命中,那些大量的无法命中的数据还在原缓存中在被移除前占据着内存)。这个结果显然是无法接受的,在网站业务中,大部分的业务数据度操作请求上事实上是通过缓存获取的,只有少量读操作会访问数据库,因此数据库的负载能力是以有缓存为前提而设计的。当大部分被缓存了的数据因为服务器扩容而不能正确读取时,这些数据访问的压力就落在了数据库的身上,这将大大超过数据库的负载能力,严重的可能会导致数据库宕机。

这个问题有解决方案,解决步骤为:

    (1)在网站访问量低谷,通常是深夜,技术团队加班,扩容、重启服务器

    2、一致性hash:

      为了解决上述问题,需要采用一致性算法。相对于取模的算法,一致性hash算法除了计算key的hash值外,还会计算每个server对应的hash值,然后将这些hash值映射到一个有限的值域上(比如0~2^32)。通过寻找hash值大于hash(key)的最小server作为存储该key数据的目标server。如果找不到,则直接把具有最小hash值的server作为目标server。

为了方便理解,可以把这个有限值域理解成一个环,值顺时针递增。

         blob.png

           就如同图上所示,三个Node点分别位于Hash环上的三个位置,然后Key值根据其HashCode,在Hash环上有一个固定位置,位置固定下之后,Key就会顺时针去寻找离它最近的一个Node,把数据存储在这个Node的MemCache服务器中。使用Hash环如果加了一个节点会怎么样,看一下:

       blob.png

              看到我加了一个Node4节点,只影响到了一个Key值的数据,本来这个Key值应该是在Node1服务器上的,现在要去Node4了。采用一致性Hash算法,的确也会影响到整个集群,但是影响的只是加粗的那一段而已,相比余数Hash算法影响了远超一半的影响率,这种影响要小得多。更重要的是,集群中缓存服务器节点越多,增加节点带来的影响越小,很好理解。换句话说,随着集群规模的增大,继续命中原有缓存数据的概率会越来越大,虽然仍然有小部分数据缓存在服务器中不能被读到,但是这个比例足够小,即使访问数据库,也不会对数据库造成致命的负载压力。

            至于具体应用,这个长度为232的一致性Hash环通常使用二叉查找树实现。

            但是问题就是:增加和删除节点之后,会影响局部节点Cache的命中范围,一样会有负载不均问题。

    3、虚拟节点:

         为解决这个问题,需要使用虚拟节点的思想:

               上面的一致性Hash算法实现,可以在很大程度上解决很多分布式环境下不好的路由算法导致系统伸缩性差的问题,但是会带来另外一个问题:负载不均。

比如说有Hash环上有A、B、C三个服务器节点,分别有100个请求会被路由到相应服务器上。现在在A与B之间增加了一个节点D,这导致了原来会路由到B上的部分节点被路由到了D上,这样A、C上被路由到的请求明显多于B、D上的,原来三个服务器节点上均衡的负载被打破了。某种程度上来说,这失去了负载均衡的意义,因为负载均衡的目的本身就是为了使得目标服务器均分所有的请求。

解决这个问题的办法是引入虚拟节点,其工作原理是:为每个物理节点(server)在环上分配100~200个点,这样环上的节点较多,就能抑制分布不均匀。当为cache定位目标server时,如果定位到虚拟节点上,就表示cache真正的存储位置是在该虚拟节点代表的实际物理server上。

另外,如果每个实际server的负载能力不同,可以赋予不同的权重,根据权重分配不同数量的虚拟节点。

。采取这样的方式,就可以有效地解决增加或减少节点时候的负载不均衡的问题。

             参考:java的memcached client(gwhalin / Memcached-Java-Client)的源码来看一下consistent hash的实现。

Memcached内存管理机制深入剖析

     Malloc:

         mallloc的全称是memory allocation,中文叫动态内存分配,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态分配内存

       早期的Memcached内存管理方式是通过malloc分配的内存,使用后通过free来回首内存。种着方式容易产生内存碎片并降低操作系统对内存的管理效率。加重操作系统内存管理器的负担,最坏的情况下,会导致操作系统对Memcached进程本身还慢,为了解决上述问题,slab allocator内存分配机制就诞生了。

 

    Slab内存管理机制:

        按照预先规定的大小,将分配给memcached服务的内存预先分割成特定长度的内存块(Chunk),再把内存相同的内存块分成组(chunk slab class),这些内存块不会释放,可以重复利用。 

    blob.png

      

     这张图片里面涉及了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

   MemCache内存分配的方式称为allocator,slab的数量是有限的,几个、十几个或者几十个,这个和启动参数的配置相关。

   MemCache中的value过来存放的地方是由value的大小决定的,value总是会被存放到与chunk大小最接近的一个slab中,比如slab[1]的chunk大小为80字节、slab[2]的chunk大小为100字节、slab[3]的chunk大小为128字节(相邻slab内的chunk基本以1.25为比例进行增长,MemCache启动时可以用-f指定这个比例),那么过来一个88字节的value,这个value将被放到2号slab中。放slab的时候,首先slab要申请内存,申请内存是以page为单位的,所以在放入第一个数据的时候,无论大小为多少,都会有1M大小的page被分配给该slab。申请到page后,slab会将这个page的内存按chunk的大小进行切分,这样就变成了一个chunk数组,最后从这个chunk数组中选择一个用于存储数据。

   如果这个slab中没有chunk可以分配了怎么办,如果MemCache启动没有追加-M(禁止LRU,这种情况下内存不够会报Out Of Memory错误),那么MemCache会把这个slab中最近最少使用的chunk中的数据清理掉,然后放上最新的数据。针对MemCache的内存分配及回收算法,总结三点:

            1、MemCache的内存分配chunk里面会有内存浪费,88字节的value分配在128字节(紧接着大的用)的chunk中,就损失了30字节,但是这也避免了管理内存碎片的问题

           2、MemCache的LRU算法不是针对全局的,是针对slab的

           3、应该可以理解为什么MemCache存放的value大小是限制的,因为一个新数据过来,slab会先以page为单位申请一块内存,申请的内存最多就只有1M,所以value大小自然不能大于1M了

 

     Slab内存管理机制的特点:

1.      提前分配大内存的Slab 1MB 在进行小对象填充Chunk。

2.      避免大量的重复的初始化和清理,减少内存管理器的负担。

3.      避免频繁malloc/free内存分配导致的碎片

 

      Slab Allocator的缺点

 

      Slab Allocator解决了当初的内存碎片问题,但新的机制也给memcached带来了新的问题。这个问题就是,由于分配的是特定长度的内存,因此无法有效利用分配的内存。例如,将100字节的数据缓存到128字节的chunk中,剩余的28字节就浪费了:

      blob.png

       就是说,如果预先知道客户端发送的数据的公用大小,或者仅缓存大小相同的数据的情况下,只要使用适合数据大小的组的列表,就可以减少浪费。但是很遗憾,现在还不能进行任何调优,只能期待以后的版本了。但是,我们可以调节slab class的大小的差别

      MC内存管理机制小结:

1.      MC的早期内存管理机制为malloc(动态内存分配)

2.      malloc(动态内存分配)产生内存碎片,导致操作系统性能急剧下降。

3.      Slab内存分配机制就产生了,可以解决内存碎片的问题。

4.      Memcached服务的内存预分割成特定长度的内存块,成为Chunk,用户缓存数据的内存空间或内存块,相当于磁盘的Block,只不过磁盘的每一个Block都是相等的,而Chunk只有同一个Slab Class内才是相等的。

5.      Slab Class:特定大小(1M)的包括多个Chunk的集合或者组,一个Memcached包含多个Slab Class,每个Slab Class包含多个相同大小的Chunk。

6.      Slab机制也有缺点,例如,Chunk的空间会浪费(通过调优因子以及大小接近的数据存放入同一MC实例)。

      使用Growth Factor对Slab Allocator内存管理机制调优

           MemCached 默认的调优因子是1.25倍;

 

Memcached的删除机制

      Memcached不会释放已分配的内存空间,在数据过期后,客户端不能通过key取出它的值,其存储空间被重新利用。

Memcached使用的是一种Lazy Expiration策略,自己不会监控存入的key/value对是否过期,而是在获取key值时查看记录的时间戳,检查key/value对空间是否过期。这种策略不会在过期检测上浪费CPU资源。

    Memcached在分配空间时,优先使用已经过期的key/value对空间,当空间占满时,Memcached就会使用LRU算法来分配空间,删除最近最少使用的key/value对,将其空间分配给新的key/value对。在某些情况下,如果不想使用LRU算法,那么可以通过“-M”参数来启动Memcached,这样,Memcached在内存耗尽时,会返回一个报错信息。

指针回收、新的数据过来重新覆盖,指针重新指向。

MemCache指令汇总

    上面说过,已知MemCache的某个节点,直接telnet过去,就可以使用各种命令操作MemCache了,下面看下MemCache有哪几种命令:

命      令

作      用

get

返回Key对应的Value值

add 

添加一个Key值,没有则添加成功并提示STORED,有则失败并提示NOT_STORED

set 

 无条件地设置一个Key值,没有就增加,有就覆盖,操作成功提示STORED

replace 

按照相应的Key值替换数据,如果Key值不存在则会操作失败 

stats

返回MemCache通用统计信息(下面有详细解读)

stats items

返回各个slab中item的数目和最老的item的年龄(最后一次访问距离现在的秒数)

stats slabs

返回MemCache运行期间创建的每个slab的信息(下面有详细解读)

version

返回当前MemCache版本号

flush_all

清空所有键值,但不会删除items,所以此时MemCache依旧占用内存

quit

关闭连接

 

 

stats指令解读

stats是一个比较重要的指令,用于列出当前MemCache服务器的状态,拿一组数据举个例子:

复制代码

STAT pid 1023

STAT uptime 21069937

STAT time 1447235954

STAT version 1.4.5

STAT pointer_size 64

STAT rusage_user 1167.020934

STAT rusage_system 3346.933170

STAT curr_connections 29

STAT total_connections 21

STAT connection_structures 49

STAT cmd_get 49

STAT cmd_set 7458

STAT cmd_flush 0

STAT get_hits 7401

STAT get_misses 57

..(delete、incr、decr、cas的hits和misses数,cas还多一个badval)

STAT auth_cmds 0

STAT auth_errors 0

STAT bytes_read 22026555

STAT bytes_written 8930466

STAT limit_maxbytes 4134304000

STAT accepting_conns 1

STAT listen_disabled_num 0

STAT threads 4

STAT bytes 151255336

STAT current_items 57146

STAT total_items 580656

STAT evicitions 0

复制代码

这些参数反映着MemCache服务器的基本信息,它们的意思是:

参  数    名

作        用

pid

MemCache服务器的进程id 

uptime

服务器已经运行的秒数

time

服务器当前的UNIX时间戳 

version

MemCache版本 

pointer_size

当前操作系统指针大小,反映了操作系统的位数,64意味着MemCache服务器是64位的 

rusage_user

进程的累计用户时间 

rusage_system 

进程的累计系统时间 

curr_connections 

 当前打开着的连接数

total_connections 

 当服务器启动以后曾经打开过的连接数

connection_structures 

服务器分配的连接构造数 

cmd_get 

get命令总请求次数 

cmd_set

set命令总请求次数 

cmd_flush 

flush_all命令总请求次数 

get_hits 

总命中次数,重要,缓存最重要的参数就是缓存命中率,以get_hits / (get_hits + get_misses)表示,比如这个缓存命中率就是99.2% 

get_misses 

总未命中次数 

auth_cmds 

认证命令的处理次数 

auth_errors 

认证失败的处理次数 

bytes_read 

总读取的字节数

bytes_written 

总发送的字节数 

 limit_maxbytes

分配给MemCache的内存大小(单位为字节) 

accepting_conns 

是否已经达到连接的最大值,1表示达到,0表示未达到

listen_disabled_num 

统计当前服务器连接数曾经达到最大连接的次数,这个次数应该为0或者接近于0,如果这个数字不断增长, 就要小心我们的服务了

threads 

当前MemCache总线程数,由于MemCache的线程是基于事件驱动机制的,因此不会一个线程对应一个用户请求 

bytes 

当前服务器存储的items总字节数

current_items 

当前服务器存储的items总数量 

total_items 

自服务器启动以后存储的items总数量 

 

   stats slab指令解读

   如果对上面的MemCache存储机制比较理解了,那么我们来看一下各个slab中的信息,还是拿一组数据举个例子:

复制代码

 1 STAT1:chunk_size 96

 2 …

 3 STAT 2:chunk_size 144

 4 STAT 2:chunks_per_page 7281

 5 STAT 2:total_pages 7

 6 STAT 2:total_chunks 50967

 7 STAT 2:used_chunks 45197

 8 STAT 2:free_chunks 1

 9 STAT 2:free_chunks_end 5769

10 STAT 2:mem_requested 6084638

11 STAT 2:get_hits 48084

12 STAT 2:cmd_set 59588271

13 STAT 2:delete_hits 0

14 STAT 2:incr_hits 0

15 STAT 2:decr_hits 0

16 STAT 2:cas_hits 0

17 STAT 2:cas_badval 0

18 …

19 STAT 3:chunk_size 216

20 …

复制代码

         首先看到,第二个slab的chunk_size(144)/第一个slab的chunk_size(96)=1.5,第三个slab的chunk_size(216)/第二个slab的chunk_size(144)=1.5,可以确定这个MemCache的增长因子是1.5,chunk_size以1.5倍增长。然后解释下字段的含义:

参  数    名

作        用

chunk_size

当前slab每个chunk的大小,单位为字节

chunks_per_page

每个page可以存放的chunk数目,由于每个page固定为1M即1024*1024字节,所以这个值就是(1024*1024/chunk_size)

total_pages

分配给当前slab的page总数

total_chunks

当前slab最多能够存放的chunk数,这个值是total_pages*chunks_per_page

used_chunks

已经被分配给存储对象的chunks数目

free_chunks

曾经被使用过但是因为过期而被回收的chunk数

free_chunks_end

新分配但还没有被使用的chunk数,这个值不为0则说明当前slab从来没有出现过容量不够的时候

mem_requested

当前slab中被请求用来存储数据的内存空间字节总数,(total_chunks*chunk_size)-mem_requested表示有多少内存在当前slab中是被闲置的,这包括未用的slab+使用的slab中浪费的内存

get_hits

当前slab中命中的get请求数

cmd_set

当前slab中接收的所有set命令请求数

delete_hits

当前slab中命中的delete请求数

incr_hits

当前slab中命中的incr请求数

decr_hits

当前slab中命中的decr请求数

cas_hits

当前slab中命中的cas请求数

cas_badval

当前slab中命中但是更新失败的cas请求数

 

发表评论