浅谈Memcache++Client
Author: FreeKnightDuzhi
Memcached 是一个高性能的分布式的内存对象缓存系统,用于动态Web应用以缓冲数据库负载,减少数据库访问次数,提升访问速度。
1:Memcached 是什么?
Memcached 是一个高性能的分布式的内存对象缓存系统,用于动态Web应用以缓冲数据库负载,减少数据库访问次数,提升访问速度。 它通过在内存里维护一个巨大的统一的Hash表来存储各种格式的数据,包括文件,数据库检索结果,甚至图像视频等。 Memcached使用了LibEvent(Linux下使用epoll)来均衡任何数量的链接,使用非阻塞网络I/O,对内部对象使用了引用计数,并使用自己的内存页块分配器和哈希表,所以其虚拟内存不会产生碎片且内存分配的时间复杂度为O(1)。 Memcached相比共享内存,特点是可以让不同主机的多个用户同时访问,而共享内存只能单机应用。 Memcached相比数据库,特点是解决了数据库的磁盘开销,SQL解析开销和阻塞带来的麻烦。 注意的是,Memcached自身没有认证和安全机制,意味着Memcached服务器需要放置在防火墙之后。 Memcached服务器下载点:http://memcached.org/ 如果你需要一个轻量级的,类型安全的,简单易用的Memcache客户端,请从 http://sourceforge.net/projects/memcachepp/ 下载,它是使用C++编写的Memcache客户端。
2:Memcached 特点?
1> 基于LibEvent事件处理。即使对服务器的连接数增加,也是O(1)性能。 2> 内置内存存储方式。自己的内存分页管理,减少碎片。基于LRU方式管理缓存,更为高效。自己的Hash算法。 3> 客户端独立,服务器独立。客户端之间没有通讯,完全独立。服务器之间也没有通讯,完全独立。 4> 跨平台。支持Linux,FreeBSD, MacOS,Windows
3:简介Memcache++
这个项目是开源的,它开发了大约一年,现被多个项目中使用。 但请注意,该项目使用Boost库,所以其中受到Boost的License额外限制。请参考 http://www.boost.org/LICENSE_1_0.txt
4:作者简述
作者Deam Michael Berris是一个从事多年C++软件开发的工程师,在2007年2月到2009年6月期间,他在 Friendster( http://www.friendster.com/ ) 编写的该项目。当时他在菲律宾电信系统从事软件开发工作。
5:项目概述
编译Memcache++客户端必须Boost C++库支持。 Memcache++客户端项目编译平台为Linux,编译环境为GCC。 该项目在 Linux(32bit 和 64bit ) GCC4.1.2 GCC4.3.3 均测试通过。 该项目在Windows( 32bit 和 64bit )Ms VC++ 2008 上测试通过。 该项目是一个只有头文件的Lib,意味着它的所有实现均在头文件中。 该项目没有过于特殊的编译要求和配置单,但有以下要求: 1>在你的项目中,除包含本项目头之外,必须包含以下Boost头:Boost.Asio, Boost.Date_Time, Boost.Fusion, Boost.Regex, Boost.Serialization, Boost.Spirit, Boost.System, Boost.Thread 2>若希望该项目为线程安全模式,必须定义 _REENTRANT 宏。这个宏是受到 Boost.Thread 内限制的。
6:简易演示Demo
下面是个简单的Demo,告诉我们如何去使用Memcache++Client去连接Memcached服务器,并从服务器获取数据,删除数据,设置数据的操作。
#include <memcachepp/memcache.hpp> // 该头文件是Memcache++Client唯一对外需要包含的头文件。
#include <string>
#include <iostream>
int main( int argc, char* argv[] )
{
// Memcache服务器句柄
memcache::handle mc;
// Memcache服务器默认开启11211端口。下面连接服务器
mc << memcache::server("localhost", 11211 ) << memcache::connect;
// 首先删除Memcache服务器的指定Key的Node
try
{
mc << memcache::delete_("FKKey");
}
catch( ... ) { }
std::string szMyString("FreeKnightValue");
std::string szCachedMsg;
try
{
mc << memcache::set("FKKey", szMyString ) // 设置Memcache服务器内FKKey的Value
<< memcache::get("FKKey", szCachedMsg ) // 然后从Memcache服务器内取出FKKey的Value
<< memcache::delete_("FKKey"); // 删除Memcache服务器内FKKey所在Node
std::cout << szCachedMsg << std::endl;
}
catch( std::runtimer_error & e )
{
std::cerr << "Unknown error: " << e.what();
}
return 0;
}
我们可以看出在使用Memcache++时,很类型标准C++流操作,当然,在早期版本,我们还有一种方式如下
using namespace memcache::fluent; key( mc, “FKKey” ) = szMyString; warp( szCachedMsg ) = get( mc, “FKKey” ); remove( mc, “FKKey” );
这种方式完全等同于上面的
mc << memcache::set(“FKKey”, szMyString ) << memcache::get(“FKKey”, szCachedMsg ) << memcache::delete_(“FKKey”);
现在的Memcache版本可以支持任意一种,我们可以根据自己习惯自由选择。
7:核心对象解释
Memcache++Client中,memcache::handle是非常重要的对外对象,它是NonCopyable(禁止拷贝构造)的。
它有如下核心成员
connection_ptr
server_info
pool_info
server_container
pool_container
它有如下核心函数
void add_server( serverHostName, serverInfo ); // 增加一个或多个Memcache服务器连接
void add_pool( serverPoolName, serverInfo ); // 增加一个服务器池连接
void connect(); // 连接所有的Memcache服务器(在addServer的参数中被定义)
string version( sizet ); // 获取Memcache服务器版本
void delete( size_t, Key, time ); // 删除一个Key的Node(参数Time意义请看下文具体补充)
void get
Memcache++Client中,”命令“是个很重要的概念。 Demo中的 memcache::set ,memcache::get 均是”命令“。每个命令均存在的函数有以下三个: HandleInstance& HandleInstance << Directive; // 将一个命令填充到Handle内。 D( Directive ); D< T1, T2 …. Tn >( t1, t2 … tn ); // 对多个命令进行构造。 void Directive( HandleInstance ); // 对一个Handle执行指定命令。 我们可以定义自己的命令,但是强制要求实现三个函数,一个构造函数一个是operator << 和operator ().我们看一下Memcache++Client里的get_directive命令源代码: template< typename T > struct get_directive { explicit get_directive( std::string const & key, T & holder ) : _key( key ), _holder( holder ){}; template< typename T > void operator() ( T & handle ) const { size_t pools = handle.pool_count(); assert( pools != 0 ); handle.get( handle.hash( _key, pools ), _key, _holder ); } private: mutable std::string _key; mutable T & holder; } Memcache++Client中,默认命令有如下: get,set,add,replace,delete,raw_get,raw_set,raw_append,raw_prepend,incr,decr,server,pool,connet. 每个C++流式命令对应的均有一个fluent格式的函数调用。 例如: mc << memcache::set( “key”, value ); 完全等同于 key( mc, “key”) = value; mc << memcache::get( “key”, value ); 完全等同于 wrap( value ) = get( mc, “key” ); 其中后面的调用被称为 fluent 格式函数调用。这些API的详细描述这里不再给出,若有不清晰,可直接邮件联系 这里只给出核心的命令 connect: 打开一个到Memcache的连接 pconnect: 打开一个到Memcache的长连接 close: 关闭一个到Memcache的连接 set,get,repalce: 保存,提取,替换 一个Memcache服务器的数据 delete: 删除一个Memcache服务器的数据 getStats: 获取当前Memcache服务器运行状态
Memcache++Client中,Handle中保存了三种策略。 其中包括:ThreadPolicy线程策略 , DateInterchangePolicy数据交换策略,HashPolicy哈希策略。 其中线程策略,使我们可以定义Handle在如何协调线程的合作,默认状态下线程策略是使用一个嵌套的ScopedLock,而且也未必是单线程,这取决于_REENTRANT宏对Boost.Thread的控制。 其中数据交换策略,默认为binary_interchange策略,我们可以修正为以下三种 binary_interchange, text_interchange, string_interchange .
8:某志补充
通常一个Memcached服务器进程一个会占用2GB内存空间。 虽然Memcached服务器是网络通讯,理论上是可以支持无限多链接,但和Apache不同之处是,它更多时候面对的稳定的连接,根据Linux线程连接并发能力考虑,保守认为Memcached最大同时连接数250左右,但已足够常规游戏服务器组使用了。 Memcached是自己的内存管理方式,和APC不同,没有基于共享内存和MMAP,所以也就没有共享内存的限制,两者是两回事。 Memcached服务器是使用预分配内存的方式,其内存分配最小单位为Slab。其中Slab可以理解为一个内存块,大小为1048576字节,正好是1MB,所以Memcached始终是整MB的使用内存。一个Slab内会分配若干个Chunk,每个Chunk会有一个Item(内部包含Item结构体以及Key,Value).Slab会有自己的ID,又会被挂接在SlabClass数据中。 Memcached服务器对内存要求相对略高,对CPU基本不做要求,在硬件配置时可考虑该点。 辅助监视Memcached可以使用Nagios开源监视软件。
9:细说MemCached 内存存储机制
MemCached是使用SlabAllocator进行内存管理的。它是将原本预先分配的内存,分割为各种不同尺寸长度的块,并且将相同长度的内存块分成组进行管理。 这里我们需要解释几个名词概念: 1:Page 是一次分配给Slab的内存空间,如我总结所说,其大小默认为1MB。之后这1MB会交由SlabAllocator的需要切割为大小不同的chunk。 2:Chunk 是实际的内存空间,大小未必一致。 3:SlabClass 是对Chunk进行管理的,每一个SlabClass内的Chunk大小是完全一致的。
当客户端或者数据库要求添加一条数据时,Memcached根据收到的数据大小(假设为100bytes)其寻则最合适数据大小的SlabChunk(假设为112bytes的SlabClass里),填充其中,并通知所属Slab,这个chunk已被占用,由Slab维护一个空闲chunk列表。
但是值得注意的是,内存的Chunk分割是在MemCached启动时便进行的,是固定长度,那么我们就如上例可知,假设Slab分割Chunk有以下几种规格的大小:88bytes, 112bytes, 144bytes, 184bytes, 512bytes … 那么我们100bytes的数据写入时,只能填充到112bytes的Chunk内,即浪费了12bytes。 若我们需要存储的对象大小均为600bytes以上,则浪费了大量的小Chunk。此时我们可以用 grown factor 进行调节。 默认时这个值为1.25。我们通过verbose查看$ memcached -f 1.25 -vv 可得知如下结果 slab class 1: chunk size 88 bytes perslab 11915 slab class 2: chunk size 112 bytes perslab 9362 slab class 3: chunk size 144 bytes perslab 7281 …. slab class 10: chunk size 744 bytes perslab 1409 …. 可知每个slab class内的chunk大小是1.25倍递增的(个别时候有误差,是为了字节对齐设置的)。 我们同样可以设置为2倍。$memcached -f 2 -vv 可得知如下结果 slab class 1: chunk size 128 bytes perslab 8192 slab class 2: chunk size 256 bytes perslab 4096 ….. slab class 10: chunk size 65536 bytes perslab 16 slab class 11: chunk size 131072 bytes perslab 8 ….. 当然这个状态可以通过Perl脚本进行监察,得知 Chunk大小,LRU内最旧的记录生存时间,分配给Slab的页数,Slab内的记录数等信息。
Memcached是内存缓存机制,没有对磁盘操作,所以,其大小受到内存制约,在优化内存使用方面,Memcached有以下手段: 1> 不释放内存。Memcached是不会释放内存的,当我们delete_命令后,服务器只会将指定记录设置为不可见,之后进行重复使用,而不会真正的free归还内存,避免了内存碎片问题。 2> 惰性释放。Memcached内部不会频繁的刷新以删除过期记录,而是在get时检查记录时间戳,这是被动的检查,所以Memcached不会在监视刷新上消耗CPU时间。 3> 使用LRU删除记录。LRU全称为Least Recently Used,就意味着,当Memcached内存不足时(从slabClass里获取未使用空间失败时),就会从最近未被使用的记录中进行搜索,查找使用最近最少使用的记录进行删除标志,重复利用内存。
10:MemCached通讯数据交换机制
MemCached当前更多的是使用binary_interchange协议,它是由16字节的包头和Key以及不定长数据组成的。包头内包含了标准的Magic字节,命令种类,键长,值长等信息。 二进制协议解决了文本协议可能出现的平台问题,以及XML等格式带来的复杂度。
11:MemCached的分布式
我们上面了解了许多,却无法发现MemCached的分布式在哪儿,而其实Memcached的分布式不存在于服务器,而是完全由客户端实现的。 假设我们要保存三个Key。分别为”Free”“Knight”“Duzhi”,另有Memcached服务器两台。 我们Memcached客户端会通过一个算法,将Key分类,假设我们设定Key第一个字母在’G’之前的数据保存在第一个Memcached服务器上,’G’之后的数据保存在第二个Memcached服务器上。 在get,set,replace等操作时,也先在算法这一层做出服务器责任分割,则可以实现Memcached的分布式。 这里,就是Memcache++Client替我们去做到的事情。
常规的余数Hash是最容易想到的,它会根据Memcached服务器个数和键的整数哈希值范围进行分布,用键的哈希值除以服务器台数得到的余数,就是所选择的服务器编号。这样是相对均衡合理的。 但这种方式的一大问题就是,当服务器个数发生更变时,代价相当昂贵。余数的变更将改变全部数据的服务器分配。此时,我们有个更好的解决方法就是ConsistentHash.
ConsistentHash的基本概念就是 首先求出Memcached服务器节点的哈希值,将其配置到0-2^32的一个圆上。 然后用同样的算法求出Key的哈希值,并映射到圆上,然后从Key哈希映射的位置开始顺时针查找,将数据保存到第一个找到的服务器节点上。 若超过了2^32仍然找不到服务器,就将其保存在第一台Memcached服务器上。 当我们增加一个Memcached服务器节点后,ConsistentHash内只有部分数据存储受到影响,而这些数据是 上一个Memcached服务器节点顺时针到新添加的Memcached服务器节点之间的KeyHash映射数据。 但是这样的话,ConsistenHash可以看的出来,在添加Memcached服务器时,它很大限度上抑制了键的重新分布,但它的哈希分布更可能没有余数法均匀,此时,有的ConsistentHash使用了虚拟节点方法,即,每个Memcached服务器视为多个虚拟的节点,在圆上分配多个点交由其负责,则更大限度的抑制分布不均匀。