關(guān)于Memcached客戶(hù)端CPU過(guò)高問(wèn)題的排查
公司網(wǎng)站使用了Memcached來(lái)做分布式緩存,最近有人反映Memcached客戶(hù)端占用CPU過(guò)高,懷疑是第三方客戶(hù)端性能不佳,進(jìn)而懷疑是文本協(xié)議的問(wèn)題,要求部門(mén)自己開(kāi)發(fā)Memcached的客戶(hù)端,使其支持二進(jìn)制協(xié)議。因?yàn)橹匦麻_(kāi)發(fā)客戶(hù)端工作量比較大,同時(shí)在日常開(kāi)發(fā)中,沒(méi)有聽(tīng)說(shuō)過(guò)Memcached客戶(hù)端遇到瓶頸。因此對(duì)此問(wèn)題進(jìn)行了排查。結(jié)果發(fā)現(xiàn)主要是由于客戶(hù)端反序列化,類(lèi)設(shè)計(jì)不合理造成的。把排查過(guò)程分享下,希望對(duì)其他人有所幫助。
首先想到是:Memcached服務(wù)器端內(nèi)存占滿(mǎn),在清理內(nèi)存中,造成客戶(hù)端socket連接不上,不斷發(fā)生異常。隨上服務(wù)器查看了Memcached的內(nèi)存占用率,連接數(shù)等,發(fā)現(xiàn)利用率均很低。暫時(shí)先排除服務(wù)器端問(wèn)題。
其次想到可能是第三方在使用socket連接池時(shí),造成資源沒(méi)有關(guān)閉,或者死鎖。隨對(duì)第三方客戶(hù)端代碼粗略讀了一遍,并搜索相關(guān)文檔。未發(fā)現(xiàn)異常代碼。暫時(shí)先排除第三方客戶(hù)端問(wèn)題。
最后想到會(huì)不會(huì)是開(kāi)發(fā)人員在代碼編寫(xiě)中出現(xiàn)了問(wèn)題。隨對(duì)反映問(wèn)題的兩個(gè)產(chǎn)品進(jìn)行了排查。發(fā)現(xiàn)了以下代碼。
- static Serializer ser = new Serializer(typeof(List<UserModule>));
- //using JsonExSerializer;
- 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>
- /// 聊天室房間
- /// </summary>
- [Serializable]
- public class Room
- {
- //房間有觀(guān)看人員數(shù)據(jù)
- 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>
- /// 添加聊天內(nèi)容
- /// </summary>
- /// <param name="content">聊天的內(nèi)容</param>
- /// <param name="viewid">發(fā)言人的id</param>
- /// <returns>返回新添加的對(duì)象</returns>
- public Content AddContent(string content, int viewid)
- {
- MaxContentID += 1;
- Content con = new Content(DateTime.Now, content, viewid, MaxContentID);
- Contents.Add(con);
- return con;
- }
- ......
- }
調(diào)用代碼為:
- 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);
- //判斷內(nèi)容是否大于100條。如果大于100條,刪除最近的100條以外的數(shù)據(jù)。
- System.IO.File.AppendAllText(@"d:\haha.txt", "最大數(shù)值:" +
- room.LimitContentCount + "###############聊天記錄數(shù):" + 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存在的問(wèn)題是:
Cache存儲(chǔ)的參數(shù)類(lèi)型為object,沒(méi)有必要先進(jìn)行一次序列化,然后再進(jìn)行存儲(chǔ)。而序列化是很消耗CPU的。
代碼2問(wèn)題:
代碼2實(shí)現(xiàn)的是一個(gè)在線(xiàn)聊天室,聊天室本身含有訪(fǎng)客,發(fā)言等內(nèi)容。在發(fā)言時(shí),對(duì)聊天室內(nèi)容進(jìn)行判斷,只顯示最近30條。新進(jìn)來(lái)訪(fǎng)客直接加到訪(fǎng)客別表中。表面上是沒(méi)什么問(wèn)題的。但是細(xì)想之下有兩個(gè)問(wèn)題:
1 聊天室類(lèi)設(shè)計(jì)的比較復(fù)雜,每次從Memcached服務(wù)端取得數(shù)據(jù)后,都要進(jìn)行類(lèi)型轉(zhuǎn)換。
2 沒(méi)有訪(fǎng)客清理機(jī)制。隨著訪(fǎng)客的不斷進(jìn)入,對(duì)象的體積會(huì)不斷增大。
對(duì)存疑部分編寫(xiě)了代碼進(jìn)行測(cè)試。測(cè)試結(jié)果果然如推測(cè)所想。測(cè)試結(jié)果如下:
場(chǎng)景 |
寫(xiě)入 |
讀取 |
大小 (單位) |
CPU |
||||
次數(shù) |
時(shí)間 |
平均 |
次數(shù) |
時(shí)間 |
平均 |
|||
本地緩存 |
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 |
|
場(chǎng)景 |
寫(xiě)入 |
讀取 |
大小 (單位) |
CPU |
||||
次數(shù) |
時(shí)間 |
平均 |
次數(shù) |
時(shí)間 |
平均 |
|||
本地緩存 |
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 |
場(chǎng)景 |
寫(xiě)入 |
讀取 |
大小 (單位) |
CPU |
||||
次數(shù) |
時(shí)間 |
平均 |
次數(shù) |
時(shí)間 |
平均 |
|||
本地緩存 |
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人次(當(dāng)天一共有1000人訪(fǎng)問(wèn),數(shù)據(jù)來(lái)源于運(yùn)營(yíng)檢測(cè)),留言?xún)?nèi)容為30條時(shí),Room體積大概為:57K
直播1000人次(當(dāng)天一共有8000人訪(fǎng)問(wèn),數(shù)據(jù)來(lái)源于運(yùn)營(yíng)檢測(cè)),留言?xún)?nèi)容為30條時(shí),Room體積大概為:350k
根據(jù)圖表可以看到以下情況:處理時(shí)間、CPU利用率和數(shù)據(jù)量大小,序列化,類(lèi)復(fù)雜性都有關(guān)系。
序列化問(wèn)題(類(lèi)型轉(zhuǎn)換)對(duì)性能影響最為明顯(可在場(chǎng)景”json1k”、場(chǎng)景直播中看到)。在Json1k中,存儲(chǔ)對(duì)象和前幾個(gè)場(chǎng)景是相同的,處理時(shí)間也相差不大,較大區(qū)別是CPU利用率由5%左右增長(zhǎng)到40%左右(反序列化時(shí)尤為明顯)。在場(chǎng)景直播系統(tǒng)中,不存在序列化問(wèn)題,但是其對(duì)象屬性中存在”訪(fǎng)客”, ”繁衍”等多個(gè)復(fù)雜對(duì)象,造成其在處理時(shí)需要處理過(guò)多的類(lèi)型轉(zhuǎn)換,同時(shí)其體積不斷增大。
存儲(chǔ)對(duì)象的大小和處理時(shí)間存在一定關(guān)系,例如場(chǎng)景”500k”,其處理時(shí)間增長(zhǎng),但是其CPU利用率并未提高,其時(shí)間增長(zhǎng)是由于對(duì)象傳輸造成。
本地緩存在內(nèi)存中進(jìn)行尋址和類(lèi)型轉(zhuǎn)換,涉及不到Socket連接,網(wǎng)絡(luò)傳輸,序列化操作,所以其處理相當(dāng)快。
就測(cè)試結(jié)果看:
本地緩存性能大約是分布式緩存性能的100倍左右。而出問(wèn)題的聊天室除了CPU增高以外,其性能更比分布式緩存再降低40倍(直播1000人次)到200倍(直播8000人次)。綜合來(lái)看,聊天室的分布式緩存比本地緩存降了4000倍,甚至更多。
但是,還沒(méi)有完。
對(duì)于第二個(gè)問(wèn)題,更改類(lèi)設(shè)計(jì),清楚無(wú)效訪(fǎng)客,即可解決。
但是第一個(gè)問(wèn)題,為什么用戶(hù)在存儲(chǔ)之前,先進(jìn)行json序列化呢?嗯,這是一個(gè)問(wèn)題。
遂問(wèn)之。
答曰,有些類(lèi)直接使用第三方客戶(hù)端存儲(chǔ)時(shí),直接存儲(chǔ)報(bào)錯(cuò),所以先序列化為json類(lèi)型,取值時(shí)再反序列化回來(lái)。
嗯,還有這事?
開(kāi)發(fā)人員說(shuō)了相關(guān)代碼。
- 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;}
- }
他說(shuō):Game對(duì)象在直接使用MemcachedClient時(shí),是不能被二進(jìn)制序列化的,因?yàn)槠?/span>User屬性類(lèi)型為IUser,為一個(gè)接口。因此想了一個(gè)解決方法,即先將Game對(duì)象進(jìn)行 json序列化將其變?yōu)樽址缓髮⒆址鎯?chǔ)到Memcached。
原來(lái)是這樣。
接著又查看了MemcachedClient源代碼,其需要將對(duì)象進(jìn)行二進(jìn)制序列化,然后進(jìn)行存儲(chǔ)。接口屬性不能被序列化,遂又對(duì)序列化問(wèn)題進(jìn)行了測(cè)試(見(jiàn)附件)。測(cè)試結(jié)果顯示上述代碼直接進(jìn)行二進(jìn)制序列化是可以的,同時(shí)直接使用第三方客戶(hù)端也是可以可行的。
問(wèn)題出在哪?難道是沒(méi)有加[Serializable]。
一查果然:一個(gè)Serializable引發(fā)的血案。。。
記得有人說(shuō)過(guò),慎用分布式,能不用盡量不用。
一方面在性能上確實(shí)下降很多,分布式存儲(chǔ)主要性能消耗在以下幾個(gè)方面:協(xié)議解析,Socket連接,數(shù)據(jù)傳輸,序列化/類(lèi)型轉(zhuǎn)換。
一方面在使用場(chǎng)景和類(lèi)設(shè)計(jì)上要求也更加嚴(yán)格。個(gè)人認(rèn)為Memcached是不太適合存儲(chǔ)特別大的文件的。雖然有人說(shuō)網(wǎng)上已經(jīng)有用來(lái)存儲(chǔ)視頻的。
還有幾個(gè)問(wèn)題希望知道的朋友回答下:
1 有沒(méi)有.Net方面的Memcached客戶(hù)端支持二進(jìn)制協(xié)議和一致性的?
2 測(cè)試中發(fā)現(xiàn),當(dāng)Memcached設(shè)置緩存過(guò)小時(shí)(例如64M),當(dāng)其內(nèi)存使用已經(jīng)到62M時(shí),再進(jìn)行存儲(chǔ),新存儲(chǔ)的內(nèi)容再取出來(lái)就是空值,不知道是什么原因。
原文標(biāo)題:Memcached客戶(hù)端CPU過(guò)高問(wèn)題的排查
鏈接:http://www.cnblogs.com/hellofox2000/archive/2010/08/17/1801329.html
【編輯推薦】