摘要:
當一個.net應用在生產環境CPU突然居高不下,如何快速準確的定位問題所在,並且對實時業務影響最小化?如何不抓Dump也不用live debug就可以知道你的應用在做什麼?如何確認你的應用是由於哪個線程的執行造成的CPU升高,該線程正在執行什麼代碼?
分析:
CPU升高的原因有很多,
1、有時候應用的負載大了,CPU自然會受業務請求的增加和增高;
2、有時候因爲GC回收使用了過高的CPU資源;
3、有時候是某個線程執行的代碼在某種情況下陷入了死循環;
4、有時候是因爲鎖爭用太激烈,某資源上的鎖釋放後,等待的線程去搶鎖引起的;
5、有時候是因爲線程太多,上下文切換太頻繁引起的。
6、每秒拋出太多的Exception。
我們一一分析
1、我們一般會用一些計數器來觀察實際的應用的負載情況和併發請求量,比如每秒接受多少請求等,所以業務量增大引起的CPU高,很容易確定。
2、GC使用的CPU百分比有專門的計數器,一看便知。
3、如果某段代碼陷入了死循環引起的CPU高,只抓Dump看~*e!clrstack和!runaway還是不太好定位問題,
a)、一般都是連續抓幾個dump,然後用!runaway來看哪些線程的用戶態時間的差很大,然後再去看該線程的調用棧。
b)、錄製Thread/Thread Id和Thread/% Processor Time計數器,同時抓dump,從計數器裏找到CPU消耗高的線程ID,然後從dump裏看調用棧和調用棧的參數本地變量等。
4、鎖爭用也有相關的.NET計數器可以直接看。
5、每個進程的線程數,每秒上下文切換次數也可以有直接的計數器可看。
6、每秒拋出的異常也有直接的計數器可看。
思路:
1、從上面看到也就是第3種地排查比較費勁,而且抓DUMP有時候容易把服務抓S,如果是一個有狀態的服務,抓死服務後果很嚴重,所以我們得想出一種更輕量級的方法去獲取服務的每個線程的調用棧。其實CLR本身有一些用於支持調試的接口,都是Com的,但.NET對此有一些包裝,可以用c#來使用這些調試API,其中當然包括附加到進程,獲取所有線程調用棧的功能。該DLL在.net sdk裏,叫MdbgCore.dll。
2、另外獲取計數器.NET也有現成的類,上篇帖子介紹過了。
3、.NET對進程的管理也有一些API,可以獲取一個進程的線程數,每個線程的啓動時間,用戶態時間,線程狀態,優先級等信息。
有了以上幾個知識點,我們就可以綜合起來寫一個比較智能化定位高CPU問題的工具。
CPU高的DEMO
我們先寫一個CPU高的DEMO,A方法因爲有sleep所以不會太消耗CPU,而B方法沒有Sleep,執行一個浮點運算,所以會造成CPU升高(佔用一個CPU的資源)。
using System;
using System.Threading;
namespace HightCPUDemo
{
internal class Program
{
private static void Main(string[] args)
{
new Thread(A).Start();
new Thread(B).Start();
Console.ReadKey();
}
private static void A(object state)
{
while (true)
{
Thread.Sleep(1000);
}
}
private static void B(object state)
{
while (true)
{
double d = new Random().NextDouble()*new Random().NextDouble();
}
}
}
}
代碼實現
我們的目標在該程序運行的時候,找出B方法,並確認它就是引起CPU高度原因,代碼如下,不太想解釋了,代碼不復雜,重在思路。
完整代碼實現
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using Microsoft.Samples.Debugging.MdbgEngine;
internal class MyThreadInfo
{
public string CallStack = "null";
public string Id;
public string ProcessorTimePercentage;
public string StartTime;
public string UserProcessorTime;
public override string ToString()
{
return
string.Format(
@"<table style=""width: 1000px;""><tr><td style=""width: 80px;"">ThreadId</td><td style=""width: 200px;"">{0}</td><td style=""width: 140px;"">% Processor Time</td><td>{1}</td></tr>
<tr><td style=""width: 80px;"">UserProcessorTime</td><td style=""width: 200px;"">{2}</td><td style=""width: 140px;"">StartTime</td><td>{3}</td></tr><tr><td colspan=""4"">{4}</td></tr></table>",
Id, ProcessorTimePercentage, UserProcessorTime, StartTime, CallStack);
}
}
internal class MyThreadCounterInfo
{
public PerformanceCounter IdCounter;
public PerformanceCounter ProcessorTimeCounter;
public MyThreadCounterInfo(PerformanceCounter counter1, PerformanceCounter counter2)
{
IdCounter = counter1;
ProcessorTimeCounter = counter2;
}
}
internal class Program
{
// Skip past fake attach events.
private static void DrainAttach(MDbgEngine debugger, MDbgProcess proc)
{
bool fOldStatus = debugger.Options.StopOnNewThread;
debugger.Options.StopOnNewThread = false; // skip while waiting for AttachComplete
proc.Go().WaitOne();
Debug.Assert(proc.StopReason is AttachCompleteStopReason);
debugger.Options.StopOnNewThread = true; // needed for attach= true; // needed for attach
// Drain the rest of the thread create events.
while (proc.CorProcess.HasQueuedCallbacks(null))
{
proc.Go().WaitOne();
Debug.Assert(proc.StopReason is ThreadCreatedStopReason);
}
debugger.Options.StopOnNewThread = fOldStatus;
}
// Expects 1 arg, the pid as a decimal string
private static void Main(string[] args)
{
try
{
int pid = int.Parse(args[0]);
var sb = new StringBuilder();
Process process = Process.GetProcessById(pid);
var counters = new Dictionary<string, MyThreadCounterInfo>();
var threadInfos = new Dictionary<string, MyThreadInfo>();
sb.AppendFormat(
@"<html><head><title>{0}</title><style type=""text/css"">table, td{{border: 1px solid #000;border-collapse: collapse;}}</style></head><body>",
process.ProcessName);
Console.WriteLine("1、正在收集計數器");
var cate = new PerformanceCounterCategory("Thread");
string[] instances = cate.GetInstanceNames();
foreach (string instance in instances)
{
if (instance.StartsWith(process.ProcessName, StringComparison.CurrentCultureIgnoreCase))
{
var counter1 =
new PerformanceCounter("Thread", "ID Thread", instance, true);
var counter2 =
new PerformanceCounter("Thread", "% Processor Time", instance, true);
counters.Add(instance, new MyThreadCounterInfo(counter1, counter2));
}
}
foreach (var pair in counters)
{
pair.Value.IdCounter.NextValue();
pair.Value.ProcessorTimeCounter.NextValue();
}
Thread.Sleep(1000);
foreach (var pair in counters)
{
try
{
var info = new MyThreadInfo();
info.Id = pair.Value.IdCounter.NextValue().ToString();
info.ProcessorTimePercentage = pair.Value.ProcessorTimeCounter.NextValue().ToString("0.0");
threadInfos.Add(info.Id, info);
}
catch
{
}
}
Console.WriteLine("2、正在收集線程信息");
ProcessThreadCollection collection = process.Threads;
foreach (ProcessThread thread in collection)
{
try
{
MyThreadInfo info;
if (threadInfos.TryGetValue(thread.Id.ToString(), out info))
{
info.UserProcessorTime = thread.UserProcessorTime.ToString();
info.StartTime = thread.StartTime.ToString();
}
}
catch
{
}
}
var debugger = new MDbgEngine();
MDbgProcess proc = null;
try
{
proc = debugger.Attach(pid);
DrainAttach(debugger, proc);
MDbgThreadCollection tc = proc.Threads;
Console.WriteLine("3、正在附加到進程{0}獲取調用棧", pid);
foreach (MDbgThread t in tc)
{
var tempStrs = new StringBuilder();
foreach (MDbgFrame f in t.Frames)
{
tempStrs.AppendFormat("<br />" + f);
}
MyThreadInfo info;
if (threadInfos.TryGetValue(t.Id.ToString(), out info))
{
info.CallStack = tempStrs.Length == 0 ? "no managment call stack" : tempStrs.ToString();
}
}
}
finally
{
if (proc != null)
{
proc.Detach().WaitOne();
}
}
foreach (var info in threadInfos)
{
sb.Append(info.Value.ToString());
sb.Append("<hr />");
}
sb.Append("</body></html>");
Console.WriteLine("4、正在生成報表");
using (var sw = new StreamWriter(process.ProcessName + ".htm", false,
Encoding.Default))
{
sw.Write(sb.ToString());
}
Process.Start(process.ProcessName + ".htm");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
單元測試
找出HeightCPUDemo的進程ID,比如是8724,然後執行PrintStack.exe 8724,輸出結果如下
E:/study/ThreadStack/PrintStack/bin/Debug>PrintStack.exe 8724
1、正在收集計數器...
2、正在收集線程信息...
3、正在附加到進程8724獲取調用棧...
4、正在生成報表...
最終會在當前目錄生成一個HightCPUDemo.htm的報表,其中哪個線程耗費了很多的CPU,及其託管調用棧一目瞭然,很快就能定位問題。
ThreadId |
10280 |
% Processor Time |
97.1 |
UserProcessorTime |
00:00:20.2187500 |
StartTime |
2009-6-24 21:58:19 |
System.Random.Sample (source line information unavailable) System.Random.NextDouble (source line information unavailable) HightCPUDemo.Program.B (Program.cs:27) System.Threading.ThreadHelper.ThreadStart_Context (source line information unavailable) System.Threading.ExecutionContext.Run (source line information unavailable) System.Threading.ThreadHelper.ThreadStart (source line information unavailable) |
參考鏈接
How the .NET Debugger Works
http://www.developerfusion.com/article/4692/how-the-net-debugger-works/
Working on managed wrappers for Native Debugging API
http://blogs.msdn.com/jmstall/archive/2006/07/05/managed-wrappers-for-native-debug-api.aspx
Runtime Call Stack Analysis with .NET
http://www.ddj.com/184405715
MDbg Linkfest
http://blogs.msdn.com/jmstall/archive/2005/11/08/mdbg_linkfest.aspx
Tool to get snapshot of managed callstacks
http://blogs.msdn.com/jmstall/archive/2005/11/28/snapshot.aspx