news 2026/7/23 10:05:49

.NET无侵入自动化探针原理和主流实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
.NET无侵入自动化探针原理和主流实现

APM探针#
当我们提到 .NET 的 APM 时,许多人首先会想到 SkyWalking 。这是因为 SkyAPM-dotnet 是第一个支持.NET应用程序的开源非商业 APM 探针实现,目前很多 .NET 项目都采用了它。在此,我们要特别感谢刘浩杨等社区领袖的辛勤付出。

除了 SkyWalking 之外, Datadog APM 也是一款功能强大的商业应用性能监测工具,旨在帮助开发人员跟踪、优化并排查应用程序中的性能问题。Datadog APM 适用于多种编程语言和框架,包括 .NET 。通过使用 Datadog 丰富的功能和可视化仪表板,我们能够轻松地识别并改进性能瓶颈。

另一个比较知名的选择是 OpenTelemetry-dotnet-contrib ,这是 CNCF-OpenTelemetry 的 .NET 应用程序 APM 探针实现。虽然它的推出时间比 SkyAPM 和 Datadog APM 稍晚,但由于其开放的标准和开源的实现,许多 .NET 项目也选择使用它。

关于 APM 探针的实现原理,我们主要分为两类来介绍:平台相关指标和组件相关指标。接下来,我们将讨论如何采集这两类指标。

平台相关指标采集#
那么APM探针都是如何采集 .NET 平台相关指标呢?其实采集这些指标在 .NET 上是非常简单的,因为.NET提供了相关的API接口,我们可以直接获得这些指标,这里指的平台指标是如 CPU 占用率、线程数量、GC 次数等指标。

比如在 SkyAPM-dotne t项目中,我们可以查看 SkyApm.Core 项目中的 Common 文件夹,文件夹中就有诸如里面有 CPU 指标、GC 指标等平台相关指标采集实现帮助类。

同样,在 OpenTelemetry-dotnet-contrib 项目中,我们可以在 Process 和 Runtime 文件夹中,查看进程和运行时等平台相关指标采集的实现。

这些都是简单的 API 调用,有兴趣的同学可以自行查看代码,本文就不再赘述这些内容。

组件相关指标采集#
除了平台相关指标采集,还有组件相关的指标,这里所指的组件相关指标拿 ASP.NET Core 应用程序举例,我们接口秒并发是多少、一个请求执行了多久,在这个请求执行的时候访问了哪些中间件( Redis 、MySql 、Http 调用、RPC 等等),访问中间件时传递的参数(Redis 命令、Sql 语句、请求响应体等等)是什么,访问中间件花费了多少时间。

在 SkyAPM-dotnet 项目中,我们可以直接在src目录找到这些组件相关指标采集的实现代码。

同样在 OpenTelemetry-dotnet-contrib 项目中,我们也可以在src目录找到这些组件相关指标采集代码。

如果看过这两个APM探针实现的朋友应该都知道,组件指标采集是非常依赖DiagnosticSource技术。.NET官方社区一直推荐的的方式是组件开发者自己在组件的关键路径进行埋点,使用DiagnosticSource的方式将事件传播出去,然后其它监测软件工具可以订阅DiagnosticListener来获取组件运行状态。

就拿 ASP.NET Core 来举例,组件源码中有HostingApplicationDiagnostics.cs这样一个类,这个类中定义了 Hosting 在请求处理过程中的几个事件。

internal const string ActivityName = “Microsoft.AspNetCore.Hosting.HttpRequestIn”;
private const string ActivityStartKey = ActivityName + “.Start”;
private const string ActivityStopKey = ActivityName + “.Stop”;
当 Hosting 开始处理请求时,会检测当前是否有监听者监听这些事件,如果有的话就会写入事件,事件也会携带当前的一些上下文信息,代码如下所示:

以 SkyAPM-dotnet 举例,有对应的HostingTracingDiagnosticProcessor.cs监听事件,然后获取上下文信息记录 APM 埋点信息,代码如下所示:

这种方式的优点有:

高效和高性能:DiagnosticSource 是 .NET 平台自带的框架,使用它硬编码可以享受到编译器和 JIT 相关优化可以避免一些性能开销。组件开发者可以控制事件传递的频率和内容,以达到最佳的性能和资源利用率。
灵活:通过使用 DiagnosticSource,组件开发者可以灵活地定义自己的事件模型,并按需发布事件。这意味着可以轻松地定制自己的监测需求,而不必担心过多的日志数据产生过大的开销。
可扩展性:使用DiagnosticSource可以让组件的监测需求随着时间的推移而演变,而不必担心日志系统的限制。开发者可以根据自己的需要添加新的事件类型,以适应不断变化的监测需求。
易用性:DiagnosticSource的 API 简单易用,订阅事件数据也很容易。这使得使用它进行组件监测变得非常容易,并且可以快速地集成到现有的监测系统中。
可移植性:DiagnosticSource可以在多个平台上运行,包括 Windows、Linux 和 macOS 等。这意味着可以使用相同的事件模型来监测不同的应用程序和服务,从而简化了监测系统的设计和管理。
不过这种方式的缺点也很明显,就是必须由组件开发者显式的添加事件代码,探针的开发者也因此束手束脚,这就导致一些没有进行手动埋点的三方组件都无法添加事件监听,所以现阶段 SkyAPM-dotnet 支持的第三方组件还不是很丰富。

那么其实只要解决如何为没有进行手动埋点的组件库加入埋点就能解决 SkyAPM-dotnet 支持第三方组件多样性的问题。

.NET方法注入#
从上一节我们可以知道,目前制约APM支持组件不够丰富的原因之一就是很多组件库都没有进行可观测性的适配,没有在关键路径进行埋点。

那么要解决这个问题其实很简单,我们只需要修改组件库关键路径代码给加上一些埋点就可以了,那应该如何给这些第三方库的代码加点料呢?聊到这个问题我们需要知道一个 .NET 程序是怎么从源代码变得可以运行的。

通常情况下,一个 .NET 程序从源码到运行会经过两次编译(忽略 ReadyToRun 、NativeAOT 、分层编译等情况)。如下图所示:

第一次是使用编译器将 C#/F#/VB/Python/PHP 源码使用 Roslyn 等对应语言编译器编译成 CIL(Common Intermediate Language,公共中间语言)。第二次使用 RuyJit 编译器将 CIL 编译为对应平台的机器码,以 C# 语言举了个例子,如下图所示:

方法注入也一般是发生在这两次编译前后,一个是在 Roslyn 静态编译期间进行方法注入,期间目标 .NET 程序并没有运行,所以这种 .NET 程序未运行的方法注入我们叫它编译时静态注入。而在 RuyJit 期间 .NET程序已经在运行,这时进行方法注入我们叫它运行时动态注入。下表中列出了比较常见方法注入方式:

框架 类型 实现原理 优点 缺点
metalama 静态注入 重写Roslyn编译器,运行时插入代码 源码修改难度低,兼容性好 目前该框架不开源,只能修改源码,不能修改已编译好的代码,会增加编译耗时
Mono.Cecil、Postsharp 静态注入 加载编译后的*.dll文件,修改和替换生成后的CIL代码 兼容性好 使用难度高,需要熟悉 CIL ,会增加编译耗时,会增加程序体积
Harmony 动态注入 创建一个方法签名与原方法一致的方法,修改Jit后原方法汇编,插入jmp跳转到重写后方法 高性能,使用难度低 泛型、分层编译支持不友好
CLR Profile API 动态注入 调用CLR接口重写方法IL代码 功能强大,公开的API支持 实现困难,需要熟悉 CIL ,稍有不慎导致程序崩溃
综合各种优缺点现阶段APM使用最多的是 CLR Profile API 的方式进行方法注入,比如 Azure AppInsights、DataDog、Elastic等.NET探针都是使用这种方式。

基于CLR Profile API 实现APM探针原理#
CLR Profile API 简介#
在下面的章节中和大家聊一聊基于 CLR Profile API 是如何实现方法注入,以及 CLR Profile API 是如何使用的。

聊到 CLR 探查器,我们首先就得知道 CLR 是什么,CLR(Common Language Runtime,公共语言运行时),可以理解为是托管运行 .NET 程序的平台,它提供了基础类库、线程、JIT 、GC 等语言运行的环境(如下图所示),它功能和 Java 的 JVM 有相似之处,但定位有所不同。

.NET 程序、CLR 和操作系统的关系如下图所示:

那么 CLR 探查器是什么东西呢?根据官方文档的描述,CLR 探查器和相关API的支持从 .NET Framework 1.0就开始提供,它是一个工具,可以使用它来监视另一个 .NET 应用程序的执行情况,它也是一个( .dll )动态链接库,CLR 在启动运行时加载探查器,CLR 会将一些事件发送给探查器,另外探查器也可以通过 Profile API 向 CLR 发送命令和获取运行时信息。下方是探查器和 CLR 工作的简单交互图:

ICorProfilerCallback提供的事件非常多,常用的主要是下方提到这几类:

CLR 启动和关闭事件
应用程序域创建和关闭事件
程序集加载和卸载事件
模块加载和卸载事件
COM vtable 创建和析构事件
实时 (JIT) 编译和代码间距调整事件
类加载和卸载事件
线程创建和析构事件
函数入口和退出事件
异常
托管和非托管代码执行之间的转换
不同运行时上下文之间的转换
有关运行时挂起的信息
有关运行时内存堆和垃圾回收活动的信息
ICorProfilerInfo提供了很多查询和命令的接口,主要是下方提到的这几类:
方法信息接口
类型信息接口
模块信息接口
线程信息接口
CLR 版本信息接口
Callback 事件设置接口
函数 Hook 接口
还有 JIT 相关的接口
通过 CLR Profile API 提供的这些事件和信息查询和命令接口,我们就可以使用它来实现一个无需改动原有代码的 .NET 探针。
自动化探针执行过程#
APM 使用 .NET Profiler API 对应用程序进行代码插桩方法注入,以监控方法调用和性能指标从而实现自动化探针。下面详细介绍这一过程:

Profiler注册:在启动应用程序时,.NET Tracer 作为一个分析器(profiler)向 CLR(Common Language Runtime)注册。这样可以让它在整个应用程序生命周期内监听和操纵执行流程。
JIT编译拦截:当方法被即时编译(JIT)时,Profiler API 发送事件通知。.NET Tracer 捕获这些事件,如JITCompilationStarted,从而有机会在方法被编译之前修改其 IL(Intermediate Language)代码。
代码修改插桩:通过操纵IL代码,.NET Tracer 在关键方法的入口和退出点插入跟踪逻辑。这种操作对原始应用程序是透明的,不需要修改源代码。跟踪逻辑通常包括记录方法调用数据、计时、捕获异常等。
上下文传播:为了连接跨服务或异步调用的请求链,.NET Tracer 会将 Trace ID 和 Span ID在分布式系统中进行传递。这使得在复杂的微服务架构中追踪请求变得更加容易。
数据收集:插桩后的代码在运行期间会产生跟踪数据,包括方法调用时间、执行路径、异常信息等。这些数据会被封装成跟踪和跨度(spans),并且通过 APM Agent 发送到 APM 平台进行后续分析和可视化。
通过使用 .NET Profiler API 对应用程序进行方法注入插桩,APM 可以实现对 .NET 程序的详细性能监控,帮助开发者和运维人员发现并解决潜在问题。

第一步,向 CLR 注册分析器的步骤是很简单的,CLR 要求分析器需要实现COM组件接口标准,微软的 COM(Component Object Model)接口是一种跨编程语言的二进制接口,用于实现在操作系统中不同软件组件之间的通信和互操作。通过 COM 接口,组件可以在运行时动态地创建对象、调用方法和访问属性,实现模块化和封装。COM 接口使得开发人员能够以独立、可复用的方式构建软件应用,同时还有助于降低维护成本和提高开发效率。COM 一般需要实现以下接口:

接口(Interfaces):COM 组件使用接口提供一套预定义的函数,这样其他组件就可以调用这些函数。每个接口都有一个唯一的接口标识(IID)。
对象(Objects):COM 对象是实现了一个或多个接口的具体实例。客户端代码通过对象暴露的接口与其进行交互。
引用计数(Reference Counting):COM 使用引用计数管理对象的生命周期。当一个客户端获取到对象的接口指针时,对象的引用计数加一;当客户端不再需要该接口时,引用计数减一。当引用计数减至零时,COM 对象会被销毁。
查询接口(QueryInterface):客户端可以通过 QueryInterface 函数获取 COM 对象所实现的特定接口。这个函数接收一个请求的接口 IID,并返回包含该接口指针的 HRESULT。
类工厂(Class Factories):为了创建对象实例,COM 使用类工厂。类工厂是实现了 IClassFactory 接口的对象,允许客户端创建新的对象实例。
比如 OpenTelemetry 中的class_factory.cpp就是声明了COM组件,其中包括了查询接口、引用计数以及创建实例对象等功能。

然后我们只需要设置三个环境变量,如下所示:

COR_ENABLE_PROFILING:将其设置为1,表示启用 CLR 分析器。
COR_PROFILER: 设置分析器的COM组件ID,使 CLR 能正确的加载分析器。
COR_PROFILER_PATH_32/64: 设置分析器的路径,32位或者是64位应用程序。
通过以上设置,CLR 就可以在启动时通过 COM 组件来调用分析器实现的函数,此时也代表着分析器加载完成。在 OpenTelemetry 和>

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/23 10:04:34

提示词工程:AI交互精准输出的核心技术

1. 提示词工程的核心价值与应用场景 在AI交互领域,提示词(Prompt)就像程序员与编译器之间的特殊语言。我经历过无数次这样的场景:输入"写首诗"得到的是小学生水平的打油诗,而改为"以李白风格创作七言绝…

作者头像 李华
网站建设 2026/7/23 10:03:05

AI模型推理成本优化:从GPU/CPU权衡到低成本部署实践

在实际 AI 应用开发中,模型推理成本是决定项目能否规模化落地的关键因素。近期,一些美国 AI 基础设施公司推出的服务方案,让开发者能够以远低于主流云厂商的价格调用高性能模型,这为中小团队和初创公司提供了新的可能性。本文将围…

作者头像 李华
网站建设 2026/7/23 10:02:54

UE4性能优化实战:用Unreal Insights精准定位渲染瓶颈

1. 项目概述:从“感觉卡顿”到“数据说话”的性能优化新范式在UE4项目开发的中后期,尤其是面向移动端或复杂PC场景时,“性能优化”这四个字总会让开发者们既爱又恨。爱的是,优化成功后带来的流畅体验和项目上线保障;恨…

作者头像 李华
网站建设 2026/7/23 10:01:37

中学[我心道]导引术(Daoyin Technique)初稿

导引术(Daoyin Technique)初稿 在田间融入自然,双手举天,脚踏大地,仰天张口望日(月)玄门吐纳(Taoist Breath Regulation),低头闭嘴冥想腹部呼吸(Abdominal breathing). Immerse yourself in nature amid the fields, raise your hands toward the sky, stand firm on the earth…

作者头像 李华
网站建设 2026/7/23 9:56:57

WebGPU与WebGL对比:下一代Web图形与计算API实战指南

1. 先搞清楚 WebGL 和 WebGPU 到底解决什么问题如果你在网页上做过 3D 图形、数据可视化或者需要 GPU 加速的计算任务,大概率已经接触过 WebGL。WebGL 让浏览器能直接调用 GPU 进行图形渲染,这是网页 3D 技术的基石。但 WebGL 基于二十多年前的 OpenGL E…

作者头像 李华
网站建设 2026/7/23 9:56:47

数字绘画教程:从零开始绘制原创角色OC的完整流程

在实际数字绘画创作中,很多初学者会困惑于如何将脑海中的原创角色(OC)稳定地呈现在画布上。一个常见的误区是直接开始刻画细节,而忽略了从整体到局部的构建流程。本文将以“我就这样(OC)”这一主题为例&…

作者头像 李华