Richie

Sometimes at night when I look up at the stars, and see the whole sky just laid out there, don't you think I ain't remembering it all. I still got dreams like anybody else, and ever so often, I am thinking about how things might of been. And then, all of a sudden, I'm forty, fifty, sixty years old, you know?

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 { getset; }
    
public string Name { getset; }
    
public DateTime Birthday { getset; }
    
public UserGender Gender { getset; }
    
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(194323),
    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 == 1return this._buckets[0as 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[] { 2134 });
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: 为客户端提供各种操作,负责各种命令的实现。也包括压缩、解压的处理,序列化、反序列化等

posted on 2009-11-22 22:49  riccc  阅读(5934)  评论(2编辑  收藏  举报

导航