一:背景
1. 講故事
大概有兩個月沒寫部落格了,關注我的朋友應該知道我最近都把精力花在了星球,這兩個月時間也陸陸續續的有朋友求助如何分析dump,有些朋友太客氣了,給了大大的紅包,哈哈?,手裡面也攢了10多個不同問題型別的dump,後續也會逐一將分析思路貢獻出來。
這個dump是一位朋友大概一個月前提供給我的,由於wx裡面求助的朋友比較多,一時也沒找到相關截圖,不得已破壞一下老規矩。???
既然朋友說api介面無響應,呈現了hangon現象,從一些過往經驗看,大概也只有三種情況。
-
大量鎖等待
-
執行緒不夠用
-
死鎖
有了這種先入為主的思想,那就上windbg說事唄。
二: windbg 分析
1. 有大量鎖等待嗎?
要想看是否鎖等待,老規矩,看一下 同步塊表
。
0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
-----------------------------
Total 1673
CCW 3
RCW 4
ComClassFactory 0
Free 397
撲了個空,啥也沒有,那就暴力看看所有的執行緒棧吧。
不看還好,一看嚇一跳,有339個執行緒卡在了 System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)
處,不過轉念一想,就算有339個執行緒卡在這裡,真的會導致程式hangon嗎? 也不一定,畢竟我看過有1000+的執行緒也不會卡死,只不過cpu爆高而已,接下來繼續研判一下是不是執行緒不夠用導致,可以從 執行緒池任務佇列
上面入手。
2. 探究執行緒池佇列
可以用 !tp
命令檢視。
0:000> !tp
CPU utilization: 10%
Worker Thread: Total: 328 Running: 328 Idle: 0 MaxLimit: 32767 MinLimit: 4
Work Request in Queue: 74
Unknown Function: 00007ffe91cc17d0 Context: 000001938b5d8d98
Unknown Function: 00007ffe91cc17d0 Context: 000001938b540238
Unknown Function: 00007ffe91cc17d0 Context: 000001938b5eec08
...
Unknown Function: 00007ffe91cc17d0 Context: 0000019390552948
Unknown Function: 00007ffe91cc17d0 Context: 0000019390562398
Unknown Function: 00007ffe91cc17d0 Context: 0000019390555b30
--------------------------------------
Number of Timers: 0
--------------------------------------
Completion Port Thread:Total: 5 Free: 4 MaxFree: 8 CurrentLimit: 4 MaxLimit: 1000 MinLimit: 4
從輸出資訊看,執行緒池中328個執行緒全部打滿,工作佇列中還有74位客人在等待,綜合這兩點資訊就已經很清楚了,本次hangon是由於大量的客人到來超出了執行緒池的接待能力所致。
3. 接待能力真的不行嗎?
這個標題我覺得很好,真的不行嗎? 到底行不行,可以從兩點入手:
-
是不是程式碼寫的爛?
-
qps是不是真的超出了接待能力?
要想找出答案,還得從那 339 個卡死的執行緒說起,仔細研究了下每一個執行緒的呼叫棧,大概卡死在這三個地方。
<1>. GetModel
public static T GetModel<T, K>(string url, K content)
{
T result = default(T);
HttpClientHandler httpClientHandler = new HttpClientHandler();
httpClientHandler.AutomaticDecompression = DecompressionMethods.GZip;
HttpClientHandler handler = httpClientHandler;
using (HttpClient httpClient = new HttpClient(handler))
{
string content2 = JsonConvert.SerializeObject((object)content);
HttpContent httpContent = new StringContent(content2);
httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
string mD5ByCrypt = Md5.GetMD5ByCrypt(ConfigurationManager.AppSettings["SsoToken"] + DateTime.Now.ToString("yyyyMMdd"));
httpClient.DefaultRequestHeaders.Add("token", mD5ByCrypt);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage result2 = httpClient.PostAsync(url, httpContent).Result;
if (result2.IsSuccessStatusCode)
{
string result3 = result2.Content.ReadAsStringAsync().Result;
return JsonConvert.DeserializeObject<T>(result3);
}
return result;
}
}
<2>. Get
public static T Get<T>(string url, string serviceModuleName)
{
try
{
T val3 = default(T);
HttpClient httpClient = TryGetClient(serviceModuleName, true);
using (HttpResponseMessage httpResponseMessage = httpClient.GetAsync(GetRelativeRquestUrl(url, serviceModuleName, true)).Result)
{
if (httpResponseMessage.IsSuccessStatusCode)
{
string result = httpResponseMessage.Content.ReadAsStringAsync().Result;
if (!string.IsNullOrEmpty(result))
{
val3 = JsonConvert.DeserializeObject<T>(result);
}
}
}
T val4 = val3;
val5 = val4;
return val5;
}
catch (Exception exception)
{
throw;
}
}
<3>. GetStreamByApi
public static Stream GetStreamByApi<T>(string url, T content)
{
Stream result = null;
HttpClientHandler httpClientHandler = new HttpClientHandler();
httpClientHandler.AutomaticDecompression = DecompressionMethods.GZip;
HttpClientHandler handler = httpClientHandler;
using (HttpClient httpClient = new HttpClient(handler))
{
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/octet-stream"));
string content2 = JsonConvert.SerializeObject((object)content);
HttpContent httpContent = new StringContent(content2);
httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
HttpResponseMessage result2 = httpClient.PostAsync(url, httpContent).Result;
if (result2.IsSuccessStatusCode)
{
result = result2.Content.ReadAsStreamAsync().Result;
}
httpContent.Dispose();
return result;
}
}
4. 尋找真相
上面我羅列的這三個方法的程式碼,不知道大家可看出什麼問題了? 對,就是 非同步方法同步化
,這種寫法本身就很低效,主要表現在2個方面。
-
開閉執行緒本身就是一個相對耗費資源和低效的操作。
-
頻繁的執行緒排程給了cpu巨大的壓力
而且這種寫法在請求量比較小的情況下還看不出什麼問題,一旦請求量稍大一些,馬上就會遇到該dump的這種情況。
三:總結
綜合來看這次hangon事故是由於開發人員 非同步方法不會非同步化
導致,改法很簡單,進行純非同步化改造 (await,async),解放呼叫執行緒,充分利用驅動裝置的能力。
這個dump也讓我想起了 CLR Via C#
書中(P646,647) 在講用 await,async 來改造 同步請求 的例子 。
我覺得這個dump就是該例子的最好佐證! ???
更多高質量乾貨:參見我的 GitHub: dotnetfly