關於Memcached客戶端CPU過高問題的排查

轉自:http://database.51cto.com/art/201008/219948.htm

公司網站使用了Memcached來做分佈式緩存,最近有人反映Memcached客戶端佔用CPU過高,懷疑是第三方客戶端性能不佳,進而懷疑是文本協議的問題,要求部門自己開發Memcached的客戶端,使其支持二進制協議。因爲重新開發客戶端工作量比較大,同時在日常開發中,沒有聽說過Memcached客戶端遇到瓶頸。因此對此問題進行了排查。結果發現主要是由於客戶端反序列化,類設計不合理造成的。把排查過程分享下,希望對其他人有所幫助。 

首先想到是:Memcached服務器端內存佔滿,在清理內存中,造成客戶端socket連接不上,不斷髮生異常。隨上服務器查看了Memcached的內存佔用率,連接數等,發現利用率均很低。暫時先排除服務器端問題。 

其次想到可能是第三方在使用socket連接池時,造成資源沒有關閉,或者死鎖。隨對第三方客戶端代碼粗略讀了一遍,並搜索相關文檔。未發現異常代碼。暫時先排除第三方客戶端問題。 

最後想到會不會是開發人員在代碼編寫中出現了問題。隨對反映問題的兩個產品進行了排查。發現了以下代碼。

  1. static Serializer ser = new Serializer(typeof(List<UserModule>)); 
  2. //using JsonExSerializer;  
  3. public static List<UserModule> GetAllUserModule(int userId)  
  4. {  
  5.     string cache = CacheManager.Current.Get<string>(GetCacheKey(userId));  
  6.     if (!string.IsNullOrEmpty(cache))  
  7.     {  
  8.         return ser.Deserialize(cache) as List<UserModule>;  
  9.     }  
  10.     else 
  11.     {  
  12.         return null;  
  13.     }  
  14. }  
  15.  
  16. public static List<UserModule> SetAllUserModule(int userId, List<UserModule> modules)  
  17. {  
  18.     if (modules != null)  
  19.     {  
  20.         string cache = ser.Serialize(modules);  
  21.         CacheManager.Current.Add(GetCacheKey(userId), cache);  
  22.     }  
  23.     else 
  24.     {  
  25.         CacheManager.Current.Remove(GetCacheKey(userId));  
  26.     }  
  27.     return modules;  

代碼片段2  

  1. /// <summary>  
  2. /// 聊天室房間  
  3. /// </summary>  
  4. [Serializable]  
  5. public class Room  
  6. {  
  7.     //房間有觀看人員數據  
  8.     List<Viewer> _viewers = null;  
  9.     List<string> _blackips = null;  
  10.     List<Viewer> _blackviewers = null;  
  11.     List<Notice> _notice = null;  
  12.     List<Speaker > _speakers = null;  
  13.     List<Content> _content = null;  
  14.  
  15.  
  16.     /// <summary>  
  17.     /// 添加新聊天者  
  18.     /// </summary>  
  19.     /// <returns>返回新添加的聊天人員</returns>  
  20.     public Viewer AddViewer()  
  21.     {  
  22.         Viewer vi = new Viewer();  
  23.         //MaxViewerID += 1;  
  24.           
  25.         //int id = MaxViewerID;   
  26.         int id = GetViewerID();   
  27.         vi.Name = GetViewerName("遊客" + id);  
  28.         //vi.IP = System.Web.HttpContext.Current.Request.UserHostAddress;  
  29.         vi.IP = "127.0.0.1";  
  30.         vi.ViewID = id;  
  31.         Viewers.Add(vi);  
  32.         return vi;   
  33.     }  
  34.  
  35. /// <summary>  
  36.     /// 添加聊天內容  
  37.     /// </summary>  
  38.     /// <param name="content">聊天的內容</param>  
  39.     /// <param name="viewid">發言人的id</param>  
  40.     /// <returns>返回新添加的對象</returns>  
  41.     public Content AddContent(string content, int viewid)  
  42.     {  
  43.         MaxContentID += 1;  
  44.         Content con = new Content(DateTime.Now, content, viewid, MaxContentID);  
  45.         Contents.Add(con);  
  46.         return con;  
  47.     }  
  48.     ......  

調用代碼爲:

  1. Room room = LiveSys.Get(key);  
  2. lock (room)  
  3. {  
  4.     if (room.MaxContentID == 0)  
  5.     {  
  6.         //ChatContentOp cpo = new ChatContentOp();  
  7.         //room.MaxContentID = cpo.GetMaxContentID();  
  8.  
  9.         room.MaxContentID = 300;  
  10.     }  
  11.     int viewerID = 123124123;  
  12.     room.AddContent(chatContent, viewerID);  
  13.     //判斷內容是否大於100條。如果大於100條,刪除最近的100條以外的數據。  
  14.     System.IO.File.AppendAllText(@"d:\haha.txt""最大數值:" + 
  15. room.LimitContentCount + "###############聊天記錄數:" + room.Contents.Count + "\r\n");  
  16.     if (room.Contents.Count > room.LimitContentCount)  
  17.     {  
  18.         room.Contents.RemoveRange(0, room.Contents.Count - room.LimitContentCount);  
  19.     }  
  20. }  
  21. LiveSys.Set(key, room); 

代碼1存在的問題是:

Cache存儲的參數類型爲object,沒有必要先進行一次序列化,然後再進行存儲。而序列化是很消耗CPU的。

代碼2問題:

代碼2實現的是一個在線聊天室,聊天室本身含有訪客,發言等內容。在發言時,對聊天室內容進行判斷,只顯示最近30條。新進來訪客直接加到訪客別表中。表面上是沒什麼問題的。但是細想之下有兩個問題:

聊天室類設計的比較複雜,每次從Memcached服務端取得數據後,都要進行類型轉換。

沒有訪客清理機制。隨着訪客的不斷進入,對象的體積會不斷增大。

對存疑部分編寫了代碼進行測試。測試結果果然如推測所想。測試結果如下:

場景

寫入

讀取

大小

(單位)

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類型,取值時再反序列化回來。

嗯,還有這事?

開發人員說了相關代碼。 

  1. interface IUser  
  2. {  
  3.     String UserId{ getset;}  
  4.     String UserName{ getset;}  
  5. }  
  6.  
  7. [Serializable]  
  8. class UserInfo : IUser  
  9. {  
  10.     String UserId{ getset;}  
  11.     String UserName{ getset;}  
  12. }  
  13. [Serializable]  
  14. class Game  
  15. {  
  16.     IUser User{ getset;}  
  17.     String UserName{ getset;}  

他說:Game對象在直接使用MemcachedClient時,是不能被二進制序列化的,因爲其User屬性類型爲IUser,爲一個接口。因此想了一個解決方法,即先將Game對象進行 json序列化將其變爲字符串,然後將字符串存儲到Memcached 

原來是這樣。

接着又查看了MemcachedClient源代碼,其需要將對象進行二進制序列化,然後進行存儲。接口屬性不能被序列化,遂又對序列化問題進行了測試(見附件)。測試結果顯示上述代碼直接進行二進制序列化是可以的,同時直接使用第三方客戶端也是可以可行的。 

問題出在哪?難道是沒有加[Serializable]

一查果然:一個Serializable引發的血案。。。

記得有人說過,慎用分佈式,能不用盡量不用。

一方面在性能上確實下降很多,分佈式存儲主要性能消耗在以下幾個方面:協議解析,Socket連接,數據傳輸,序列化/類型轉換。

一方面在使用場景和類設計上要求也更加嚴格。個人認爲Memcached是不太適合存儲特別大的文件的。雖然有人說網上已經有用來存儲視頻的。

還有幾個問題希望知道的朋友回答下:

有沒有.Net方面的Memcached客戶端支持二進制協議和一致性的?

測試中發現,當Memcached設置緩存過小時(例如64M),當其內存使用已經到62M時,再進行存儲,新存儲的內容再取出來就是空值,不知道是什麼原因。

發佈了30 篇原創文章 · 獲贊 30 · 訪問量 74萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章