《性能之巅:洞悉系统、企业与云计算》第一章(绪论)和第二章(方法)的笔记,请参考Part 1,第三章(操作系统)的笔记,请参考Part 2,本文是第四章——观测工具。
在实践中,工具不能覆盖所有方面,系统性能专家利用推论和解释:用间接的工具和统计来弄清楚系统的活动。
工具类型
工具的一种分类:
有些工具不止适合一个象限,top(1)
有一个系统级别的视图,DTrace也有进程级别的能力。还有一些性能工具是基于剖析(profiling)的,对系统或进程做一系列快照,以此来进行观测。
计数器
内核维护各种统计数据,称为计数器,用于对事件计数。计数器的使用可认为是零开销的,因为它们默认就是开启的,而且始终由内核维护。唯一的使用开销是从用户空间读取它们的时候(可忽略不计)。
系统级别工具:
- vmstat:虚拟内存和物理内存的统计,系统级别;
- mpstat:每个CPU的使用情况;
- iostat:每个磁盘I/O的使用情况,由块设备接口报告;
- netstat:网络接口的统计,TCP/IP栈的统计,以及每个连接的一些统计信息;
- sar:各种各样的统计,能归档历史数据。
进程级别工具:
- ps:进程状态,显示进程的各种统计信息,包括内存和CPU;
- top:按一个统计数据(如CPU使用)排序,显示排名高的进程。基于Solaris的系统对应的工具是
prstat(1M)
; - pmap:将进程的内存段和使用统计一起列出。
进程工具以进程为导向,使用内核为每个进程维护的计数器,从/proc
目录读取统计信息。
跟踪
跟踪收集每一个事件的数据以供分析。跟踪框架一般默认是不启用的,因为跟踪捕获数据会有CPU开销,另外还需要不小的存储空间来存放数据。这些开销会拖慢所跟踪的对象,在解释测量时间时需要加以考虑。
日志,包括系统日志,可以认为是一种默认开启的低频率跟踪。日志包括每一个事件的数据,虽然通常只针对偶发事件,如错误和警告。
系统级别工具:
- tcpdump:网络包跟踪(用libpcap库);
- snoop:为基于Solaris的系统打造的网络包跟踪工具;
- blktrace:块I/O跟踪(Linux);
- iosnoop:块I/O跟踪(基于DTrace);
- execsnoop:跟踪新进程(基于DTrace);
- dtruss:系统级别的系统调用缓冲跟踪(基于DTrace);
- DTrace:跟踪内核的内部活动和所有资源的使用情况(不仅仅是网络和块I/O),支持静态和动态的跟踪;
- SystemTap:跟踪内核的内部活动和所有资源的使用情况,支持静态和动态的跟踪;
- perf:Linux性能事件,跟踪静态和动态的探针。
进程级别工具:
- strace:基于Linux系统的系统调用跟踪;
- truss:基于Solaris系统的系统调用跟踪;
- gdb:源代码级别的调试器,广泛应用于Linux系统;
- mdb:Solaris系统的一个具有可扩展性的调试器。
剖析
剖析,profiling,通过对目标收集采样或快照来归纳目标特征。剖析工具,剖析器profiler,有时会稍微改变这一频率,避免采样与目标活动同一步调,因为这样可能会导致多算或少算。
基于时间+硬件缓存的剖析器:
- oprofile:Linux系统剖析;
- perf:Linux性能工具集,包含有剖析的子命令;
- DTrace:程序化剖析,基于时间的剖析用自身的profile provider,基于硬件事件的剖析用cpc provider;
- SystemTap:程序化剖析,基于时间的剖析用自身的timer tapset,基于硬件事件的剖析用自身perf tapset;
- cachegrind:源自valgrind工具集,能对硬件缓存的使用做剖析,也能用kcachegrind做数据可视化;
- Intel VTune Amplifier XE:Linux和Windows的剖析,拥有包括源代码浏览在内的图形界面;
- Oracle Solaris Studio:用自带的性能分析器对Solaris和Linux做剖析,拥有包括源代码浏览在内的图形界面。
监视sar
最广泛用于监视单一操作系统的工具是sar(1)
,基于计数器的,在预定的时间(通过cron)执行以记录系统计数器的状态。读取自己统计信息的归档数据(若开启)来打印历史统计信息。
在Linux中,sar(1)
是通过sysstat包提供的。
类似工具:System Data Recorder和Collectl。
观测来源
系统性能统计的主要来源是:/proc
、/sys
和kstat。一份比较完整的观测来源清单如下表:
Type | Linux | Solaris |
进程级计数器 | /proc | /proc,lxproc |
系统级计数器 | /proc,/sys | kstat |
设备驱动和调试信息 | /sys | kstat |
进程级跟踪 | ptrace,uprobes | procfs,dtrace |
性能计数器 | perf event | libcpc |
网络跟踪 | libpcap | libdlpi,libpcap |
进程级延时指标 | 延时核算 | 微状态核算 |
系统级跟踪 | tracepoints,kprobes,ftrace | dtrace |
/proc
一个提供内核统计信息的文件系统接口。/proc
由内核动态创建,不需要任何存储设备(在内存中运行)。多数文件是只读的,为观测工具提供统计数据。一部分文件是可写的,用于控制进程和内核的行为。
Linux中/proc
的文件系统类型是proc
,Solaris则是procfs
。
Linux中与进程性能观测相关的文件:
- limits:实际的资源限制;
- maps:映射的内存区域;
- sched:CPU调度器的各种统计;
- schedstat:CPU运行时间、延时和时间分片;
- smaps:映射内存区域的使用统计;
- stat:进程状态和统计,包括总的CPU和内存的使用情况;
- statm:以页为单位的内存使用总结;
- status:stat和statm的信息,用户可读;
- task:每个任务的统计目录。
Linux中与性能观测相关的系统级别的文件:
- cpuinfo:物理处理器信息,包含所有虚拟CPU、型号、时钟频率和缓存大小;
- diskstats:对于所有磁盘设备的磁盘I/O统计;
- interrupts:每个CPU的中断计数器;
- loadavg:负载平均值;
- meminfo:系统内存使用明细;
- net/dev:网络接口统计;
- net/tcp:活跃的TCP套接字信息;
- schedstat:系统级别的CPU调度器统计;
- self:关联当前进程ID路径的符号链接,为了使用方便;
- slabinfo:内核slab分配器缓存统计;
- stat:内核和系统资源的统计,CPU、磁盘、分页、交换区、进程;
- zoneinfo:内存区信息。
Solaris下,/proc
只有进程状态的统计。系统级别的观测采用其他框架,主要是kstat。与性能观测相关的文件:
- map:虚拟地址空间映射;
- psinfo:进程的各种信息,包括CPU和内存的使用;
- status:进程状态信息;
- usage:扩展的进程活动统计,包括进程微状态、错误、块、上下文切换,以及系统调用计数;
- lstatus:与status相似,但包含的是每一个线程的统计;
- lpsinfo:与psinfo相似,但包含的是每一个线程的统计;
- lusage:与usage相似,但包含的是每一个线程的统计;
- lwpsinfo:针对代表性LWP(目前最活跃)的轻量级进程(线程)统计,还有lwpstatus和lwpsinfo文件;
- xmap:扩展的内存映射统计(尚无文档);
sys
kstat
Solaris上有一个为系统级别的观测工具所用的内核统计框架(kstat)。kstat包含绝大多数资源的统计,一个典型的系统在kstat里能有上万计的可用统计。与/proc
和/sys
不同,kstat没有伪文件系统,要用ioctl()从/dev/kstat
读取。一般是调用libkstat库里的函数来执行这一操作,或用Sun::Solaris::Kstat
,该Perl库具有同样的功能(虽然一些偏好libkstat的发行版里取消对该Perl库的支持)。命令行工具kstat(1M)
也能提供统计数据,还能用在shell脚本里。
kstat是四元组的结构:module:instance:name:statistic
- module:一般指的是创建统计数据的内核模块,如sd指SCSI磁盘驱动,zfs指的是ZFS;
- instance:某些模块是以多个实例的形式存在的,例如对每一个SCSI磁盘都有一个sd模块。instance是一个枚举值;
- name:一组统计数据的名字;
- statistic:单个的统计值名;
延时核算
开启CONFIG_TASK_DELAY_ACCT
选项的Linux系统按以下状态跟踪每个任务的时间:
- 调度器延时:等待轮到上CPU;
- 块I/O:等待块I/O完成;
- 交换:等待换页(内存压力);
- 内存回收:等待内存回收例程;
微状态核算
Solaris上有线程级别和CPU级别的微状态核算(microstate accounting),针对预先定义好的状态可以记录高精度的时间。相较基于tick的指标,精确度有很大的提升,还提供一些附加的状态,用于性能分析。
CPU级别的指标是通过kstat暴露给用户空间工具,而进程级别的指标是通过/proc
。
mpstat(1M)
可用于输出CPU的微状态,如usr、sys和idl列,分别对应内核代码中的CMS_USER、CMS_SYSTEM和CMS_IDLE。
prstat -m
可用于打印线程的微状态,如USR、SYS。
其他观测源
其他:
- CPU性能计数器:可编程的硬件寄存器,提供低层级的性能信息,包括CPU周期计数、指令计数、停滞周期等。在Linux上是通过
perf_events
接口,或系统调用perf_event_open()
,或perf(1)
来访问这些计数器。Solaris里是通过libcpc,或包括cpustat(1M)在内的工具来访问的; - 进程级别跟踪:跟踪的是用户级别软件事件,如系统调用和函数调用。一般执行的代价较高,会拖慢跟踪的目标。Linux上有系统调用ptrace()来控制进程跟踪,strace(1)用来跟踪系统调用,uprobes来做用户级别的动态跟踪。Solaris的系统用procfs和truss(1)来跟踪系统调用,DTrace做动态跟踪;
- 内核跟踪:Linux中,tracepoints提供静态的内核探针(原先叫做内核标记,kernel markers),kprobes提供动态探针。工具ftrace、perf(1)、DTrace和SystemTap都用到这两项。Solaris系统,静态和动态的探针都由dtrace内核模块提供。DTrace、SystemTap都用到内核跟踪;
- 网络嗅探:网络嗅探提供一种从网络设备上抓包的方法,能对数据包和协议的性能做详细的调查。在Linux上,嗅探的功能是通过libpcap库和
/proc/net/dev
提供的,命令行工具则有tcpdump(8)。Solaris上,嗅探功能是通过libdlpi库和/dev/net
提供的,命令行工具是snoop(1M)。往Solaris系统移植的libpcap库和tcpdump(8)还在开发中。捕获和检查所有的数据包其实无论对于CPU还是存储都是有开销的; - 进程核算:进程核算可以追溯到大型机时代,那时候要对使用计算机的部门和用户收费,是基于进程的执行和运行时间计费的。现在它以某种形式存在于Linux和基于Solaris的系统上,有时能在进程级别对性能分析有所帮助。工具atop(1)用进程核算能捕捉到短暂存活的进程并显示其信息,而用
/proc
快照的办法很可能无法觉察到这件事; - 系统调用:一些可用的系统调用和库函数调用能提供某些性能指标。其中包括getrusage(),这个函数调用是为进程拿到自己资源的使用统计,包括用户时间、系统时间、错误、消息,以及上下文切换。Solaris用的是swapctl(),这个系统函数用于swap 设备的管理和统计(Linux对应的是/proc/swap)。
其他:
- Linux:I/O核算、blktrace、timer_stats、lockstat、debugfs;
- Solaris:扩展核算(extended accounting)、流核算(flow accounting)、Solaris审计。
最后,可用工具打开/dev/mem/
或/dev/kmem
直接读取内核内存。
DTrace
DTrace的设计是生产环境安全的,拥有最小的性能开销。
静态和动态跟踪
静态跟踪:在编译之前加进代码里的静态探针
动态跟踪:在编译之后软件运行时加入的动态探针
int指令引发一个软中断,该软中断已经接到指示执行动态跟踪的action。当动态跟踪被禁用时,指令会回到原来的状态。这是内核地址空间的现场修改(live patching),所采用的技术会因处理器类型的不同而有所不同。
只有当动态跟踪开启后,才能插入指令。没开启时,是没有附加指令的,因此也没有任何探针效果。这就是不使用时零开销(zero overhead when not in use)。使用时来自附加指令的开销是与探针触发的频率成比例的:跟踪事件的频率和所执行的action。
DTrace能动态跟踪函数的入口和返回,以及任何在用户空间的指令。由于这是在CPU指令上动态建立探针,而CPU指令在软件不同版本时会发生变化,所以这是一个不稳定接口(unstable interface)。在跟踪的软件版本更新时可能需要变更所有的Dtrace单行命令和脚本。
探针
DTrace探针是以四元组命名的,provider:module:function:name
,provider是相关探针的集合。module和function是动态产生的,标记探针指示的代码位置。name是探针的名字。
provider
可用的DTrace provider取决于你的DTrace和操作系统的版本,包括:
- syscall:系统调用自陷表;
- vminfo:虚拟内存统计;
- sysinfo:系统统计;
- profile:任意频率的采样;
- sched:内核调度事件;
- proc:进程级别事件,创建、执行、退出;
- io:块设备接口跟踪(磁盘I/O);
- pid:用户级别动态跟踪;
- tcp:TCP协议事件,连接、发送和接收;
- ip:IP协议事件,发送和接收;
- fbt:内核级别动态跟踪。
很多provider都是用静态跟踪来实现的,好处是接口稳定,代价(缺点)是监测视野有点限制。为了确保对目标软件的不同版本都能适用,尽量用静态provider来编写脚本。
参数
探针通过一组称为参数的变量来提供数据。参数的使用取决于provider。系统调用的provider给每一个系统调用都做入口和返回的探针。
D语言
D语言与awk类似,能用作单行命令也能写脚本。语句形式如下:probe_description /predicate/ {action}
action是一系列以分号间隔的语句,当探针触发时执行。predicate是可选的过滤表达式。
例句:proc:::exec-success /execname=="httpd"/{trace(pid);}
解读:如果进程名是"httpd",会跟踪proc provider中的exec-success
探针并执行trace(pid)
这一action。exec-success
探针常常用于跟踪新进程的创建和系统调用exec()的执行。当前的进程名是用内置变量execname检索出来的,而当前的进程ID则是通过pid。
内置变量
内置变量是用来做计算和判断的,可以通过action打印出来。常用内置变量如下表所示。
变量 | 描述 |
execname | 执行在CPU上进程的名字(字符串) |
uid | 执行在CPU上的用户ID |
pid | 执行在CPU上的进程ID |
timestamp | 当前时间,自启动以来的纳秒数 |
vtimestamp | CPU上的线程时间,单位是纳秒 |
| 探针参数(uint64_t) |
| 探针参数(类型化的) |
curthread | 指向当前线程内核结构的指针 |
probefunc | 探针描述(字符串)的函数组件 |
probename | 探针描述(字符串)的命名组件 |
curpsinfo | 当前进程信息 |
action
常用的action见下表
action | 描述 |
| 打印arg |
| 打印格式化的字符串 |
| 返回来自内核空间的字符串 |
| 返回来自用户空间地址的字符串(需要内核执行一次从用户空间到内核空间的复制操作) |
| 打印内核级别的栈跟踪,如果有count,按count截断 |
| 打印用户级别的栈跟踪,如果有count,按count截断 |
| 从内核程序计数器(pc),返回内核函数名 |
| 从用户程序计数器(pc),返回用户函数名 |
| 退出DTrace并返回状态 |
| 截断聚合变量,或是全部(删除所有的键),或按照指定键的数目(count)做截断 |
| 删除聚合变量的值(键保留) |
| 格式化地打印聚合变量 |
最后三个action使用一种特殊的称为聚合型(aggregation)的变量类型。
变量类型
变量类型表如下,按照使用偏好排列(先是聚合变量,然后按开销从低到高)。
类型 | 前缀 | 作用域 | 开销 | 多CPU安全 | 赋值示例 |
聚合变量 | @ | 全局 | 低 | 是 |
|
带键的聚合变量 | @[] | 全局 | 低 | 是 |
|
从句局部变量 |
| 从句实例 | 非常低 | 是 |
|
线程局部变量 |
| 线程内 | 中等 | 是 |
|
标量 | 无 | 全局 | 中下 | 否 |
|
关联数组 | 无 | 全局 | 中上 | 否 |
|
解读:
- 线程局部变量的作用域是在线程内;
- 子句局部变量用于中间计算,只在针对同一探针描述的action子句中有效;
- 多个CPU同时对同一个标量做写入会损坏变量状态,这不大可能,但确实会发生,对于字符串标量也要同样小心(字符串损坏)。
聚合变量(aggregation)是一类特殊的变量类型,可以由CPU单独计算汇总之后再传递到用户空间。该变量类型拥有最低的开销,是另一种数据汇总的方法。
用于填充聚合变量的action见下表
聚合Action | 描述 |
| 发生计数 |
| 对value求和 |
| 记录value的最小值 |
| 记录value的最大值 |
| 用2的幂次方直方图记录value |
| 用给定最小值、最大值和步进值做线性直方图记录value |
| 用混合对数/线性直方图记录value |
单行命令
跟踪系统调用open(),打印进程名和文件路径名:dtrace -n 'syscall::open:entry { printf("%s %s", execname, copyinstr(arg0)); }'
Oracle Solaris 11很大程度上修改系统调用自陷表(系统调用的provider是构建在此之上的),这样在该系统上跟踪open()就变成:dtrace -n 'syscall::openat:entry { printf("%s %s", execname, copyinstr(arg1)); }'
按进程名归纳所有CPU的交叉调用:dtrace -n 'sysinfo:::xcalls { @[execname] = count(); }'
按99Hz采样内核级栈:dtrace -n 'profile:::profile-99 { @[stack()] = count(); }'
脚本
例如来自DTraceToolkit脚本集合的脚本bitesize.d
,GitHub,根据进程名显示请求的磁盘I/O大小:
#!/usr/sbin/dtrace -s /* 解释器行,表示脚本文件可从命令行执行 */
#pragma D option quiet /* 设置安静模式,压缩DTrace的默认输出 */
/* 打印头 */
dtrace:::BEGIN
{
printf("Tracing... Hit Ctrl-C to end.\n");
}
/* 进程io启动 */
io:::start
{
/* fetch details */
this->size = args[0]->b_bcount;
/* store details */
@Size[pid, curpsinfo->pr_psargs] = quantize(this->size);
}
/* 打印最终报告 */
dtrace:::END
{
printf("\n%8s %s\n", "PID", "CMD");
printa("%8d %S\n%@d\n", @Size);
}
开销
DTrace通过利用每个CPU的内核缓冲区和内核的聚合总结,减小跟踪的开销。默认状态下,DTrace以每秒一次这样一个温和的频率,从内核空间往用户空间传递数据。还有其他减小开销和提高安全性的各种功能,比如有的例程如果发现系统不能响应就会终止跟踪。
跟踪执行的开销是与跟踪的频率和所执行的action息息相关的。跟踪块设备I/O的频率通常不高(1000 I/O每秒或更少),开销是可以忽略的。另一方面,跟踪网络I/O时,当包的速率达到每秒百万次时,就会引起显著的开销。
action也是有代价的。
把数据存入变量也是有开销的,特别是关联数组。
文档和资源
书:
- Dynamic Tracing Guide
- DTrace: Dynamic Tracing in Oracle Solaris, Mac OS X and FreeBSD
资源:
基于DTrace封装的工具:
- execsnoop
- Oracle的ZFS Appliance Analytics
- Joyent公司的Cloud Analytics
SystemTap
SystemTap是专为Linux打造的,对用户级和内核级的代码都提供静态和动态跟踪的功能,采用其他的内核框架做源:静态探针用tracepoints、动态探针用kprobes、用户级别的探针用uprobes。
不足:稳定性问题,一些版本会导致内核崩溃或挂起;启动较慢、错误信息难懂、隐式功能无文档、语言还不够精炼。
将DTrace移植到Linux:Oracle的Oracle Enterprise Linux。
探针
探针:由句号分隔,可选的内置选项(放在括号中),如:
- begin:程序开始;
- end:程序结束;
- syscall.read:系统调用read()的开始;
- syscall.read.return:系统调用read()的结束;
- kernel.function(“sys_read”):内核函数sys_read()的开始;
- kernel.function(“sys_read”).return:内核函数sys_read()的结束;
- socket.send:发送包;
- timer.ms(100):对单一CPU每100ms触发一次的探针;
- timer.profile:按内核时钟频率对所有的CPU都触发的探针,用于采样/剖析;
- process(“a.out”).statement(“*@main.c:100”):跟踪目标进程,可执行文件
a.out
,main.c
的第100行。
tapset
tapset:一组相关的探针。tapset还能附带用来执行action。tapset举例:
- syscall:系统调用;
- ioblock:块设备接口和I/O调度器;
- scheduler:内核CPU调度器事件;
- memory:进程和虚拟内存的使用;
- scsi:SCSI目标的事件;
- networking:网络设备事件,包括接收和传输;
- tcp:TCP协议事件,包括发送和接收事件;
- socket:套接字事件。
action和内置变量
SystemTap还提供许多的action和内置变量:
-
execname()
:可获取进程名; -
pid()
:可获取当前进程ID; -
print_backtrace()
:可打印内核栈的回溯信息。
示例
stap -ve 'global stats; probe syscall.read.return { stats <<< $return; }
probe end { printf("\n\trval (bytes)\n"); print(@hist_log(stats)); }'
解读:
- 单行命令跟踪系统调用read(),将返回读取的大小结果保存成一张2的幂次方的直方图;
- 选项
-v
会打印出编译阶段的详细信息,通知用户跟踪已经开启; - 单行命令以声明全局变量stats作为开始,这是SystemTap所要求的预声明;
- 探针的定义用关键词probe作为开头,匹配系统调用read()的返回;
- action是用变量stats记录返回值
$return
的,用的统计操作符是<<<。采用这样通用的数值记录方式,之后可以用于不同的数据汇总中; - end探针把统计数据作为直方图打印出来;
- 如果不输出直方图,SystemTap在退出时也会打印一个基本的数字总结;
- read()的
$return
值有些是负值,这代表的是返回的错误码errno。
SystemTap对比DTrace
两条等价的单行命令:
stap -e 'global stats; probe syscall.read.return { stats <<< $return; }
probe end { printf("\trval (bytes)\n"); print(@hist_log(stats)); }'
dtrace -n 'syscall::read:return { @["rval(bytes)"] = quantize(arg0); }'
SystemTap脚本举例:
stap -e 'global s; probe syscall.read.return {
if ($return >= 0) { s[execname()] <<< $return; }
}
probe end {
printf("\n%-36s %8s %8s %10s\n", "EXEC", "CALLS", "AVGSZ", "TOTAL");
foreach(k in s+) {
printf("%-36s %8d %8d %10d\n", k, @count(s[k]), @avg(s[k]), @sum(s[k]));
}
} '
解读:这个单行命令根据进程名给出读操作返回大小的统计数据。用三个不同的函数给调用次数CALLS、平均大小AVGSZ(字节)和总大小TOTAL提供数据。若用DTrace来做这件事,需要三个不同的聚合变量,一种类型一个。
区别:
- DTrace不提供if,用谓词作为分支;
- DTrace当前没有循环能力,除了展开循环,出于安全考虑不执行回跳;SystemTap 为循环设置上界,脚本里的无限循环不会在内核上下文里挂起;
- SystemTap可直接访问统计数值,如s[k],而DTrace的聚合变量的打印只能依靠自己或用聚合函数来处理。
开销
当程序首次执行时,SystemTap在编译阶段会消耗几秒CPU资源。SystemTap会将程序缓存下来,这样开销不会每次使用时都发生。还可以在不同的系统上编译SystemTap程序,然后将缓存的结果传输给目标系统。
另一个额外开销是内核分析的内核调试信息,通常不包括在Linux的发行版中(可能是几百兆字节大小)。
文档和资源
书:SystemTap Language Reference。
perf
LPE,Linux Performance Events,Linux性能事件,简称perf,现在所支持的性能观测的范围已相当宽泛。虽然没有DTrace和SystemTap那样的实时编程能力,但perf可执行静态和动态跟踪(基于tracepoint、kprobe和uprobe)、profiling、检查栈跟踪、局部变量和数据类型。已成为Linux内核主线的一部分,最容易使用的(如果已安装),所提供的观测能力足够满足大多数问题排查。
观测工具的观测
观测工具和构建其上的统计都是由软件实现的,而所有的软件都是潜在有Bug的。存在以下问题:
- 工具不总是正确的;
- Man手册页不总是正确的;
- 能用的指标可能不完整;
- 能用的指标可能设计得很差;
当多个观测工具覆盖的范围有重叠时,就能用它们来互相检查。
另一个验证的技术是施加已知的负载,看看观测工具表现得是否与你预计的结果相同。可以用微基准测试工具,用它们的报告结果做比较。
缺少指标比用不合适的指标更难发现。
练习
- 什么是剖析?
- 什么是跟踪?
- 静态跟踪和动态跟踪有什么区别?