众所周知,性能问题是所有实用应用在迭代过程中必然要面对的问题。对于此类问题,简单地投入更多硬件资源的做法可能会取得一定效果。但总的来看,此类做法的边际成本是不断上升的。换言之,随着性能需求的上涨,要换取同样的性能提升,仅凭硬件升级所需要的成本会越来越高。故而性能优化是每一位运维/软件开发人员必须掌握的技术。
Profiling
在进行应用性能优化实践时,首先面对的就是热点定位,即确定那些带来巨大资源耗散的代码位置。而在不借助外部工具的前提下,定位资源热点是一件相当困难的事。它需要当事人对于应用实现本身有一个整体的把握,了解应用架构内每一个功能模块代码的路径与细节。与此同时,当事人还要对于应用实现所依赖的第三方功能库的表现有一定的把握。对于那些具备一定规模的应用系统,具备前述素质的工程师的数量屈指可数。而即便是这些百里挑一的优秀人才,其热点预估也不能保证一定是准确的。
Profiling API
常见的 Profiler 实现多采取如下架构:
Profiler 架构
需要补充说明一点,上文所说的 ICorProfilerCallback 接口实际上存在有 ICorProfilerCallback ~ ICorProfilerCallback7 这样7个版本的接口定义。高标号的接口版本向下兼容,但会提供新的功能扩展。不过,更高标号的接口往往也需要有更新版本的 CLR 来支持(如调用 ICorProfilerCallback7 需要在环境中部署 Framework 4.6.1 以上版本),在实际使用时需要多加注意。
目前 Profiling API 所支持的特性
正如前文所述,Profiler 对于程序行为的描述源自 profiling API 所提供的信息。在目前版本中,凭借 profiling API 能够获取到下列事件的消息通知:
- CLR 的启动与关停
- application domain 的创建与关闭
- 程序集的加载与卸载
- 模块(Module)的加载与卸载
- COM vtable 的创建与销毁
- JIT 编译与 的出发
- 类的加载卸载
- 线程的创建与销毁
- 函数的进入与返回
- 托管代码与非托管代码的执行切换
- 运行时挂起
- 运行时堆内存信息与 GC 活动
随着 技术的演进,未来的 Profiling API 或许能够提供更多的信息。不过,以下功能点是 Profiling API 不会实现的,请在应用时回避:
- 非托管代码的执行信息
- 运行时修改自身代码的应用的 Profiling(如 AOP)
- 边界检验
- 远程 profiling
- 高可靠性环境下的 profiling
线程相关
对于加载了 Profiler DLL 的进程而言,其在创建新线程时,新线程本身也会产生 ICorProfilerCallback 接口下定义的各种事件通知。这一过程中,Profiler 不必去显式地指定一个 ThreadID 以使得 Profiling API 生效。同样的,Profiler 完全可以简单地在代码中使用 thread-local 的存储方式,用不着费心地去进行存储位置的全局重定向。
当然,还是有一些要点需要我们在并发背景下留意。例如,Profiling API 本身并不能保障数据结构的线程安全,因此我们需要在可能产生并行访问的地方给 Profiler 代码冲突区加锁以保证 Profiler 的行为符合预期。同样,对于多线程的应用场景来说,Profiler 不应该假设 ICorProfilerCallback 的各个接口存在一定的先后顺序。举例来说,一个有两个同样线程的程序在运行时可能会先产生一个 FunctionEnter 然后才产生 ICorProfilerCallback::JITCompilationFinished。
还有一个线程相关的问题是来自于 COM 接口的。上文中我们说过 Profiler 事实上是实现为一个 COM 组件的,但其实 CLR 在运行时并不会去初始化 COM。这是为了避免在应用代码指定线程模型前,CLR 调用 [CoInitialize][Ref12] 来指定应用线程模型。同样地,在 Profiler 内部,不要去调用 CoInitialize 以避免与应用代码产生冲突。
调用栈
获取调用栈信息是应用 Profiling 时的一项关键需求。针对这一需求,Profiling API 提供给 Profiler 编写者两种实现方式:栈快照和倒影栈。
其他需要留意之处
前文强调过,Profiler 是一个非托管的 DLL 库,会在应用运行时被加载到 CLR 中并与应用处于同一进程空间下。如此,Profiler DLL 实质上是不受托管代码的访问控制的。其运行唯一的限制就是运行 Profiler 的 OS 用户必须拥有足够权限。因此,对于要部署 Profiler 的技术人员来说,必须要明白这其中可能的风险,提早进行准备。例如可以把 Profiler DLL 加到访问控制列表(ACL)中以免恶意用户对其加以利用。
还有,Profiler DLL 作为 CLR 的一个插件,其运行错误可能会引起 CLR 本身的崩溃,在实施时一定要足够小心。而对于那些运行在栈内存空间紧张的环境下的 Profiler,要警惕因为 ICorProfilerCallback 导致栈溢出而引起的应用崩溃。在这种资源受限的环境中,要尽可能地减少 Profiler 自身的资源耗散。尽力避免原本能够运行的应用因为 Profiler 而导致无法运行的情况。
结语
本文简述了 Profiling 技术的总体情况,并就其中的一些重要技术点进行了阐述。希望能帮助读者初步理解 Profiling 技术。后续,我们将具体到代码实施层面,就 Profiling 的实现进行详细讨论