公司网站使用了Memcached来做分布式缓存,最近有人反映Memcached客户端占用CPU过高,怀疑是第三方客户端性能不佳,进而怀疑是文本协议的问题,要求部门自己开发Memcached的客户端,使其支持二进制协议。因为重新开发客户端工作量比较大,同时在日常开发中,没有听说过Memcached客户端遇到瓶颈。因此对此问题进行了排查。结果发现主要是由于客户端反序列化,类设计不合理造成的。把排查过程分享下,希望对其他人有所帮助。

创新互联长期为上千余家客户提供的网站建设服务,团队从业经验10年,关注不同地域、不同群体,并针对不同对象提供差异化的产品和服务;打造开放共赢平台,与合作伙伴共同营造健康的互联网生态环境。为梁子湖企业提供专业的成都网站制作、成都做网站、外贸营销网站建设,梁子湖网站改版等技术服务。拥有十多年丰富建站经验和众多成功案例,为您定制开发。
首先想到是:Memcached服务器端内存占满,在清理内存中,造成客户端socket连接不上,不断发生异常。随上服务器查看了Memcached的内存占用率,连接数等,发现利用率均很低。暂时先排除服务器端问题。
其次想到可能是第三方在使用socket连接池时,造成资源没有关闭,或者死锁。随对第三方客户端代码粗略读了一遍,并搜索相关文档。未发现异常代码。暂时先排除第三方客户端问题。
最后想到会不会是开发人员在代码编写中出现了问题。随对反映问题的两个产品进行了排查。发现了以下代码。
- static Serializer ser = new Serializer(typeof(List
 )); - //using JsonExSerializer;
 - public static List
 GetAllUserModule(int userId) - {
 - string cache = CacheManager.Current.Get
 (GetCacheKey(userId)); - if (!string.IsNullOrEmpty(cache))
 - {
 - return ser.Deserialize(cache) as List
 ; - }
 - else
 - {
 - return null;
 - }
 - }
 - public static List
 SetAllUserModule(int userId, List modules) - {
 - if (modules != null)
 - {
 - string cache = ser.Serialize(modules);
 - CacheManager.Current.Add(GetCacheKey(userId), cache);
 - }
 - else
 - {
 - CacheManager.Current.Remove(GetCacheKey(userId));
 - }
 - return modules;
 - }
 
代码片段2:
- ///
 - /// 聊天室房间
 - ///
 - [Serializable]
 - public class Room
 - {
 - //房间有观看人员数据
 - List
 _viewers = null; - List
 _blackips = null; - List
 _blackviewers = null; - List
 _notice = null; - List
 _speakers = null; - List
 _content = null; - ///
 - /// 添加新聊天者
 - ///
 - ///
 返回新添加的聊天人员 - 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;
 - }
 - ///
 - /// 添加聊天内容
 - ///
 - /// 聊天的内容
 - /// 发言人的id
 - ///
 返回新添加的对象 - 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 = LiveSys.Get(key);
 - 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  | |
| 
 场景  | 
 写入  | 
 读取  | 
 大小 (单位)  | 
 CPU  | ||||
次数  | 时间  | 平均  | 次数  | 时间  | 平均  | |||
本地缓存  | 10000  | 0.015625  | 1.5625E-06  | 10000  | 0.015625  | 1.5625E-06  | 1k  | 0  | 
MemClient  | 10000  | 18.015625  | 0.001801  | 10000  | 25.96875  | 0.002596  | 1k  | 6%  | 
Json1k  | 1000  | 1.15625  | 0.001156  | 1000  | 3.078125  | 0.003078  | 1k  | 40%  | 
Json8k  | 1000  | 1.859375  | 0.001859  | 1000  | 32.484375  | 0.032484  | 8k  | 50%  | 
直播1000人次  | 1000  | 45.046875  | 0.045046  | 1000  | 50k  | 30-40%  | ||
直播8000人次  | 100  | 31.703125  | 0.317031  | 100  | 350k  | 50%  | ||
500k  | 100  | 7.0625  | 0.070625  | 100  | 6.421875  | 0.064218  | 500k  | 6%  | 
直播1000人次(当天一共有1000人访问,数据来源于运营检测),留言内容为30条时,Room体积大概为:57K
直播1000人次(当天一共有8000人访问,数据来源于运营检测),留言内容为30条时,Room体积大概为:350k
根据图表可以看到以下情况:处理时间、CPU利用率和数据量大小,序列化,类复杂性都有关系。
序列化问题(类型转换)对性能影响最为明显(可在场景”json1k”、场景直播中看到)。在Json1k中,存储对象和前几个场景是相同的,处理时间也相差不大,较大区别是CPU利用率由5%左右增长到40%左右(反序列化时尤为明显)。在场景直播系统中,不存在序列化问题,但是其对象属性中存在”访客”, ”繁衍”等多个复杂对象,造成其在处理时需要处理过多的类型转换,同时其体积不断增大。
存储对象的大小和处理时间存在一定关系,例如场景”500k”,其处理时间增长,但是其CPU利用率并未提高,其时间增长是由于对象传输造成。
本地缓存在内存中进行寻址和类型转换,涉及不到Socket连接,网络传输,序列化操作,所以其处理相当快。
就测试结果看:
本地缓存性能大约是分布式缓存性能的100倍左右。而出问题的聊天室除了CPU增高以外,其性能更比分布式缓存再降低40倍(直播1000人次)到200倍(直播8000人次)。综合来看,聊天室的分布式缓存比本地缓存降了4000倍,甚至更多。
但是,还没有完。
对于第二个问题,更改类设计,清楚无效访客,即可解决。
但是第一个问题,为什么用户在存储之前,先进行json序列化呢?嗯,这是一个问题。
遂问之。
答曰,有些类直接使用第三方客户端存储时,直接存储报错,所以先序列化为json类型,取值时再反序列化回来。
嗯,还有这事?
开发人员说了相关代码。
- interface IUser
 - {
 - String UserId{ get; set;}
 - String UserName{ get; set;}
 - }
 - [Serializable]
 - class UserInfo : IUser
 - {
 - String UserId{ get; set;}
 - String UserName{ get; set;}
 - }
 - [Serializable]
 - class Game
 - {
 - IUser User{ get; set;}
 - String UserName{ get; set;}
 - }
 
他说:Game对象在直接使用MemcachedClient时,是不能被二进制序列化的,因为其User属性类型为IUser,为一个接口。因此想了一个解决方法,即先将Game对象进行 json序列化将其变为字符串,然后将字符串存储到Memcached。
原来是这样。
接着又查看了MemcachedClient源代码,其需要将对象进行二进制序列化,然后进行存储。接口属性不能被序列化,遂又对序列化问题进行了测试(见附件)。测试结果显示上述代码直接进行二进制序列化是可以的,同时直接使用第三方客户端也是可以可行的。
问题出在哪?难道是没有加[Serializable]。
一查果然:一个Serializable引发的血案。
记得有人说过,慎用分布式,能不用尽量不用。
一方面在性能上确实下降很多,分布式存储主要性能消耗在以下几个方面:协议解析,Socket连接,数据传输,序列化/类型转换。
一方面在使用场景和类设计上要求也更加严格。个人认为Memcached是不太适合存储特别大的文件的。虽然有人说网上已经有用来存储视频的。
还有几个问题希望知道的朋友回答下:
1 有没有.Net方面的Memcached客户端支持二进制协议和一致性的?
2 测试中发现,当Memcached设置缓存过小时(例如64M),当其内存使用已经到62M时,再进行存储,新存储的内容再取出来就是空值,不知道是什么原因。
                网页标题:关于Memcached客户端CPU过高问题的排查
                
                地址分享:http://www.csdahua.cn/qtweb/news11/62311.html
            
网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网