memcached client - memcacheddotnet (Memcached.ClientLibrary) 1.1.5

.NET memcached client library - 1.1.5
Project Home: http://sourceforge.net/projects/memcacheddotnet/
主要从java版本import过来
主要特性:socket相关的配置比较丰富,对socket的管理处理得比较细致;支持可配置的load balance;支持数据压缩;有考虑failover问题
缺陷:不支持cas操作;不支持consistent hashing;基本停止了更新,没有与java版本同步;从java import过来时就有一些java版本上的功能没有实现,例如对socket启用、禁用Nagle算法,socket读取时的timeout等

大致看了下java版本的代码,已经支持consistent hashing,连接池的管理、节点状态的管理等方面修改了不少东西,但还是不支持cas操作

Examples
用于测试的基本代码
private  SockIOPool _pool;

private   void  Setup()
{
    String[] serverlist 
=  {  " 127.0.0.1:11211 "  };
    
this ._pool  =  SockIOPool.GetInstance( " default " );
    
this ._pool.SetServers(serverlist);  // 设置服务器列表
    
// 各服务器之间负载均衡的设置
     this ._pool.SetWeights( new   int [] {  1  });
    
// socket pool设置
     this ._pool.InitConnections  =   5 // 初始化时创建的连接数
     this ._pool.MinConnections  =   5 // 最小连接数
     this ._pool.MaxConnections  =   250 // 最大连接数
    
// 连接的最大空闲时间,下面设置为6个小时(单位ms),超过这个设置时间,连接会被释放掉
     this ._pool.MaxIdle  =   1000   *   60   *   60   *   6 ;
    
// 通讯的超时时间,下面设置为3秒(单位ms),.NET版本没有实现
     this ._pool.SocketTimeout  =   1000   *   3 ;
    
// socket连接的超时时间,下面设置表示连接不超时,即一直保持连接状态
     this ._pool.SocketConnectTimeout  =   0 ;
    
this ._pool.Nagle  =   false // 是否对TCP/IP通讯使用Nalgle算法,.NET版本没有实现
    
// 维护线程的间隔激活时间,下面设置为60秒(单位s),设置为0表示不启用维护线程
     this ._pool.MaintenanceSleep  =   60 ;
    
// socket单次任务的最大时间,超过这个时间socket会被强行中断掉(当前任务失败)
     this ._pool.MaxBusy  =   1000   *   10 ;
    
this ._pool.Initialize();
}
private   void  Shutdown()
{
    
this ._pool.Shutdown();
}
private  MemcachedClient GetClient()
{
    MemcachedClient client 
=   new  MemcachedClient();
    client.PoolName 
=   " default " ;
    
return  client;
}

public   enum  UserGender
{
    Male 
=   1 ,
    Female 
=   2 ,
    Unspecified 
=   0 ,
}
[Serializable]
public   class  User
{
    
public   int  ID {  get set ; }
    
public   string  Name {  get set ; }
    
public  DateTime Birthday {  get set ; }
    
public  UserGender Gender {  get set ; }
    
public   override   string  ToString()
    {
        
return   new  StringBuilder()
            .Append(
" User{ " )
            .Append(
" ID: " ).Append( this .ID).Append( " , Name:\ "" ).Append(this.Name).Append( " \ "" )
            .Append(
" , Birthday:\ "" ).Append(this.Birthday.ToString( " yyyy - MM - dd " )).Append( " \ "" )
            .Append(
" , Gender: " ).Append( this .Gender)
            .Append(
" } " ).ToString();
    }
}

Basic examples: get, set, expiration
this .Setup();

// Basic examples: get, set, expiration
MemcachedClient mc  =   this .GetClient();
mc.Set(
" key_1 " " A " .PadRight( 20 ' A ' ));
mc.Set(
" key_2 " " B " .PadRight( 20 ' B ' ), DateTime.Now.AddSeconds( 30 ));
mc.Set(
" key_3 " " C " .PadRight( 20 ' C ' ), DateTime.Now.AddSeconds( 15 ));
Console.WriteLine(
" {0}: " , DateTime.Now.ToString( " HH:mm:ss fff " ));
Console.WriteLine(
" \tkey_1: {0}\tno expiration " , mc.Get( " key_1 " ));
Console.WriteLine(
" \tkey_2: {0}\texpires after 30s " , mc.Get( " key_2 " ));
Console.WriteLine(
" \tkey_3: {0}\texpires after 15s " , mc.Get( " key_3 " ));

Thread.Sleep(
18   *   1000 );  // make the thread sleep for 18s, key_3 should expired
Console.WriteLine( " {0}: sleep 18s " , DateTime.Now.ToString( " HH:mm:ss fff " ));
Console.WriteLine(
" \tkey_1: {0} " , mc.Get( " key_1 " ));
Console.WriteLine(
" \tkey_2: {0} " , mc.Get( " key_2 " ));
Console.WriteLine(
" \tkey_3: {0} " , mc.Get( " key_3 " ));

mc.Add(
" key_1 " " X " .PadRight( 20 ' X ' ));
mc.Add(
" key_2 " " Y " .PadRight( 20 ' Y ' ));
mc.Add(
" key_3 " " Z " .PadRight( 20 ' Z ' ));
Console.WriteLine(
" {0}: try to change values by using add command " , DateTime.Now.ToString( " HH:mm:ss fff " ));

// make the thread sleep 15s, key_2 should expired and key_3 should be set a new value
Thread.Sleep( 15   *   1000 );
Console.WriteLine(
" {0}: sleep 15s " , DateTime.Now.ToString( " HH:mm:ss fff " ));
Console.WriteLine(
" \tkey_1: {0} " , mc.Get( " key_1 " ));
Console.WriteLine(
" \tkey_2: {0} " , mc.Get( " key_2 " ));
Console.WriteLine(
" \tkey_3: {0} " , mc.Get( " key_3 " ));

// object get and set
Console.WriteLine( " set an User object to cache server, then get it " );
User user 
=   new  User()
{
    ID 
=   601981 ,
    Name 
=   " riccc.cnblogs.com " ,
    Birthday 
=   new  DateTime( 1943 2 3 ),
    Gender 
=  UserGender.Male
};
mc.Set(
" user " , user);
user 
=  (User)mc.Get( " user " );
Console.WriteLine(user);

this.Shutdown();
Test output:
   

Multiple gets test
// Attention: gets commands only supported by memcached 1.2.5 or higher versions, 
//   so the flowing code needs memcached 1.2.5 at least
Hashtable values  =  mc.GetMultiple( new   string [] {  " key_1 " " key_2 " " key_3 "  });
Console.WriteLine(
" gets command test " );
foreach  ( object  key  in  values.Keys)
    Console.WriteLine(
" \t{0}: {1} " , key, values[key]);

Load balance test
从Memcached.ClientLibrary中把key-server映射的逻辑提出来,建立的测试代码如下:
public   class  ClientLibraryLoadBalance
{
    
private  ArrayList _servers;
    
private  ArrayList _weights;
    
private  ArrayList _buckets  =   new  ArrayList();

    
public  ClientLibraryLoadBalance( string [] servers,  int [] weights)
    {
        
this ._servers  =   new  ArrayList(servers);
        
this ._weights  =   new  ArrayList(weights);
        
if  (weights  !=   null   &&  weights.Length  >   0 )
        {
            
this ._buckets.Clear();
            
for  ( int  i  =   0 ; i  <  weights.Length; i ++ )
                
for  ( int  j  =   0 ; j  <  weights[i]; j ++ )
                    
this ._buckets.Add( this ._servers[i]);
        }
    }
    
private   string  MappingToServer( string  key)
    {
        
if  ( this ._buckets.Count  ==   1 return   this ._buckets[ 0 as   string ;
        
int  hashCode  =  key.GetHashCode();
        
int  bucket  =  hashCode  %  _buckets.Count;
        
if  (bucket  <   0 )
            bucket 
+=  _buckets.Count;
        
return   this ._buckets[bucket]  as   string ;
    }
    
public   void  MappingTest( int  keyCount)
    {
        
int [] mappingCount  =   new   int [ this ._servers.Count];
        
for  ( int  i  =   0 ; i  <  keyCount; i ++ )
        {
            
string  server  =   this .MappingToServer(Guid.NewGuid().ToString());
            
int  serverIndex  =   this ._servers.IndexOf(server);
            mappingCount[serverIndex]
++ ;
        }
        Console.WriteLine(
" {0} keys mapping to {1} servers " , keyCount.ToString( " #,### " ),  this ._servers.Count);
        
for  ( int  i  =   0 ; i  <  mappingCount.Length; i ++ )
            Console.WriteLine(
" {0}: {1} keys " this ._servers[i], mappingCount[i].ToString( " #,### " ));
    }
}

执行测试的代码:
ClientLibraryLoadBalance lb  =   new  ClientLibraryLoadBalance(
    
new   string [] {  " A.com " " B.com " " C.com " " D.com "  },
    
new   int [] {  2 1 3 4  });
lb.MappingTest(
1000000 );
lb.MappingTest(
10000000 );
lb.MappingTest(
50000000 );

测试结果如下图:
   
   
从测试结果来看分布的情况非常理想

功能特性说明
Memcached.ClientLibrary的key-server映射比较简单,基本原理就是对key求hash值,用hash值对服务器数量进行模运算,该key值被分配到模运算结果为索引的那台server上

Load Balance配置
通过SocketIOPool的Weights属性设置。假如有server A、B、C,根据其机器配置决定负载分别为40%、30%、30%,则如下配置即可:
pool.SetServers(new string[] { "A", "B", "C" });
pool.SetWeights(new int[] { 4, 3, 3 });

SocketIOPool的私有属性_buckets用于存放key-server映射的索引,内容为server的地址。为实现负载均衡的设置,key-server映射时不是直接对服务器数量进行模运算,而是对_buckets的count取模。如果某个服务器的Weights被设置为2,则该服务器在_buckets中会放2条记录,以这样的方式实现各服务器之间的负载分配
需要注意,如果设置了Weights,会给socket pool的设置带来影响。比如socket pool设置初始化连接数为5,按照上面Weights的设置,初始化时服务器A会创建4*5=20个连接,而B和C会分别创建15个连接。但是有的情况下又是以server为单位进行控制的,例如维护线程在检查最小连接数、最大连接数时,不管Weights如何设置,均以server为单位做检查。这是代码处理上不一致的问题

压缩
启用数据压缩,需要设置MemcachedClient对象的EnableCompression属性,并设置CompressionThreshold值
CompressionThreshold是启用压缩的阀值,默认为15k,即数据超过15k大小时将使用ICSharpCode.SharpZipLib对数据进行压缩
memcached的通讯协议中,存数据时可以为每个数据项提供一个16位的flag,用以对数据进行特殊标记,取数据时memcached将该标记原样返回。Memcached.ClientLibrary使用flag记录数据是否有压缩、是否使用了序列化等,读取服务器返回的数据时,如果flag表明该数据有压缩,则使用ICSharpCode.SharpZipLib对其解压

key的hash算法支持
因为Memcached.ClientLibrary直接使用hash值进行key-server映射,因此hash算法起的作用比较大。内部支持3种hash算法
HashingAlgorithm.Native: 即使用.NET本身的hash算法,速度快,但与其他client可能不兼容,例如需要和java、ruby的client共享缓存的情况
HashingAlgorithm.OldCompatibleHash: 可以与其他客户端兼容,但速度慢
HashingAlgorithm.NewCompatibleHash: 可以与其他客户端兼容,据称速度快
他允许使用其他hash算法,使用方式是MemcachedClient对象的Get、Set等方法,都有提供hash值的重载版本,client自己使用其他hash算法对key求hash值,然后传给MemcachedClient

同时使用多个SocketIOPool
比如已有系统A、B,分别使用自己的memcached server,之间不共享,现在开发系统C需要同时使用A、B的memcached server,则系统C中可以创建2个SocketIOPool;又比如,在系统中希望使用2个独立的memcached server,1个用于存放一些readonly、特殊的缓存数据,另一部分存放其他正常缓存数据等;比如web server上专门用一组memcached server存放用户session状态数据,用另一组存放应用层缓存数据等等
使用方法:可以对SocketIOPool设置名称,创建MemcachedClient对象时指定SocketIOPool的名称

代码结构、处理方式
只有3个类完成主要功能

SockIO: 负责socket通讯,例如创建socket对象、建立连接、读、写等。socket对象也是一直保持连接状态的
连接超时的管理:
他提供了连接超时的配置选项,这在高并发的情况下遇到server不可用时,可以用来防止大量socket连接阻塞执行线程(windows自身的连接超时时间以秒为单位)
windows的socket连接超时通过注册表配置,运用于整个windows,而API在建立socket连接时并没有连接超时时间设置
Memcached.ClientLibrary的实现方式为,如果配置了连接超时时间,每次创建socket连接时新开一个线程,用他创建sokcet对象并进行连接,执行线程则不停的sleep并检查新线程是否连接成功,如果连接成功则返回socket连接,否则达到超时时间时,主线程直接返回,新线程则交由.NET和windows进行释放
其实现上仍存在一个待处理问题,即需要对新开线程的数量进行管理,否则虽然执行线程没有被阻塞,还是会浪费大量线程资源尝试socket连接

SockIOPool:
1. socket pool的管理
2. 服务器节点状态管理
3. key-server映射管理
4. failover处理
5. 负载均衡的实现

socket pool管理
1. SocketPool的私有属性_availPool存放空闲的socket列表,_busyPool存放正在作业的socket列表
   _availPool和_busyPool都是HashTable,key为server的string,值为一个HashTable,存放SocketIO实例(这个HashTable的key为SocketIO实例对象,值为加入pool的时间,这个加入时间将用于最大空闲时间、最大工作时间的控制)
2. SocketPool.Initialize时,为每个server创建InitConnections数量的socket连接,放入空闲列表中
   如前面所说,实际是针对每个Weights单位创建初始化连接数量的
3. 处理请求时,如果空闲列表中存在,则从空闲列表取出socket,放入工作列表,并返回给请求者;作业结束后如果socket仍然是连接状态,则从工作列表中删除,放回空闲列表
   如果空闲列表中没有可用socket,则创建socket对象并返回给请求者
   创建过程的处理:并不是每次仅仅创建一个socket对象,第一次遇到socket不够用时将创建1个,第二次创建2个,第三次4个,每次创建数量将增倍,单次创建的最大数量为MinConnections/4(MinConnections小于4时取MinConnections)。多创建的socket放入空闲列表,最后一个放入工作列表并返回给请求者。SocketIOPool的_createShift存放下次创建时的倍数,key为server的string,值为倍数,维护线程在释放空闲列表中多余的socket之后会重置这个倍数
   这样的处理方式,有利于某时间段内请求快速上升时的处理性能
4. 处理请求时如果发生socket通讯异常,socket对象被真正释放掉,并从工作列表中移除
   如果发生其他类型异常,则将socket从工作列表移除并放入空闲列表
5. 如果有设置MaintenanceSleep,则每个socket pool会开一个维护线程,每间隔MaintenanceSleep时间执行一次维护工作
   维护内容包括:最小连接数、最大连接数、最长空闲时间、最长作业时间的检查控制。最小连接数和最大连接数是以空闲队列的数量做控制的,并不包含当前作业的socket数量。最长空闲时间、最长作业时间以_availPool、_busyPool中记录的时间为基准,socket对象每次加入_availPool和_busyPool时都记录了加入时间,socket作业时间如果超过最长作业时间设置,该socket会被强行中断掉。socket超过最大连接数设置时,并不是一次全部把超过的数量全部释放掉,类似于创建连接时的处理方式,也是逐次递减的释放socket对象,直到等于最大连接数设置

节点状态管理
1. SocketIOPool的私有属性_hostDead存放死节点
   key为server的string,值为加入_hostDead的时间;_hostDeadDuration存放下次尝试连接死节点的时间间隔,key为server的string,值为间隔时间
2. 如前面Load Balance配置中提到的,_buckets存放了当前所有节点,并且根据Weights的设置进行分配。节点变成dead server时也不会从_buckets中移除
3. 某个时间点某个server变得不可用时:
   作业中的socket将发生异常,直接被释放掉
   空闲队列中的socket仍会被分配给后续的请求,但这些请求将发生异常,将socket释放,请求的处理失败
   最后该server相关的socket会全部被释放掉,新的请求将尝试创建新的socket连接
4. 任何时候创建新的socket连接时:
   如果发生socket异常,则该节点被添加到_hostDead中,_hostDeadDuration的初始化值设置为100ms
   后续针对dead server的创建请求,依赖于failover的设置
   如果处理结果为节点可用了,则将该节点从_hostDead和_hostDeadDuration中移除,该节点恢复为正常工作节点

failover处理
用于在节点发生故障变成死节点时,提供后续的处理机制
启用failover(设置为true)时:
每隔一定时间间隔才尝试重新连接该节点,期间原来映射到该节点的key将被重新映射到其他可用节点上,该节点恢复之后,这些key会重新映射回该节点
具体处理过程为:
   新的请求被映射到dead server时,检查_hostDeadDuration
   如果还没有达到设定的时间间隔,则重新将key映射到其他可用server进行处理
   如果时间间隔已经到达,则尝试重新连接,连接成功会将该节点从_hostDead中移除,连接失败则将_hostDeadDuration中对应的间隔时间翻倍,即下次将等待更长的时间再尝试连接
禁用failover时:
任何时候新的请求被映射到dead server上,都尝试重新建立socket连接,如果连接建立失败,给客户端返回null值(get、gets等命令)或者是操作失败(返回false值,对于set、add等命令)

MemcachedClient: 为客户端提供各种操作,负责各种命令的实现。也包括压缩、解压的处理,序列化、反序列化等
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值