第二章:性能计数器与监控
性能计数器这东西,说白了就是系统的「体检报告」。我刚做.NET性能调优那会儿,总觉得靠直觉就能找到瓶颈。结果呢?被现实狠狠教育了一顿。后来我养成了一个习惯——先看计数器,再动手改代码。
这一章,我们就来聊聊Windows性能计数器、.NET CLR专有的那些计数器,以及怎么自己动手造一个自定义计数器。
2.1 Windows性能计数器:系统级「听诊器」
Windows自带了一套非常完整的性能计数器体系。你想想看,CPU忙不忙、内存够不够、磁盘在不在狂转,这些都能通过计数器看到。
我个人最常用的几个系统计数器:
- Processor\% Processor Time:CPU使用率。如果持续超过80%,你得小心了。
- Memory\Available MBytes:可用内存。低于几百MB时,GC会频繁触发。
- PhysicalDisk\Avg. Disk Queue Length:磁盘队列长度。超过2说明磁盘是瓶颈。
- Network Interface\Bytes Total/sec:网络吞吐量。看你的应用是不是在疯狂收发数据。
我记得有一次线上事故,接口响应突然变慢。我第一反应是代码有bug,结果一看磁盘队列长度飙到了15。原来是日志库在同步写文件,把磁盘IO打满了。换成异步写入后,问题立刻解决。
2.2 .NET CLR性能计数器:深入运行时内部
系统级计数器只能看到「表象」。要真正理解.NET应用的运行时行为,你得看CLR专属计数器。这些计数器藏在「.NET CLR Memory」、「.NET CLR Exceptions」等类别下面。
我重点讲几个最关键的:
| 计数器类别 | 计数器名称 | 说明 |
|---|---|---|
| .NET CLR Memory | # Gen 0 Collections | 第0代GC次数。频繁触发说明小对象分配太多。 |
| .NET CLR Memory | % Time in GC | GC占用的CPU时间百分比。超过10%就要优化内存了。 |
| .NET CLR Memory | Large Object Heap size | 大对象堆大小。持续增长说明有大对象泄漏。 |
| .NET CLR Exceptions | # of Exceps Thrown | 每秒抛出的异常数。异常很昂贵,别拿它做流程控制。 |
| .NET CLR LocksAndThreads | Contention Rate / sec | 锁竞争频率。高并发下这个值高了,说明锁设计有问题。 |
为什么会这样?因为字符串是不可变的。你每次用「+」拼接,都在堆上创建一个新字符串。这些小对象很快填满第0代,触发GC。嗯,这里要注意——GC不是免费的,它要暂停所有线程。
2.3 自定义性能计数器:造自己的「专属仪表」
系统计数器和CLR计数器虽然强大,但有时候不够用。比如你想统计「每秒处理了多少订单」、「缓存命中率是多少」。这时候就得自己动手了。
.NET 提供了 System.Diagnostics.PerformanceCounter 类,可以创建和读写自定义计数器。我一般分三步走:
- 创建类别和计数器(通常在应用启动时做一次)
- 在业务代码中更新计数器值
- 用PerfMon或其他工具读取
来看一个实际例子。假设我们要统计「订单处理速率」:
// 第一步:创建计数器类别(需要管理员权限,通常安装时做)
if (!PerformanceCounterCategory.Exists("MyApp"))
{
var counters = new CounterCreationDataCollection();
// 速率计数器
var rateCounter = new CounterCreationData
{
CounterName = "订单处理速率",
CounterType = PerformanceCounterType.RateOfCountsPerSecond32
};
counters.Add(rateCounter);
// 总数计数器
var totalCounter = new CounterCreationData
{
CounterName = "订单总数",
CounterType = PerformanceCounterType.NumberOfItems32
};
counters.Add(totalCounter);
PerformanceCounterCategory.Create("MyApp", "我的应用性能计数器",
PerformanceCounterCategoryType.SingleInstance, counters);
}
// 第二步:在业务代码中更新
var ratePerfCounter = new PerformanceCounter("MyApp", "订单处理速率", false);
var totalPerfCounter = new PerformanceCounter("MyApp", "订单总数", false);
// 每处理一个订单就调用
void ProcessOrder(Order order)
{
// ... 处理订单逻辑 ...
ratePerfCounter.Increment(); // 速率计数器自动计算每秒次数
totalPerfCounter.Increment(); // 总数加1
}
我曾经在一个高并发网关项目里,每个请求都更新计数器,结果发现计数器写入成了新的瓶颈。后来改成每5秒汇总一次,性能开销几乎可以忽略不计。
2.4 如何高效采集与分析
光有计数器还不够,你得知道怎么采集和分析。我常用的方式有三种:
- Performance Monitor (PerfMon):Windows自带,适合临时排查。可以设置数据收集器集,记录到日志文件。
- dotnet-counters:.NET Core/5+ 的跨平台工具。命令行下就能看,非常轻量。
- 接入APM系统:比如SkyWalking、Prometheus + Grafana。适合生产环境长期监控。
我个人习惯是:开发环境用PerfMon快速定位,测试环境用dotnet-counters做压力测试分析,生产环境必须上APM系统。
嗯,这一章的内容就到这里。记住一句话:没有数据支撑的性能优化,都是瞎猜。计数器就是你的眼睛,用好它们,你就能看到系统最真实的样子。