轉自:http://database.51cto.com/art/201008/219948.htm
公司網站使用了Memcached來做分佈式緩存,最近有人反映Memcached客戶端佔用CPU過高,懷疑是第三方客戶端性能不佳,進而懷疑是文本協議的問題,要求部門自己開發Memcached的客戶端,使其支持二進制協議。因爲重新開發客戶端工作量比較大,同時在日常開發中,沒有聽說過Memcached客戶端遇到瓶頸。因此對此問題進行了排查。結果發現主要是由於客戶端反序列化,類設計不合理造成的。把排查過程分享下,希望對其他人有所幫助。
首先想到是:Memcached服務器端內存佔滿,在清理內存中,造成客戶端socket連接不上,不斷髮生異常。隨上服務器查看了Memcached的內存佔用率,連接數等,發現利用率均很低。暫時先排除服務器端問題。
其次想到可能是第三方在使用socket連接池時,造成資源沒有關閉,或者死鎖。隨對第三方客戶端代碼粗略讀了一遍,並搜索相關文檔。未發現異常代碼。暫時先排除第三方客戶端問題。
最後想到會不會是開發人員在代碼編寫中出現了問題。隨對反映問題的兩個產品進行了排查。發現了以下代碼。
- 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
- {
- //房間有觀看人員數據
- 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 = 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時,再進行存儲,新存儲的內容再取出來就是空值,不知道是什麼原因。