memcached代码分析详解
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
memcached分析详解
目录
1.文档目的 (1)
1.1.前言 (1)
2.memcached是什么 (2)
2.1. memcached的特征 (2)
3.memcached适合的场合 (4)
4.memcached的代码分析 (5)
4.1. main流程 (5)
4.2. memcached服务流程(TCP) (6)
4.3. memcached状态转换和通信协议处理 (7)
4.4. memcached核心数据结构 (7)
4.5. Slab Allocation机制:整理内存以便重复使用 (8)
5.memcached的使用优化 (10)
5.1. 命中率 (10)
5.2. 空间利用率 (11)
5.3. 加速比 (12)
5.4. 安全性能 (12)
6.memcached的测试分析 (13)
6.1. 读写memcache指令测试 (13)
6.2. 服务端系统负载 (13)
6.3. 空间分配,命中率 (14)
7.memcached的中间层客户端编写 (16)
8.libevent简介 (17)
9.memcached应用 (18)
10.结束语 (20)
1. 文档目的
1.1. 前言
文档就是简单的把memcached做一个代码走读和分析,起到一个抛砖引玉的作用;
目的就是让大家在使用memcached这个工具时,多一些对工具的了解,从而确定你的程序是否真的需要用memcached来实现不可;
短短2个小时也讲不了多少,主要是做一个学习探讨,如果大家感兴趣的话后期可以再做培训
牛人真多啊,向先行者致敬!
2. memcached是什么
memcached广泛应用在大负载高并发的网站上,是一种非常成熟的产品(称为一项技术也未尝不可)。
像facebook,youtube,yahoo,sina,sohu,netease,豆瓣等网站均或多或少使用了该项产品。
memcached在以用户为中心的网站上,表现尤其突出,例如sns,blog等web2.0应用的站点。
这些站点一般来讲,特别注重用户体验,用户对服务器的响应速度要求很高,用户数据相对比较复杂、关连度比较高,需要经常对数据库进行更新和检索。
许多Web应用都将数据保存到RDBMS中,应用服务器从中读取数据并在浏览器中显示。
但随着数据量的增大、访问的集中,就会出现RDBMS的负担加重、数据库响应恶化、网站显示延迟等重大影响。
这时就该memcached大显身手了。
memcached是高性能的分布式内存缓存服务器。
一般使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。
2.1. memcached的特征
1)memcached的服务器客户端通信并不使用复杂的XML等格式,而使用简单的基于文本行的协议。
因此,通过telnet 也能在memcached上保存数据、取得数据。
下面是例子。
$ telnet localhost 8119
Trying 127.0.0.1...
Connected to localhost.localdomain (127.0.0.1).
Escape character is '^]'.
set foo 0 0 3 (保存命令)
bar (数据)
STORED (结果)
get foo (取得命令)
V ALUE foo 0 3 (数据)
bar (数据)
协议可以参考:
2)为了提高性能,memcached中保存的数据都存储在memcached内置的内存存储空间中。
由于数据仅存在于内存中,因此重启memcached、重启操作系统会导致全部数据消失。
另外,内容容量达到指定值之后,就基于LRU(Least Recently Used)算法自动删除不使用的缓存。
memcached本身是为缓存而设计的服务器,因此并没有过多考虑数据的永久性问题。
3)memcached尽管是“分布式”缓存服务器,但服务器端并没有分布式功能。
各个memcached不会互相通信以共享信息。
那么,怎样进行分布式呢?这完全取决于客户端的实现。
3. memcached适合的场合
memcached是“分布式”的内存对象缓存系统,那么就是说,那些不需要“分布”的,不需要共享的,或者干脆规模小到只有一台服务器的应用,memcached不会带来任何好处,相反还会拖慢系统效率,因为网络连接同样需要资源,即使是UNIX/Windows本地连接也一样。
测试数据显示,memcached本地读写速度要比直接.NET内存数组慢几十倍,而APC、共享内存方式都和直接数组差不多。
可见,如果只是本地级缓存,使用memcached是非常不划算的。
Memcached在很多时候都是作为数据库前端cache使用的。
因为它比数据库少了很多SQL解析、磁盘操作等开销,而且它是使用内存来管理数据的,所以它可以提供比直接读取数据库更好的性能,在大型系统中,访问同样的数据是很频繁的,memcached可以大大降低数据库压力,使系统执行效率提升。
另外,memcached 也经常作为服务器之间数据共享的存储媒介,例如在SSO系统中保存系统单点登陆状态的数据就可以保存在memcached中,被多个应用共享。
需要注意的是,memcached使用内存管理数据,所以它是易失的,当服务器重启,或者memcached进程中止,数据便会丢失,所以 memcached不能用来持久保存数据。
很多人的错误理解,memcached的性能非常好,好到了内存和硬盘的对比程度,其实memcached使用内存并不会得到成百上千的读写速度提高,它的实际瓶颈在于网络连接,它和使用磁盘的数据库系统相比,好处在于它本身非常“轻”,因为没有过多的开销和直接的读写方式,它可以轻松应付非常大的数据交换量,所以经常会出现两条千兆网络带宽都满负荷了,memcached进程本身并不占用多少CPU资源的情况。
4. memcached的代码分析4.1. main流程
4.2. memcached服务流程(TCP)
4.3. memcached状态转换和通信协议处理
需要说明的是,这里需要排除所有出错处理.很显然,不管是哪种操作下,一旦出错,信息需要通过conn_write状态往client写入出错信息的,那么在string_out时,必定转入conn_write状态.
而且,很多细节也没有在流程图中给出,如统计信息的处理,超时后get操作时删除等等.对于在memcache协议中定义的其他操作,如stats,version,quit,flush_all,own,disown等等由于使用很少,在流程中没有详细给出,可以查看源代码.
4.4. memcached核心数据结构
1. item结构
item是存储在memcache的key-value对的抽象.由于组织item存放是按照LRU算法组织的.那么在其中有几个成员在修改源代码时必须注意,time是最近访问时间.exptime是item消亡时间.item是一个双向列表.同时还挂在一个Hash table上.
2. conn结构
conn结构是联系上下文的关键对象.对于每个连接的到来,都有一个conn结构与其对应,并且对应到某个连接状态,进入状态转换而完成操作.
conn在程序开始也进行了一次预分配,分配200个连接空间.当200个使用完之后便是按需分配,到达一个分配一个.
conn和item,iovec(内核级别缓冲结构)关联.
3. slabclass_t结构
slabclass_t保存了分级大小的空间槽,以分别适用于不同大小的item存放.取决于两个命令行参数,-f和-n.在应用 slabclass_t时,定义的是一个数组,该数组长度取决于增长的指数级别和初始值大小(32+chunk_size),每个空间槽是不允许大于1M 的,也就是1048576.
4. settings结构
系统获取的命令行参数保存的地方.
5. stats结构:
统计信息保存地方,可以考虑对其进行操作以适应不同的统计信息处理,如获取某个时间段的get 命中率等等操作.
4.5. Slab Allocation机制:整理内存以便重复使用
1)memcached默认情况下采用了名为Slab Allocator的机制分配、管理内存。
在该机制出现以前,内存的分配是通过对所有记录简单地进行malloc和free来进行的。
但是,这种方式会导致内存碎片,加重操作系统内存管理器的负担,最坏的情况下,会导致操作系统比memcached进程本身还慢。
Slab Allocation的原理相当简单。
将分配的内存分割成各种尺寸的块(chunk),并把尺寸相同的块分成组(chunk的集合)(图1)。
2)memcached根据收到的数据的大小,选择最适合数据大小的slab(图2)。
memcached中保存着slab内空闲chunk的列表,根据该列表选择chunk,然后将数据缓存于其中。
3)Slab Allocator解决了当初的内存碎片问题,但新的机制也给memcached带来了新的问题。
这个问题就是,由于分配的是特定长度的内存,因此无法有效利用分配的内存。
例如,将100字节的数据缓存到128字节的chunk中,剩余的28字节就浪费了(图3)。
4)memcached在启动时指定Growth Factor因子(通过-f选项),就可以在某种程度上控制slab之间的差异。
默认值为1.25。
但是,在该选项出现之前,这个因子曾经固定为2,称为“powers of 2”策略。
将memcached引入产品,或是直接使用默认值进行部署时,最好是重新计算一下数据的预期平均长度,调整growth factor,以获得最恰当的设置。
内存是珍贵的资源,浪费就太可惜了。
5. memcached的使用优化
在优化memcache的工作之前,需要了解memcache体系的工作流程.一个分布式的memcache的运作是需要三个部分的,多台提供 memcache服务的servers(简称S),一个进行分布式映射的中间层lib(其实这个也可以当作客户端的一部分,简称L),和进行 memcache请求的客户端(简称C).
在memcache工作时,有以下四个步骤:
1. C通过带有特性化的Key值的命令串,向L请求memcache服务,L将命令串进行分解,并通过对key的某种Hash算法决定S的地址
2. L将分解的(Comm Key-Value)或者(Comm Key)向相关的S请求memcache服务.
3. 相关的S根据memcache协议向L返回服务结果.
4. L将结果进行聚集包装后返回给C一个人性化的响应.
从上面的分析可以看出,分布式的memcache服务是需要很大的网络开销的.对于一般的应用而言,是否都要进行memcache的优化,甚至是否需要用到memcache,都需要进行权衡分析.如果只是本地小规模的应用,在数据库可承受的范围内,是宁愿采用数据库+文件缓存的方式.1.1版本的 memcache走TCP模式在本地应用的处理速度甚至比不上Mysql数据的Unix域套接口的处理速度的一半,当然会更加比不上在程序内部直接操作内存了.虽然1.2版本的memcache已经提供了-s参数指定Unix域套口和-u指定udp模式.而且如果不需要用到分布式的话,不推荐使用 memcache,除非你的内存足够大到浪费的程度.
优化可以从以下几个方面进行:
5.1. 命中率
对于缓存服务而言,命中率是至关重要的.命中率的提升可以通过多种方案实现.其一,提高服务获取的内存总
量.这无疑是增加命中的最直接的办法,将缓存数据完全放入数据池中.只要连接不失效,就一定命中.其二,提高空间利用率,这实际上也是另一种方式的增加内存总量.具体实现在下一个方面给出.其三,对于一些很特别的memcache应用,可以采用多个memcache服务进行侦听,分开处理,针对服务提供的频繁度划分服务内存,相当于在应用一级别上再来一次LRU.其四,对于整体命中率,可以采取有效的冗余策略,减少分布式服务时某个server发生服务抖动的情况.如,14台机器实现分布式memcache,划分两组服务,其中一组13台做一个分布式的memcache,一组1台做整个的memcache备份.对于update操作,需要进行两边,get操作只需要一遍,一旦访问失效,则访问备份服务器.这样,对于备份服务器需要内存比较大,而且只适应于读操作大于写操作的应用中.这可以认为是RAID3,当然,也可以采用RAID1完全镜像.
5.2. 空间利用率
对于使用memcache做定长数据缓存服务而言,是可以在空间利用率上进行优化.甚至最简单的办法可以不
用更改memcache的源码遍可以完成由 -f和-n参数的配合可以做到定长优化,不过极可能需要浪费掉预分配的199M内存空间.当然前提是memcache的版本是1.2,同时如果使用的是 1.2.0和1.2.1的话,需要更改掉一个BUG,那就是getopt时将opt串中最后一个”s”改成”n”,希望memcache能在以后的版本发现这个BUG.例如,如果key是一个定长id(如一个8位的流水号00000001),value是一个定长的串(如16位的任意字符串),对应于一个 chunk_size可以这么计算:chunk_size = sizeof(item) + nkey + *nsuffix + nbytes = 32 + 9 + (flag的数位长度 )2+ (16)的数位长度) 2+(两换行的长度)4 + 17 = 40 + 10 + 16 = 66,那么可以通过 -f 1.000001 -n `expr 66 - 32`,即 -f 1.000001 -n 34 来启动memcache.这种情况下,会浪费掉memcache预先分配的200M空间中的199M.从第2个预分配等级到第200个预分配等级将不会用到.然而,存在解决办法,那就是在编译memcache是加入编译参数-DDONT_PREALLOC_SLABS,或者在源代码中加
入#define DONT_PREALLOC_SLABS即可,只是会去除memcache的预分配内存机制.
如果需要memcache的预分配内存机制,那么需要对其源代码进行修改.修改如下:
引用
1. 在slabs.c中,将函数slabs_clsid改成:
unsigned int slabs_clsid(size_t size)
{ unsigned int res = POWER_SMALLEST;
if(size==0)
return 0;
res = (size)%power_largest;
return res;
}
2. 在item.c中,将函数 item_make_header改为:
int item_make_header(char *key, uint8_t nkey, int flags, int nbytes,
char *suffix, int *nsuffix)
{
*nsuffix = sprintf(suffix, " %u %u\r\n", flags, nbytes - 2);
return sizeof(item)+ nkey + *nsuffix + nbytes + hash(key,nkey,0);
}
3. 在item.c中,将函数 item_free改为:
void item_free(item *it)
{ unsigned int ntotal = it->slabs_clsid;
assert((it->it_flags & ITEM_LINKED) == 0);
assert(it != heads[it->slabs_clsid]);
assert(it != tails[it->slabs_clsid]);
assert(it->refcount == 0);
it->slabs_clsid = 0;
it->it_flags |= ITEM_SLABBED;
slabs_free(it, ntotal);
}
做一个轮流存储的机制使用预分配的内存,这样的好处是其他地方不需要做任何修改就可以了,当然你可以在源代码中加入上面的代码,并将它们放在一个自定义的宏后面.
5.3. 加速比
加速比,也即事件的处理效率.是否可以修改libevent的事件处理效率,需要研究.如果内存空间很大,可以将freeconn的数值调大,增加预分配的conn内存大小.
5.4. 安全性能
memcache还存在一个比较显著的问题,那就是其安全性能.只要了解memcache监听的端口,对于能够使用分布式memcache进行数据通信的网络环境的机器,都可以通过memcache协议于memcache服务器进行通信,获取或种植数据.不能保证种植进内存里的数据不会被别有用心的人再利用.也不能保证服务器的内存不被漫天遍地的垃圾数据所堆积,造成命中极低.
memcache的设计理念在一个轻字,如果对每次Client的通讯需要校验身份,那么恐怕memcache也就达不到其想要的效果了.存在解决办法缓解这个问题,一般而言,需要使用memcache服务的机器,可以在Server维持一张红色列表.这张表上的机器便可以获取服务.很显然,memcache并非任意Client都能访问,只有信任的机器访问,那么为什么不将这些信任的机器放在一个/etc/mem_passwd下呢.
还有,memcached走udp时,很大几率接受到upd时,都会使服务死掉,特别是set,add,replace时,这个问题需要去考究一下.不过没有时间了.
6. memcached的测试分析
服务器端memcache在命令行运行的参数:
memcached –d –m 512 –l *.*.*.* -u ** -f 1.00001 –n 16 –c 10000 –vv
6.1. 读写memcache指令测试
6.2. 服务端系统负载
通过自己编写的服务器端,对单结点的memcache进行了连接压力测试.其中测试用例的编写是这样的:启用七个客户端,每个客户端串行运行1000个进程,每个进程开3000线程,每个线程执行10次memcache的读操作或者写操作(操作相同).客户端并发连接.
1. 客户端(7)的环境:Intel(R) Xeon(R) CPU 5120 @ 1.86GHz,4G memory.
2. 服务器端(1)的环境:Intel(R) Xeon(R) CPU 5120 @ 1.86GHz,4G memory.
3. 网络环境:100M网卡,Cisco交换机.
很显然,memcache的运行在系统cpu的消耗上占十分少的比重,即便是很恐怖的并发连接也不会给系统带来多大的负载,因为其磁盘IO free(所有操作都在内存中)和相应的内存分配机制决定其占用cpu的极少,而相反,在网络IO上却花费很大的时间.
6.3. 空间分配,命中率
由于本地测试的get数据非常固定,因此命中率基本为100%.在10.68.1.31上运行了一个有前端应用的memcachce服务器,运行时间已经有364个多小时了.
因此通过10.68.1.31上的数据说明(版本为1.1.13).通过memcache的统计协议可以清楚的看到其
命中率高达95.9%
7. memcached的中间层客户端编写省略
8. libevent简介
libevent是一个事件触发的网络库,适用于windows,linux,bsd等多种平台,内部使用iopc/epoll/kqueue 等系统调用管理事件机制,而且根据libevent官方网站上公布的数据统计,似乎也有着非凡的性能.
从代码中看,libevent支持用户使用三种类型的事件,分别是网络IO,定时器,信号三种,在定时器的实现上使用了红黑树(RB tree)的数据结构,以达到高效查找,排序,删除定时器的目的,网络IO 上,libevent的epoll居然用的EPOLLLT水平触发的方式,不容易出错,但是在效率上可能比EPOLLET要低一些.跟网络无关的,libevent也有一些缓冲区管理的函数,libevent没有提供缓存的函数.而且libevent 的接口形式非常值得参考.
9. memcached应用
原文:Scaling memcached at Facebook
作者:Paul Saab
翻译:ShiningRay
如果你翻阅过一些关于大型网站扩展(Scaling)的资料,那么你可能听说过一个叫memcached的东西。
memcached是一个高性能、分布式的内存对象缓存系统。
我们Facebook可能是世界上最大的memcached 用户了。
我们利用memcached来减轻数据库的负担。
memcached确实很快,但是我们还要让他更快、更高效。
我们使用了超过800台服务器,提供超过28TB的内存来服务于用户。
在过去的一年里,随着 Facebook 的用户量直线上升,我们遇到了一系列的扩展问题。
日益增长的需求使得我们必须对操作系统和memcached 进行一些修改,以获得足够的性能来为我们的用户提供最好的体验。
因为我们有好几千台机器,每个都运行了几百个Apache进程甚至更多,最终导致到memcached进程的TCP 链接有几十万个。
这些链接本身并不是什么大问题,但是memcached为每个TCP链接分配内存的方法却很成问题。
memcached为每个链接使用单独的缓存进行数据的读写。
当达到几十万链接的时候,这些累计起来达好几个G——这些内存其实可以更好地用于存储用户数据。
为了收复这些内存,我们实现了一个针对TCP和UDP套接字的每线程共享的链接缓存池。
这个改变使每个服务器可以收回几个G的内存。
虽然TCP上我们改进了内存的使用效率,但我们还是转向了UDP,目的是让get(获取)操作能降低网络流量、让multi-get(同时并行地获取几百个键值)能实现应用程序级别的流量控制。
我们发现Linux上到了一定负载之后,UDP的性能下降地很厉害。
这是由于,当从多个线程通过单个套接字传递数据时,在UDP 套接字锁上产生的大量锁竞争导致的。
要通过分离锁来修复内核恐怕不太容易。
所以,我们使用了分离的UDP套接字来传递回复(每个线程用一个答复套接字)。
这样改动之后,我们就可以部署UDP同时后端性能不打折。
另一个Linux中的问题是到了一定负载后,某个核心可能因进行网络软终端处理会饱和而限制了网络IO。
在Linux中,网络中断只会总是传递给某个核心,因此所有的接受软终端的网络处理都发生在该内核上。
另外,我们还发现某些网卡有过高的中断频率。
我们通过引入网络接口的“投机”轮询解决了这两个问题。
在该模型中,我们组合了中断驱动和轮询驱动的网络IO。
一旦进入网络驱动(通常是传输一个数据包时)以及在进程调度器的空闲循环的时候,对网络接口进行轮询。
另外,我们也用到了中断(来控制延迟),不过网络中断用到的数量大大减少(一般通过大幅度提升中断联结阈值interrupt coalescing thresholds)。
由于我们在每个核心上进行网络传输,同时由于在调度器的空闲循环中对网络IO进行轮询,我们将网络处理均匀地分散到每个核心上。
最后,当开始部署8核机器的时候,我们在测试中发现了新的瓶颈。
首先,memcached的stat工具集依赖于一个全局锁。
这在4核上已经很令人讨厌了,在8核上,这个锁可以占用20-30%的CPU使用率。
我们通过将stats工具集移入每个线程,并且需要的时候将结果聚合起来。
其次,我们发现随着传递UDP数据包的线程数量的增加,性能却在降低。
最后在保护每个网络设备的传送队列的锁上发现了严重的争用。
数据包是由设备驱动进行入队传输和出队。
该队列由Linux的“netdevice”层来管理,它位于IP和设备驱动之间。
每次只能有一个数据包加入或移出队列,这造成了严重的争用。
我们当中的一位工程师修改了出队算法,实现了传输的批量出队,去掉了队列锁,然后批量传送数据包。
这个更正将请求锁的开销平摊到了多个数据包,显著地减少了锁争用,这样我们就能在8核系统上将memcached伸展至8线程。
做了这些修改之后,我们可以将memcached提升到每秒处理20万个UDP请求,平均延迟降低为173微秒。
可以达到的总吞吐量为30万UDP请求 /s,不过在这个请求速度上的延迟太高,因此在我们的系统中用处不大。
对于普通版本的Linux和memcached上的50,000 UDP请求/s而言,这是个了不起的提升。
我们希望尽快将我们的修改集成到官方的memcached仓库中去,我们决定在这之前,先将我们对memcached
的修改发布到github上。
10.结束语
重新审视现在的体系
memcached支持外部存储的难点是,网络和事件处理相关的代码(核心服务器)与内存存储的代码紧密关联。
这种现象也称为tightly coupled(紧密耦合)。
必须将内存存储的代码从核心服务器中独立出来,才能灵活地支持外部引擎。
因此,基于我们设计的API,memcached被重构成下面的样子:
重构之后,我们与1.2.5版、二进制协议支持版等进行了性能对比,证实了它不会造成性能影响。
在考虑如何支持外部引擎加载时,让memcached进行并行控制(concurrency control)的方案是最为容易的,但是对于引擎而言,并行控制正是性能的真谛,因此我们采用了将多线程支持完全交给引擎的设计方案。
以后的改进,会使得memcached的应用范围更为广泛。