浅谈动态跟踪技术之DTrace

什么是动态追踪(Dynamic Tracing)

举个简单例子,一个人正在健身房里跑步,我们用摄像机📹偷偷对他进行录像,事后我们就可以使用录像对这个人运动过程中的步频、速度、呼吸节奏等数据进行分析研究,以便为这个人提供更加科学的付费健身咨询服务(这个人并不知道被录像了)。仔细想想这不是一个很有价值的事吗?

对这个例子的整个过程分析,它大致有几个特点:

1、对健身者是无感知,不影响他当前正在进行的运动,也不需要他配合(低影响)
2、我们录像的时机可以随时开始,随时结束(低耦合)
3、录像的对象可以是人,也可以是物体(可移植)

这就是动态追踪技术(Dynamic Tracing),在计算机领域是一种后现代的高级调试技术,可以帮助开发者在非常短的时间内,回答一些很难的关于软件系统方面的问题,从而更快速地排查和解决问题,也可以帮助产品决策者实时了解当前产品的线上运行情况。在火箭🚀般飞速发展的移动互联网时代,APP的用户规模,业务种类繁多,逻辑越来越复杂,代码编写也不再是单一语言。作为开发者、产品决策者正在面临无法掌控整个系统的巨大压力,动态追踪技术实际就能帮助我们实现这种愿景。

动态追踪的原理

动态追踪技术通常是基于操作系统内核来实现的。操作系统内核其实可以控制整个软件世界,因为它其实是处于"造物主"这样的一个地位。它拥有绝对的权限,同时它可以确保我们针对软件系统发出的各种"查询"不会影响到软件系统本身的正常运行。换句话说,我们的这种查询必须是足够安全的,是可以在生产环境上大量使用的。一般是通过探针这样的机制来发起查询。我们会在软件系统的某一个层次,或者某几个层次上面,安置一些探针(测试人员术语叫打桩,产品人员术语叫埋点),然后我们会自己定义这些探针所关联的处理程序,当关联程序不运行时,这些探针在系统中不会产生影响。而这些关联程序可以通过这些探针实时地向我们反馈信息,帮助解决许多重大问题。

这有点像中医里面的针灸,就是说如果我们把软件系统看成是一个人,我们可以往他的一些穴位上扎一些“针”,那么这些针头上面通常会有我们自己定义的一些“传感器”,我们可以自由地采集所需要的那些穴位上的关键信息,然后把这些信息汇总起来,产生可靠的病因诊断和可行的治疗方案。这里的追踪通常涉及两个纬度。一个是时间纬度,因为这个软件还一直在运行,它便有一个在时间线上的连续的变化过程。另一个纬度则是空间纬度,因为可能它涉及到多个不同的进程,包含内核进程,而每个进程经常会有自己的内存空间、进程空间,那么在不同的层次之间,以及在同一层次的内存空间里面,我可以同时沿纵向和横向,获取很多在空间上的宝贵信息。这有点儿像蛛蛛在蛛网上搜索猎物。
<摘录至https://openresty.org/posts/dynamic-tracing>

DTrace初探

DTrace 是动态追踪技术的鼻祖,它于 21 世纪初诞生于 Solaris 操作系统,是由原来的 Sun Microsystems 公司的工程师编写的,先后被移植到 LinuxFreeBSDNetBSDMac 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系统上的探针图:

1522497825129.jpg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容