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

創(chuàng)新互聯(lián)長期為上千余家客戶提供的網(wǎng)站建設服務,團隊從業(yè)經(jīng)驗10年,關(guān)注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務;打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為梁子湖企業(yè)提供專業(yè)的成都網(wǎng)站制作、成都做網(wǎng)站、外貿(mào)營銷網(wǎng)站建設,梁子湖網(wǎng)站改版等技術(shù)服務。擁有十多年豐富建站經(jīng)驗和眾多成功案例,為您定制開發(fā)。
首先想到是:Memcached服務器端內(nèi)存占滿,在清理內(nèi)存中,造成客戶端socket連接不上,不斷發(fā)生異常。隨上服務器查看了Memcached的內(nèi)存占用率,連接數(shù)等,發(fā)現(xiàn)利用率均很低。暫時先排除服務器端問題。
其次想到可能是第三方在使用socket連接池時,造成資源沒有關(guān)閉,或者死鎖。隨對第三方客戶端代碼粗略讀了一遍,并搜索相關(guān)文檔。未發(fā)現(xiàn)異常代碼。暫時先排除第三方客戶端問題。
最后想到會不會是開發(fā)人員在代碼編寫中出現(xiàn)了問題。隨對反映問題的兩個產(chǎn)品進行了排查。發(fā)現(xiàn)了以下代碼。
- 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
- {
- //房間有觀看人員數(shù)據(jù)
- 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;
- }
- ///
- /// 添加聊天內(nèi)容
- ///
- /// 聊天的內(nèi)容
- /// 發(fā)言人的id
- ///
返回新添加的對象 - 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,沒有必要先進行一次序列化,然后再進行存儲。而序列化是很消耗CPU的。
代碼2問題:
代碼2實現(xiàn)的是一個在線聊天室,聊天室本身含有訪客,發(fā)言等內(nèi)容。在發(fā)言時,對聊天室內(nèi)容進行判斷,只顯示最近30條。新進來訪客直接加到訪客別表中。表面上是沒什么問題的。但是細想之下有兩個問題:
1 聊天室類設計的比較復雜,每次從Memcached服務端取得數(shù)據(jù)后,都要進行類型轉(zhuǎn)換。
2 沒有訪客清理機制。隨著訪客的不斷進入,對象的體積會不斷增大。
對存疑部分編寫了代碼進行測試。測試結(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人次(當天一共有1000人訪問,數(shù)據(jù)來源于運營檢測),留言內(nèi)容為30條時,Room體積大概為:57K
直播1000人次(當天一共有8000人訪問,數(shù)據(jù)來源于運營檢測),留言內(nèi)容為30條時,Room體積大概為:350k
根據(jù)圖表可以看到以下情況:處理時間、CPU利用率和數(shù)據(jù)量大小,序列化,類復雜性都有關(guān)系。
序列化問題(類型轉(zhuǎn)換)對性能影響最為明顯(可在場景”json1k”、場景直播中看到)。在Json1k中,存儲對象和前幾個場景是相同的,處理時間也相差不大,較大區(qū)別是CPU利用率由5%左右增長到40%左右(反序列化時尤為明顯)。在場景直播系統(tǒng)中,不存在序列化問題,但是其對象屬性中存在”訪客”, ”繁衍”等多個復雜對象,造成其在處理時需要處理過多的類型轉(zhuǎn)換,同時其體積不斷增大。
存儲對象的大小和處理時間存在一定關(guān)系,例如場景”500k”,其處理時間增長,但是其CPU利用率并未提高,其時間增長是由于對象傳輸造成。
本地緩存在內(nèi)存中進行尋址和類型轉(zhuǎn)換,涉及不到Socket連接,網(wǎng)絡傳輸,序列化操作,所以其處理相當快。
就測試結(jié)果看:
本地緩存性能大約是分布式緩存性能的100倍左右。而出問題的聊天室除了CPU增高以外,其性能更比分布式緩存再降低40倍(直播1000人次)到200倍(直播8000人次)。綜合來看,聊天室的分布式緩存比本地緩存降了4000倍,甚至更多。
但是,還沒有完。
對于第二個問題,更改類設計,清楚無效訪客,即可解決。
但是第一個問題,為什么用戶在存儲之前,先進行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時,是不能被二進制序列化的,因為其User屬性類型為IUser,為一個接口。因此想了一個解決方法,即先將Game對象進行 json序列化將其變?yōu)樽址缓髮⒆址鎯Φ組emcached。
原來是這樣。
接著又查看了MemcachedClient源代碼,其需要將對象進行二進制序列化,然后進行存儲。接口屬性不能被序列化,遂又對序列化問題進行了測試(見附件)。測試結(jié)果顯示上述代碼直接進行二進制序列化是可以的,同時直接使用第三方客戶端也是可以可行的。
問題出在哪?難道是沒有加[Serializable]。
一查果然:一個Serializable引發(fā)的血案。
記得有人說過,慎用分布式,能不用盡量不用。
一方面在性能上確實下降很多,分布式存儲主要性能消耗在以下幾個方面:協(xié)議解析,Socket連接,數(shù)據(jù)傳輸,序列化/類型轉(zhuǎn)換。
一方面在使用場景和類設計上要求也更加嚴格。個人認為Memcached是不太適合存儲特別大的文件的。雖然有人說網(wǎng)上已經(jīng)有用來存儲視頻的。
還有幾個問題希望知道的朋友回答下:
1 有沒有.Net方面的Memcached客戶端支持二進制協(xié)議和一致性的?
2 測試中發(fā)現(xiàn),當Memcached設置緩存過小時(例如64M),當其內(nèi)存使用已經(jīng)到62M時,再進行存儲,新存儲的內(nèi)容再取出來就是空值,不知道是什么原因。
網(wǎng)頁題目:關(guān)于Memcached客戶端CPU過高問題的排查
分享路徑:http://www.dlmjj.cn/article/dhshghe.html


咨詢
建站咨詢
