浅谈动态跟踪技术之DTrace
什么是动态追踪(Dynamic Tracing)
举个简单例子,一个人正在健身房里跑步,我们用摄像机📹偷偷对他进行录像,事后我们就可以使用录像对这个人运动过程中的步频、速度、呼吸节奏等数据进行分析研究,以便为这个人提供更加科学的付费健身咨询服务(这个人并不知道被录像了)。仔细想想这不是一个很有价值的事吗?
对这个例子的整个过程分析,它大致有几个特点:
1、对健身者是无感知,不影响他当前正在进行的运动,也不需要他配合(低影响)
2、我们录像的时机可以随时开始,随时结束(低耦合)
3、录像的对象可以是人,也可以是物体(可移植)
这就是动态追踪技术(Dynamic Tracing),在计算机领域是一种后现代的高级调试技术,可以帮助开发者在非常短的时间内,回答一些很难的关于软件系统方面的问题,从而更快速地排查和解决问题,也可以帮助产品决策者实时了解当前产品的线上运行情况。在火箭🚀般飞速发展的移动互联网时代,APP的用户规模,业务种类繁多,逻辑越来越复杂,代码编写也不再是单一语言。作为开发者、产品决策者正在面临无法掌控整个系统的巨大压力,动态追踪技术实际就能帮助我们实现这种愿景。
动态追踪的原理
动态追踪技术通常是基于操作系统内核来实现的。操作系统内核其实可以控制整个软件世界,因为它其实是处于"造物主"这样的一个地位。它拥有绝对的权限,同时它可以确保我们针对软件系统发出的各种"查询"不会影响到软件系统本身的正常运行。换句话说,我们的这种查询必须是足够安全的,是可以在生产环境上大量使用的。一般是通过探针这样的机制来发起查询。我们会在软件系统的某一个层次,或者某几个层次上面,安置一些探针(测试人员术语叫打桩,产品人员术语叫埋点),然后我们会自己定义这些探针所关联的处理程序,当关联程序不运行时,这些探针在系统中不会产生影响。而这些关联程序可以通过这些探针实时地向我们反馈信息,帮助解决许多重大问题。
这有点像中医里面的针灸,就是说如果我们把软件系统看成是一个人,我们可以往他的一些穴位上扎一些“针”,那么这些针头上面通常会有我们自己定义的一些“传感器”,我们可以自由地采集所需要的那些穴位上的关键信息,然后把这些信息汇总起来,产生可靠的病因诊断和可行的治疗方案。这里的追踪通常涉及两个纬度。一个是时间纬度,因为这个软件还一直在运行,它便有一个在时间线上的连续的变化过程。另一个纬度则是空间纬度,因为可能它涉及到多个不同的进程,包含内核进程,而每个进程经常会有自己的内存空间、进程空间,那么在不同的层次之间,以及在同一层次的内存空间里面,我可以同时沿纵向和横向,获取很多在空间上的宝贵信息。这有点儿像蛛蛛在蛛网上搜索猎物。
<摘录至https://openresty.org/posts/dynamic-tracing>
DTrace初探
DTrace 是动态追踪技术的鼻祖,它于 21 世纪初诞生于 Solaris 操作系统,是由原来的 Sun Microsystems 公司的工程师编写的,先后被移植到 Linux、FreeBSD、NetBSD 及 Mac OS X 等操作系统上。iOS 系统也有,大名鼎鼎的 Instrument 工具就是基于 DTrace 实现的,而且更多的功能还在随着 iOS 系统进行版本迭代。
DTrace 工具组件包括提供器和探测器:
- 提供器:由
dtrace
内核驱动命令及附加在上面的 DTrace 脚本组成(后缀名.d)。Mac OS X 默认就安装了dtrace
工具;脚本使用D语言
编写,也叫 d 脚本,Mac OS X的/usr/share/examples/DTTk/
目录下有很多例子; - 探测器(即探针):由提供器启动,可标识所检测的模块和函数,其名称标准格式为
提供器:模块:函数:名称
,每个探针还具有一个唯一的整数标识符。在苹果开源的 xnu 中可以看到苹果版的 DTrace 源码,打包为内核模块来收集跟踪数据,它提供接口通过dtrace
内核驱动命令访问内核数据,在内核源码中很多带有provider
关键字都属于标识某个模块数据的探针。其定义如下:
// 探针定义
typedef struct sdt_provider {
const char *sdtp_name; /* name of provider */
const char *sdtp_prefix; /* prefix for probe names */
dtrace_pattr_t *sdtp_attr; /* stability attributes */
dtrace_provider_id_t sdtp_id; /* provider ID */
} sdt_provider_t;
// xnu中在使用的一些探针
sdt_provider_t sdt_providers[] = {
{ "vtrace", "__vtrace____", &vtrace_attr, 0 },
{ "sysinfo", "__cpu_sysinfo____", &info_attr, 0 },
{ "vminfo", "__vminfo____", &info_attr, 0 },
{ "fpuinfo", "__fpuinfo____", &fpu_attr, 0 },
{ "sched", "__sched____", &stab_attr, 0 },
{ "proc", "__proc____", &stab_attr, 0 },
{ "io", "__io____", &stab_attr, 0 },
{ "ip", "__ip____", &stab_attr, 0 },
{ "tcp", "__tcp____", &stab_attr, 0 },
{ "mptcp", "__mptcp____", &stab_attr, 0 },
{ "mib", "__mib____", &stab_attr, 0 },
{ "fsinfo", "__fsinfo____", &fsinfo_attr, 0 },
{ "nfsv3", "__nfsv3____", &stab_attr, 0 },
{ "nfsv4", "__nfsv4____", &stab_attr, 0 },
{ "sysevent", "__sysevent____", &stab_attr, 0 },
{ "sdt", "__sdt____", &sdt_attr, 0 },
{ "boost", "__boost____", &stab_attr, 0},
{ NULL, NULL, NULL, 0 }
};
D语言
这里的 D语言 语法大部分跟 C语言 非常相似(这就带来了很好的可移植性),但总体架构是不同的。每个脚本由若干个探针语句组成。它们都符合如下的形式:
probe descriptions
/ predicate /
{
action statements
}
其中断言 (predicate) 和动作语句 (action statement) 部分都是可选的。
probe descriptions
即探针描述定义了语句匹配什么类型的探针,结构就是之前提到的提供器:模块:函数:名称 — provider:module:function:name
,所有的部分都可以省略。
其中BEGIN
语句在所有探针开始之前运行,END
语句在脚本退出时候执行。
语法很简单,设计很复杂,更详细的介绍参见。,本文只介绍后面Demo例子中会使用到的方法:
内建变量:
pid —— 当前进程的进程id
tid —— 当前线程的线程id
uid —— 当前进程的用户id
timestamp —— 一个纳秒级的计数器的当前时间戳
probemod —— 当前探针描述的模块名称部分
probefunc —— 当前探针描述的函数名称部分
常用函数:
void trace(expression) —— 最基本的操作,将将 D 表达式用作其参数并跟踪结果到定向缓冲区
void printf(string format, ...) —— 与trace操作一样,printf跟踪 D 表达式。不过,printf允许格式化输出
当运行dtrace
工具时,我们传入的脚本被编译成字节码。接着字节码被传入安插了探针的代码中(通常是 kernel)。在 kernel 中有一个解释器来运行这些字节码。当将静态探针加入可执行程序 (一个 app 或 framework),它们被作为S_DTRACE_DOF
(Dtrace Object Format) 部分被加入,并且在程序运行时被加载进 kernel。这样 DTrace 就知道当前的静态探针。
Dtrace示例
使用dtrace -l
可以查看 Mac OS X 系统上的所有探针,dtrace -l -m <module_name>
可以查看指定模块的探针
注意
1、如果出现错误:
dtrace: failed to initialize dtrace: DTrace requires additional privileges
可以这样提升dtrace
工具的权限:
sudo dtrace -l
2、如果出现错误:
dtrace: system integrity protection is on, some features will not be available
可以从安全模式关闭
csrutil disable
举个例子,假设需求是要跟踪系统malloc
方法的所有分配内存大小,可以设计探针的定义文件DTraceDemo.probe
:
provider DTraceDemo {
probe malloc_log(void *ptr, size_t size);
};
然后,执行下面的命令生成探针的头文件,后面带入测试工程中编译
/*
* 更多dtrace用法,参见sudo dtrace --help
*(其实没有help参数,习惯而已)
*/
sudo dtrace -h -o DTraceDemo.h -s DTraceDemo.probe
测试代码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSInteger count = 0;
BOOL stop = NO;
while ( !stop ) {
// 分配10次,每次10M内存
size_t len = 10*1024*1024;
void *ptr = (void *)malloc(len);
memset(ptr, '\0', len);
// 插入探针
DTRACEDEMO_MALLOC_LOG(ptr, len);
if (count > 10) { stop = YES; }
count += 1;
sleep(2);
}
}
return 0;
}
最后,创建自己的脚本vim DTraceDemo.d
,文件名必须以 .d 后缀结尾,这是 D 脚本的约定结尾。更多d脚本例子参见。
#!/usr/sbin/dtrace -s
#pragma D option quiet
BEGIN
{
trace("Begin trace malloc!\n");
}
DTraceDemo*:::malloc_log
{
printf("malloc ptr:0x%p size:%lld thread:%lu\n", arg0, arg1, tid);
}
END
{
printf("End trace!");
}
使用sudo dtrace -s DTraceDemo.d
命令开始 dtrace 测试,接着启动测试功能得到输出如下:
Begin trace malloc!
malloc ptr:0x105000000 size:10485760 thread:381125
malloc ptr:0x105a00000 size:10485760 thread:381125
malloc ptr:0x106400000 size:10485760 thread:381125
malloc ptr:0x106e00000 size:10485760 thread:381125
malloc ptr:0x107800000 size:10485760 thread:381125
malloc ptr:0x108200000 size:10485760 thread:381125
malloc ptr:0x108c00000 size:10485760 thread:381125
malloc ptr:0x109600000 size:10485760 thread:381125
malloc ptr:0x10a000000 size:10485760 thread:381125
malloc ptr:0x10aa00000 size:10485760 thread:381125
malloc ptr:0x10b400000 size:10485760 thread:381125
malloc ptr:0x10be00000 size:10485760 thread:381125
^C
End trace!
结束语
在许多不同的情况下,需要跟踪应用程序。对于开发人员来说,可以通过跟踪应用程序来诊断问题,这可能比使用调试器更方便。DTrace 等工具的跟踪能力更强,可以获得非常有针对性的丰富的应用程序信息。在 iOS 开发中经常使用的调试工具 instruments,其核心数据采集原理即 DTrace。
最后附上源码传送门以及Mac OS X系统上的探针图: