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