公司网站使用了memCached来做分布式缓存,最近有人反映memCached客户端占用CPU过高,怀疑是第三方客户端性能不佳,进而怀疑是文本协议的问题,要求部门自己开发memCached的客户端,使其支持二进制协议。因为重新开发客户端工作量比较大,同时在日常开发中,没有听说过memCached客户端遇到瓶颈。因此对此问题进行了排查。结果发现主要是由于客户端反序列化,类设计不合理造成的。把排查过程分享下,希望对其他人有所帮助。
首先想到是:memCached服务器端内存占满,在清理内存中,造成客户端socket连接不上,不断发生异常。随上服务器查看了memCached的内存占用率,连接数等,发现利用率均很低。暂时先排除服务器端问题。
其次想到可能是第三方在使用socket连接池时,造成资源没有关闭,或者死锁。随对第三方客户端代码粗略读了一遍,并搜索相关文档。未发现异常代码。暂时先排除第三方客户端问题。
最后想到会不会是开发人员在代码编写中出现了问题。随对反映问题的两个产品进行了排查。发现了以下代码。
代码片段1:
public static List<UserModule> GetAllUserModule(int userId)
{
string cache = CacheManager.Current.Get<string>(GetCacheKey(userId));
if (!string.IsNullOrEmpty(cache))
{
return ser.Deserialize(cache) as List<UserModule>;
}
else
{
return null;
}
} public static List<UserModule> SetAllUserModule(int userId, List<UserModule> modules)
{
if (modules != null)
{
string cache = ser.Serialize(modules);
CacheManager.Current.Add(GetCacheKey(userId), cache);
}
else
{
CacheManager.Current.Remove(GetCacheKey(userId));
}
return modules;
}
代码片段2:
/// 聊天室房间
/// </summary>
[Serializable]
public class Room
{
//房间有观看人员数据
List<Viewer> _viewers = null;
List<string> _blackips = null;
List<Viewer> _blackviewers = null;
List<Notice> _notice = null;
List<Speaker > _speakers = null;
List<Content> _content = null;
/// <summary>
/// 添加新聊天者
/// </summary>
/// <returns>返回新添加的聊天人员</returns>
public Viewer AddViewer()
{
Viewer vi = new Viewer();
//MaxViewerID += 1;
//int id = MaxViewerID;
int id = GetViewerID();
vi.Name = GetViewerName("游客" + id);
//vi.IP = System.Web.HttpContext.Current.Request.UserHostAddress;
vi.IP = "127.0.0.1";
vi.ViewID = id;
Viewers.Add(vi);
return vi;
} /// <summary>
/// 添加聊天内容
/// </summary>
/// <param name="content">聊天的内容</param>
/// <param name="viewid">发言人的id</param>
/// <returns>返回新添加的对象</returns>
public Content AddContent(string content, int viewid)
{
MaxContentID += 1;
Content con = new Content(DateTime.Now, content, viewid, MaxContentID);
Contents.Add(con);
return con;
}
......
}
调用代码为:
Room room
lock (room)
{
if (room.MaxContentID == 0)
{
//ChatContentOp cpo = new ChatContentOp();
//room.MaxContentID = cpo.GetMaxContentID();
room.MaxContentID = 300;
}
int viewerID = 123124123;
room.AddContent(chatContent, viewerID);
//判断内容是否大于100条。如果大于100条,删除最近的100条以外的数据。
System.IO.File.AppendAllText(@"d:\haha.txt", "最大数值:" + room.LimitContentCount + "###############聊天记录数:" + room.Contents.Count + "\r\n");
if (room.Contents.Count > room.LimitContentCount)
{
room.Contents.RemoveRange(0, room.Contents.Count - room.LimitContentCount);
}
}
LiveSys.Set(key, room);
代码1存在的问题是:
Cache存储的参数类型为object,没有必要先进行一次序列化,然后再进行存储。而序列化是很消耗CPU的。
代码2问题:
代码2实现的是一个在线聊天室,聊天室本身含有访客,发言等内容。在发言时,对聊天室内容进行判断,只显示最近30条。新进来访客直接加到访客别表中。表面上是没什么问题的。但是细想之下有两个问题:
1 聊天室类设计的比较复杂,每次从memCached服务端取得数据后,都要进行类型转换。
2 没有访客清理机制。随着访客的不断进入,对象的体积会不断增大。
对存疑部分编写了代码进行测试。测试结果果然如推测所想。测试结果如下:
场景 |
写入 |
读取 |
大小 (单位) |
CPU |
||||
次数 |
时间 |
平均 |
次数 |
时间 |
平均 |
|||
本地缓存 |
10000 |
0.03125 |
0 |
10000 |
0 |
0 |
1k |
0 |
MemClient |
10000 |
19.2656 |
0.001926 |
10000 |
22.75 |
0.002275 |
1k |
|
Json1k |
1000 |
2.8437 |
0.002843 |
1000 |
5.375 |
0.005375 |
1k |
|
Json8k |
1000 |
3.8593 |
0.003859 |
1000 |
29.0312 |
0.029031 |
8k |
|
直播1000人次 |
1000 |
38.9375 |
0.038937 |
1000 |
|
|
50k |
|
直播8000人次 |
100 |
18.25 |
0.1825 |
100 |
|
|
350k |
|
500k |
100 |
7.375 |
0.07375 |
100 |
7.09375 |
0.070937 |
500k |
|
场景 |
写入 |
读取 |
大小 (单位) |
CPU |
||||
次数 |
时间 |
平均 |
次数 |
时间 |
平均 |
|||
本地缓存 |
10000 |
0.03125 |
3.125E-06 |
10000 |
0.015625 |
1.5625E-06 |
1k |
0 |
MemClient |
10000 |
19.78125 |
0.001978 |
10000 |
21.953125 |
0.002195 |
1k |
|
Json1k |
1000 |
2.03125 |
0.002031 |
1000 |
6.078125 |
0.006078 |
1k |
|
Json8k |
1000 |
2.765625 |
0.002765 |
1000 |
55.375 |
0.055375 |
8k |
|
直播1000人次 |
1000 |
38.53125 |
0.038531 |
1000 |
|
|
50k |
|
直播8000人次 |
100 |
17.96875 |
0.179687 |
1000 |
|
|
350k |
|
500k |
100 |
7.5 |
0.075 |
100 |
6.5625 |
0.065625 |
500k |
|
场景 |
写入 |
读取 |
|