ETW 是 Windows 上真正低开销的事件采集机制
ETW(Event Tracing for Windows)不是 .NET 专属,而是 Windows 内核级的高性能事件追踪子系统。它用内核缓冲区 + 环形队列 + 延迟写入设计,
EnableTrace开启后,单次事件写入通常仅需 Console.WriteLine 或日志库的毫秒级开销。关键在于:它不依赖托管堆分配、不触发 GC、不走 .NET 日志管道。
实际使用中,
EventSource类只是 ETW 的 .NET 封装层,它在运行时自动生成 ETW provider GUID,并把
WriteEvent调用翻译为
EventWriteTransfer等原生 ETW API。这意味着你写的
EventSource代码,最终跑的是 Windows 原生事件路径。
常见误区是认为“加了
[EventSource(Name = "MyApp")]就自动高性能”——其实不然。如果在事件方法里做了字符串拼接、对象序列化、或调用了
ToString(),这些操作仍在用户态执行,会显著拖慢吞吐。性能收益只在“事件写入”环节,前置计算仍由你负责。
EventSource.WriteEvent 的参数传递必须是原始类型或结构体
EventSource不支持任意对象序列化。它只接受
int、
string、
Guid、
DateTime、
long、枚举、以及标记
[EventData]的简单
struct。传入 class 实例、
Dictionary<string object></string>或匿名类型会直接抛出
ArgumentException:“The event field type is not supported.”
典型错误写法:
public void LogRequest(HttpRequest req) {
WriteEvent(1, req.Path, req.Method); // ❌ req.Path 可能是 null 或复杂属性链
}
正确做法是提前提取值,且避免空引用:
用req?.Path ?? "(null)"替代
req.Path不要传
req.Headers,而应传
req.Headers.Count或预提取关键 header 值 若需结构化数据,定义轻量
struct并用
[NonEvent]方法做转换
用 PerfView 捕获 EventSource 事件时要注意 Provider 名称匹配
PerfView 默认只收集已知 provider(如
Microsoft-Windows-DotNETRuntime),你自定义的
EventSource必须显式启用。Provider 名称默认是类名全限定名,但可通过构造函数覆盖:
public sealed class MyEventSource : EventSource
{
public static MyEventSource Log = new MyEventSource();
private MyEventSource() : base("MyCompany-MyApp") { } // ✅ 显式指定名称
}
启动 PerfView 时,必须在 “Collect → Additional Providers” 中填入:
MyCompany-MyApp:0x10000:5(其中
0x10000是 Level=Verbose,
5是 Keyword=All)。漏掉冒号或关键字位会导致事件完全不出现。
另一个常见问题:程序启动后才打开 PerfView,会错过初始化阶段的事件(如
EventSource自身的
ManifestData事件)。建议先在 PerfView 中点击 “Collect”,再启动目标进程。
高频率场景下必须用 EventSourceMessageAttribute 控制字段裁剪
当每秒写入数千次事件时,即使参数是原始类型,字符串字段仍可能成为瓶颈。ETW 对每个事件的大小有限制(默认约 64KB),但更现实的瓶颈是内存拷贝和内核缓冲区竞争。
[EventSourceMessage]不是装饰用的——它让编译器在生成 manifest 时把字段标记为可选(
eventFieldAttr="Optional"),配合 PerfView 的 “Filter Events” 或
TraceEvent库的
Filter,可在采集时跳过未启用的字段,减少序列化开销。
例如:
[Event(1, Level = EventLevel.Informational)]
public void RequestStarted(
string path,
[EventSourceMessage] string userAgent, // ✅ 可被过滤掉
int statusCode)
{
WriteEvent(1, path, userAgent, statusCode);
}
这样在低开销采集中,可只保留
path和
statusCode,彻底跳过
userAgent字符串的复制与写入。
真正容易被忽略的是:这种裁剪只在 ETW 层生效;如果你用
EventListener在进程内监听,所有字段仍会传入,裁剪无效。所以生产环境高频打点,务必搭配外部工具(PerfView / Windows Performance Recorder)采集,而非依赖进程内监听。
