一例併發導致網站cpu 佔用100% 的處理
阿新 • • 發佈:2019-02-18
最近在早上更新公司網站的客戶管理系統的程式程式碼,更新過後對網站的頁面進行了簡單的訪問測試,都正常,沒有問題。就沒在管。可是過了會,有人反映網站開啟很慢。我於是趕快檢查,確實很慢,半天打不開。然後用遠端桌面開啟伺服器,也是很慢,過了好一會才進到伺服器,開啟工作管理員,發覺客戶管理系統所在的程序w3wp.exe cpu佔用高達100% 。觀察了一會,一直如此,因為並不清楚問題所在,但是這個問題必須馬上解決。嘗試結束此程序,結束後cpu 佔用馬上下來了。這個時候系統馬上重新開啟了一個客戶管理系統所在網站的新程序。此時再訪問客戶管理系統網站,一切都正常。觀察了一段時間,不再出現cpu 佔用100% 的問題。
在windows 事件檢視器中檢視應用程式日誌,發現了sqlserver大量超時資訊。這說明不了什麼,因為w3wp.exe 把cpu都佔用了,自然超時。其他的就是發現了另一個網站 有關redis的操作出現了錯誤。難道是這裡的問題?可又覺得不像,一個網站出現的問題怎麼會影響另一個,又不是在一個程序中。只有先放放了。
第二天上午,由於客服管理系統有新內容更新,再次更新,這次更新過後,同樣的問題又出現了,結束程序後,此問題又消失。看了windows 事件日誌,還是和昨天差不多的樣子。這次我查了下錯誤日誌,發覺大致有3個錯誤比較多。第一個:伺服器無法在已傳送 HTTP 標頭之後設定狀態,因為這個錯誤其他網站MVC類的網站也有,應該不是cpu 佔用100% 導致的。第二個錯誤是:
已添加了具有相同鍵的項 ,同樣是在訪問/service/GetReviewMode 出現的。也沒看出什麼問題。決定再放一放看。
第三天上午,再次更新網站,同樣的問題又出現了。結束程序,問題消失。但這次卻不能掉以輕心,第三次出現,而且是因為在更新時出現,前兩次還可以解釋為湊巧,這次卻再不能這樣解釋了。而我看了錯誤日誌,仍然是訪問/service/GetReviewMode 導致兩個錯誤,一個是未將物件引用設定到物件的例項,另一個是已添加了具有相同鍵的項。這次我仔細看了這兩個錯誤的堆疊提示。
其中未將物件引用設定為物件例項的有兩種,第一種:
在 SXF.Utils.EnumDescription.GetFieldTexts(Type enumType, SortType sortType) 在 SXF.Utils.EnumDescription.GetFieldTexts(Type enumType) 在 SXF.Utils.EnumDescription.GetFieldText(Object enumValue) 在 LMSoft.Web.Controllers.ServiceController.GetReviewMode(Int32 status)第二種:
在 System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add) 在 SXF.Utils.EnumDescription.GetFieldTexts(Type enumType, SortType sortType) 在 SXF.Utils.EnumDescription.GetFieldTexts(Type enumType) 在 SXF.Utils.EnumDescription.GetFieldText(Object enumValue) 在 LMSoft.Web.Controllers.ServiceController.GetReviewMode(Int32 status)而已添加了具有相同鍵的項的堆疊提示是:
在 System.ThrowHelper.ThrowArgumentException(ExceptionResource resource) 在 System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add) 在 SXF.Utils.EnumDescription.GetFieldTexts(Type enumType, SortType sortType) 在 SXF.Utils.EnumDescription.GetFieldTexts(Type enumType) 在 SXF.Utils.EnumDescription.GetFieldText(Object enumValue) 在 LMSoft.Web.Controllers.ServiceController.GetReviewMode(Int32 status)
這三個錯誤都指向了一個action :GetReviewMode的同一個方法:GetFieldText ,難道這個方法有問題?於是百度,並沒有搜尋到可用的資訊。於是我搜索:
System.Collections.Generic.Dictionary`2.Insert, 這次搜尋到一篇文章,介紹說在單執行緒的時候C# dictionary插入資料,不會出現問題,多執行緒的時候會出現未將物件引用設定為物件例項的錯誤。我這個方法確實是向dictionary中插入資料了,難道是這裡的問題?於是我進行分析,此action 是我在分頁中用ajax呼叫的,用來顯示回訪狀態,並且每頁顯示10行,在載入的時候要呼叫10次,如果公司有多個人同時訪問這個列表頁面,確實可能導致併發問題。於是我分析GetFieldText 方法程式碼:
/// <summary> /// 獲得指定列舉型別中,指定值的描述文字 /// </summary> /// <param name="enumValue">列舉值,不要作任何型別轉換</param> /// <returns>描述字串</returns> public static string GetFieldText(object enumValue) { List<EnumDescription> fieldTexts = GetFieldTexts(enumValue.GetType()) as List<EnumDescription>; if (CollectionHelper.IsNullOrEmpty<EnumDescription>(fieldTexts)) { return string.Empty; } EnumDescription description = fieldTexts.Find(item => item.m_fieldIno.Name.Equals(enumValue.ToString())); if (description == null) { return string.Empty; } return description.Description; }
這個方法呼叫了GetFieldTexts 方法,而從堆疊輸出上看,就是在這裡出錯的,此方法的原始碼是這樣寫的:
/// <summary> /// 獲取列舉型別定義的所有文字,按定義的順序返回 /// </summary> /// <param name="enumType">列舉型別</param> /// <returns>所有定義的文字</returns> public static IList<EnumDescription> GetFieldTexts(Type enumType) { return GetFieldTexts(enumType, SortType.Default); } /// <summary> /// 獲取列舉型別定義的所有文字 /// </summary> /// <param name="enumType">列舉型別</param> /// <param name="sortType">排序型別</param> /// <returns>列舉描述集合</returns> public static IList<EnumDescription> GetFieldTexts(Type enumType, SortType sortType) { if (!EnumDescriptionCache.ContainsKey(enumType.FullName)) { FieldInfo[] fields = enumType.GetFields(); IList<EnumDescription> list = new List<EnumDescription>(); foreach (FieldInfo info in fields) { object[] customAttributes = info.GetCustomAttributes(typeof(EnumDescription), false); if (customAttributes.Length == 1) { EnumDescription item = (EnumDescription) customAttributes[0]; item.m_fieldIno = info; list.Add(item); } } EnumDescriptionCache.Add(enumType.FullName, list); } IList<EnumDescription> list2 = EnumDescriptionCache[enumType.FullName]; if (list2.Count <= 0) { throw new NotSupportedException("列舉型別[" + enumType.Name + "]未定義屬性EnumValueDescription"); } if (sortType != SortType.Default) { for (int i = 0; i < list2.Count; i++) { for (int j = i; j < list2.Count; j++) { bool flag = false; switch (sortType) { case SortType.DisplayText: if (string.Compare(list2[i].Description, list2[j].Description) > 0) { flag = true; } break; case SortType.Rank: if (list2[i].EnumRank > list2[j].EnumRank) { flag = true; } break; } if (flag) { EnumDescription description2 = list2[i]; list2[i] = list2[j]; list2[j] = description2; } } } } return list2; }
這裡先判斷:
if (!EnumDescriptionCache.ContainsKey(enumType.FullName))
然後在不包含的情況下,經過一系列處理插入:
EnumDescriptionCache.Add(enumType.FullName, list);
單執行緒下這樣是沒問題的,但是如果是多執行緒,則這樣處理,沒有鎖定 EnumDescriptionCache 物件,卻是可以導致重複插入的。我看了下 EnumDescriptionCache 的定義,此變數是靜態的。private static IDictionary<string, IList<EnumDescription>> EnumDescriptionCache = new Dictionary<string, IList<EnumDescription>>();
我們知道一個類的靜態變數,是可以在不同執行緒間公用的。到這裡,幾乎可以確認就是這裡的問題了。而為什麼會在網站更新後才會出現這個問題,那是因為,靜態變數在初始化後在整個應用程式域中一直存在,一旦dll 檔案更新,才會重新初始化。知道是這裡的問題,因此對GetFieldTexts 方法在更新 EnumDescriptionCache 靜態變數時使用鎖來進行處理,防止在處理工程中其他使用者改變這個靜態變數的值。程式碼如下:
/// <summary> /// 獲取列舉型別定義的所有文字 /// </summary> /// <param name="enumType">列舉型別</param> /// <param name="sortType">排序型別</param> /// <returns>列舉描述集合</returns> public static IList<EnumDescription> GetFieldTexts(Type enumType, SortType sortType) { Monitor.Enter(EnumDescriptionCache); try { if (!EnumDescriptionCache.ContainsKey(enumType.FullName)) { FieldInfo[] fields = enumType.GetFields(); IList<EnumDescription> list = new List<EnumDescription>(); foreach (FieldInfo info in fields) { object[] customAttributes = info.GetCustomAttributes(typeof(EnumDescription), false); if (customAttributes.Length == 1) { EnumDescription item = (EnumDescription)customAttributes[0]; item.m_fieldIno = info; list.Add(item); } } EnumDescriptionCache.Add(enumType.FullName, list); } IList<EnumDescription> list2 = EnumDescriptionCache[enumType.FullName]; if (list2.Count <= 0) { throw new NotSupportedException("列舉型別[" + enumType.Name + "]未定義屬性EnumValueDescription"); } if (sortType != SortType.Default) { for (int i = 0; i < list2.Count; i++) { for (int j = i; j < list2.Count; j++) { bool flag = false; switch (sortType) { case SortType.DisplayText: if (string.Compare(list2[i].Description, list2[j].Description, StringComparison.Ordinal) > 0) { flag = true; } break; case SortType.Rank: if (list2[i].EnumRank > list2[j].EnumRank) { flag = true; } break; } if (flag) { EnumDescription description2 = list2[i]; list2[i] = list2[j]; list2[j] = description2; } } } } return list2; } finally { Monitor.Exit(EnumDescriptionCache); } }
在try 之前加入了Monitor.Enter(EnumDescriptionCache); 進行加鎖,在finally 中進行解鎖:Monitor.Exit(EnumDescriptionCache); 至於為什麼不使用 lock 進行加鎖和解鎖,是因為我看到一篇文章介紹 Lock 與 Monitor 的區別,解釋說:Lock關鍵字實際上是一個語法糖,它將Monitor物件進行封裝,給object加上一個互斥鎖 。也就是說 Lock 實際最終使用的仍然是Monitor 。更改程式碼後在本地進行了測試,然後上傳到伺服器,對伺服器進行監控,這次更新程式碼,未出現客戶管理系統網站cpu 佔用 100% 的情況。上線,經過兩天觀察,沒有再出現cpu 佔用100% 的情況,可以肯定這個問題就是此方法中靜態變數在修改的時候沒有考慮到併發情況導致
本篇文章參考的資料:
http://www.cnblogs.com/chengxingliang/p/3150731.html
http://blog.csdn.net/gaobobo138968/article/details/43672785