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
用于测试的基本代码
Basic examples: get, set, expiration
Multiple gets test
Load balance test
从Memcached.ClientLibrary中把key-server映射的逻辑提出来,建立的测试代码如下:
执行测试的代码:
测试结果如下图:
从测试结果来看分布的情况非常理想
功能特性说明
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: 为客户端提供各种操作,负责各种命令的实现。也包括压缩、解压的处理,序列化、反序列化等
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;
}
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();
}
}
{
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://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();
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]);
// 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("#,###"));
}
}
{
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);
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: 为客户端提供各种操作,负责各种命令的实现。也包括压缩、解压的处理,序列化、反序列化等