第一次是使用编译器将 C#/F#/VB/Python/PHP 源码使用 Roslyn 等对应语言编译器编译成

第一次是使用编译器将 C#/F#/VB/Python/PHP 源码使用 Roslyn 等对应语言编译器编译成
第二次使用 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 是什么CLRCommon 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向 CLRCommon Language Runtime注册。这样可以让它在整个应用程序生命周期内监听和操纵执行流程。JIT编译拦截当方法被即时编译JIT时Profiler API 发送事件通知。.NET Tracer 捕获这些事件如JITCompilationStarted从而有机会在方法被编译之前修改其 ILIntermediate Language代码。代码修改插桩通过操纵IL代码.NET Tracer 在关键方法的入口和退出点插入跟踪逻辑。这种操作对原始应用程序是透明的不需要修改源代码。跟踪逻辑通常包括记录方法调用数据、计时、捕获异常等。上下文传播为了连接跨服务或异步调用的请求链.NET Tracer 会将 Trace ID 和 Span ID在分布式系统中进行传递。这使得在复杂的微服务架构中追踪请求变得更加容易。数据收集插桩后的代码在运行期间会产生跟踪数据包括方法调用时间、执行路径、异常信息等。这些数据会被封装成跟踪和跨度spans并且通过 APM Agent 发送到 APM 平台进行后续分析和可视化。通过使用 .NET Profiler API 对应用程序进行方法注入插桩APM 可以实现对 .NET 程序的详细性能监控帮助开发者和运维人员发现并解决潜在问题。第一步向 CLR 注册分析器的步骤是很简单的CLR 要求分析器需要实现COM组件接口标准微软的 COMComponent Object Model接口是一种跨编程语言的二进制接口用于实现在操作系统中不同软件组件之间的通信和互操作。通过 COM 接口组件可以在运行时动态地创建对象、调用方法和访问属性实现模块化和封装。COM 接口使得开发人员能够以独立、可复用的方式构建软件应用同时还有助于降低维护成本和提高开发效率。COM 一般需要实现以下接口接口InterfacesCOM 组件使用接口提供一套预定义的函数这样其他组件就可以调用这些函数。每个接口都有一个唯一的接口标识IID。对象ObjectsCOM 对象是实现了一个或多个接口的具体实例。客户端代码通过对象暴露的接口与其进行交互。引用计数Reference CountingCOM 使用引用计数管理对象的生命周期。当一个客户端获取到对象的接口指针时对象的引用计数加一当客户端不再需要该接口时引用计数减一。当引用计数减至零时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 和>那后面的JIT编译拦截以及其它功能如何实现呢我们举一个现实存在的例子如果我们需要跟踪每一次 Reids 操作的时间和执行命令的内容那么我们在应该修改StackExchange.RedisExecuteAsyncImpl方法从message中读取执行命令的内容并记录整个方法耗时。那么APM如何实现对Redis ExecuteAsyncImpl进行注入的可以打开dd-trace-dotnet仓库也可以打开opentelemetry-dotnet-instrumentation仓库这两者的方法注入实现原理都是一样的只是代码实现上有一些细微的差别。这里我们还是以 dd-trace-dotnet 仓库代码为例。打开tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation目录里面所有的源码都是通过方法注入的方式来实现APM埋点有非常多的组件埋点的实现比如 MQ 、Redis 、 CosmosDb 、Couchbase 等等。