记一次 .NET 某云采购平台API 挂死分析

一:背景

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就是该例子的最好佐证!????????????

END

工作中的你,是否已遇到 ... 

1. CPU爆高

2. 内存暴涨

3. 资源泄漏

4. 崩溃死锁

5. 程序呆滞

等紧急事件,全公司都指望着你能解决...  危难时刻才能展现你的技术价值,作为专注于.NET高级调试的技术博主,欢迎微信搜索: 一线码农聊技术,免费协助你分析Dump文件,希望我能将你的踩坑经验分享给更多的人。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值