lttng学习笔记

摘要

Ceph随处可见lttng的影子,那么lttng到底是什么呢?你可以把它当作一个优化过的,速度超级快的printf函数,借助它你就能调试目标代码,或者用来分析程序的性能。这篇博文主要介绍lttng相关的概念,原理以及如何使用它进行跟踪程序。

概念


上图显示了lttng中几个重要的概念以及它们之间的关系

会话(session)

当使用lttng跟踪某个或多个程序的运行时,首先得创建一个session,然后再在这个session里面指定你要跟踪的tracepoint事件。你可以同时创建多个session,这些session可以并行且互不干扰地跟踪系统中运行的程序,所以session的意义就在于为你提供了独立的跟踪环境。

域(domain)

创建了一个session之后,首先得告诉lttng你想要跟踪的tracepoint事件,而这些tracepoint事件可能来源于内核中,也可能来源于应用层程序,因此lttng将事件来源分为如下几个domain:

  • Linux kernel
  • User space
  • java.util.loggin会话(session)
  • log4j

之所以需要指明trace事件所在的domain,一个是因为trace事件可能重名,另外是因为不同的domain支持的特性也不一样。

通道(channel)

Channel是一个事件的集合,它最基本的作用就在于维护一个环状的共享缓冲区,负责记录产生的trace事件,并交由后台“消费者”进程进行处理。每个Channel都有它自带的参数,例如子缓存区的大小和数量,负责的trace事件等,你可以通过参数来调整lttng的性能。

当你在当前session中指定了需要监控的事件时,就会创建至少一个channel负责记录该事件。你也可以在一个domain中创建多个channel,且它们都负责同一个trace事件,这样的后果就是,当你查看最终的trace结果时,一个事件出现了多次。

channel loss mode

Lttng将channel中的环状缓冲区划分为多个小块的子缓冲区。产生的事件会被顺序写入某个子缓冲区直到它被写满(如上图中的黄色的子缓冲区)此时该缓冲区就会被标记为consumable,下一个空闲的缓冲区就会被用来接收trace事件。

一般来说产生trace事件的速率远小于消耗的速率,但是也不排除产生速率超过消耗速率的可能,此时很可能所有缓冲区已经填满,无法接收接下来出现的trace事件。但是lttng的设计的初衷就是要构建一个对程序性能影响很小,且不会产生阻塞的trace工具,所以这种情况下,lttng选择丢弃一些trace事件避免阻塞的发生。Lttng有两种丢弃方式:

  • discard mode:直接丢弃最新产生的trace事件;
  • overwrite mode:直接覆盖掉最老的trace事件,从而腾出可用的子缓冲区来存放新的trace事件。

采取哪种方式依赖于你想要达到的目的,但需要注意的是,当你选择覆盖的方式时,被腾出来的子缓冲区中的所有trace事件都会被丢弃掉。

子缓冲区的大小和数量

Channel可以通过参数来控制子缓冲区的大小和数量。
Lttng使用ctf格式来记录trace事件,这意味着trace数据以很紧凑的方式组织在一起。例如,内核产生的事件的平均大小只有32字节。所以1MB大小的缓冲区已经算是很大的了。在设定子缓冲区大小的时候需要特别注意的是,lttng在子缓冲区间进行切换有很大的CPU开销,所以:

  • Trace产生速率大时:倾向于使用大缓冲区以降低事件丢失的可能,这也意味着降低了缓冲区切换的频率;
  • Trace产生速率低时:倾向于使用小缓冲区,此时缓冲区切换的开销可忽略不计;
  • 内存受限的系统:首先需要降低的是缓存区的数量,其次才是缓冲区的大小。应该尽量使用大缓冲区,从而降低缓冲区切换的频率。

在“数量少,但子缓冲区大”和“数量多,但子缓冲区小”之间有一个权衡,即子缓冲区切换频率和overwrite mode下数据丢失多少之间的权衡。例如下图中两个配置分别为2*4MB和8*1MB的channel,后者切换的开销是前者的两倍,但是overwrite发生时只会有1/8的trace的丢失,而前者将丢失1/2的trace。

在discard模式下,子缓冲区的数量没有太大的意义。

子缓冲区切换的计时器

Channel还可以设定一个计时器。当计时器超时时,即便正在使用的子缓冲区还没填充完,也会发生子缓冲区的切换。这样做的好处就是,当trace事件产生的速率实在是太低时,lttng仍然能够周期性地进行trace事件的提交,而不是一直等待对应的子缓冲区填满为止。

缓冲区模式

对于应用层的程序,有两个缓冲区模式:

  • Per-PID buffering: 每个进程一个缓冲区
  • Per-UID buffering: 一个用户的所有进程共享一个缓冲区

前者将会耗用更多的缓冲区,但是可以避免进程之间的相互干扰,例如某个产生trace很快的进程只会填满自己的缓冲区,而影响其它的进程。

事件(Event)

前面一直在说trace事件,那么trace事件到底是什么呢?在平时写程序的时候,大家总喜欢往里面添加printf函数,可以借此来跟踪调试程序的执行;如果通过printf打印当前的时间,还可以分析关键模块的性能。Lttng也有类似与printf的函数:

tracepoint(provider, tracepoint_name, \*);

当程序执行tracepoint函数的时候就会产生相应的trace事件,其中provider::tracepoint_name是trace事件的名称,*则表示伴随该trace事件的其它信息(类似于printf需要打印的字符串以及各类数值等)。那些被你指定需要监控的trace事件就会被记录到channel的共享缓冲区中。

组件

有了以上概念,读者肯定能够猜想到lttng大概的工作原理,这里将继续介绍lttng的软件组成。

Lttng大概可分为3个部分,内核trace模块(LTTNG-modules),应用层库模块(LTTNG-UST),以及lttng的后台进程和相关控制工具(LTTNG-tools)。下图中蓝色部分表示的组件属于LTTNG-tools,浅绿色部分的组件属于LTTNG-UST,赤色部分的属于LTTNG-modules。

lttng-sessiond

Lttng中最为关键的部分为lttng-sessiond后台进程,它是整个lttng的核心。它负责管理所有的session以及session内部包含的信息(拥有的channel,channel的配置参数以及能的事件等)。同时通过内部通信协议来控制其它组件。

Lttng-sessiond的一个很重要的职责就是记录当前可用的trace事件类型。应用程序在启动的时候通过助LTTNG-UST库连接到Lttng-sessiond,并注册应用程序拥有的trace事件类型,例如前面提到过的provider::tracepoint_name;和应用程序不一样的,内核模块会在lttng-sessiond启动的时候自动被加载,而且lttng-sessiond会主动拉取内核事件类型,而不是让内核来注册。

Lttng-sessiond会创建一个本地的socket,用户通过该socket向lttng-sessiond发送控制命令。liblttng-ctl库将底层的通信协议进行了包装,用户可以借助它提供的api来控制lttng-sessiond。

Lttng-consumerd

Lttng-consumerd就是传说中的“消费者”进程。它与应用层和内核共享缓冲区,并使用这些共享缓冲区收集应用程序或内核产生的trace事件,并将这些trace数据输出到指定文件。当你在一个session中指定要跟踪的trace事件类型时,lttng-sessiond就会在相应的domain中创建一个lttng-consumerd进程,由该进程收集该domain中产生的trace事件。

Lttng-consumerd并不会因为session销毁而销毁,它会继续保留而被下一个session适应。Lttng-consumerd进程是lttng-sessiond的子进程,所以lttng-sessiond的退出会导致lttng-consumerd的退出。

lttng-ust

应用程序通过Lttng-ust库提供的宏来来定义trace事件,同时借助Lttng-ust库提供提供的动态链接库,在应用程序启动的时候与lttng-sessiond和lttng-consumerd进行通信,从而注册trace事件的类型,并在产生trace事件的时候将事件写入到共享缓冲区。

使用

这里只举一个简单的例子,说明如何通过lttng-ust提供的头文件和库定义trace事件类型,如何编译链接目标程序,以及如何使用lttng来跟踪程序。更多的例子请看我的github

定义traces事件类型

概念:

  • tracepoint provider: 用来定义trace事件的命名空间
  • tracepoint:定义的trace事件类型

定义一个tracepoint有点像定义一个函数,首先需要指明它所属的类(tracepoint provider)以及tracepoint本身的名称,接着还需要定义它的输入参数,以及输出结果。例如下面的用来定义trace类型的tp.tp文件:

1
2
3
4
5
6
7
8
9
10
TRACEPOINT_EVENT(osd, // provider的名称
do_osd_op_pre_copy_get,// tracepoint的名称
TP_ARGS( // 输入参数列表
const char*, oid,
uint64_t, snap),
TP_FIELDS( // 输出列表
ctf_string(oid, oid)
ctf_integer(uint64_t, snap, snap)
)
)

tp.tp是一个模板文件,借助lttng提供的lttng-gen-t工具,可以生成所需的tp.h,tp.c以及tp.o文件:
lttng-gen-t tp.tp

接下来需要做的就是让应用程序调用lttng版的printf,即tracepoint()函数,下面是main.c文件:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include “tp.h”
int main()
{
int count = 0;
while(count++ < 10)
tracepoint(osd, do_osd_op_pre_copy_get, “xxxx”, count);
return 0;
}

main每次执行tracepoint(),就会产生一个类型为osd:do_osd_op_pre_copy_get的trace事件。

编译链接

先将main.c编译成object文件,然后和前面得到的tp.o文件链接到一起构成完成的程序:

1
2
gcc –c main.c
gcc –o app main.o tp.o

这样一来你就得到了最终的程序app

使用lttng跟踪

  • 创建一个名为mytrace的session:lttng create mytrace
  • 指定要监控的trace事件类型:

    lttng enable-event –u osd:do_osd_op_pre_copy_get

    其中-u表明domain是userspace,osd:do_osd_op_pre_copy_get则是上面刚刚定义的trace事件类型。使用lttng list mytrace查看mystrace的信息,可以看到session中自动创建了一个名为channel0的channel;使用px x|grep lttng可以看到lttng-sessiond自动创建了一个后台lttng-consumerd进程。

  • 开始监控trace事件:lttng start mytrace
  • 运行上面得到的应用程序:./app
  • 停止监控trace,并显示监控的内容:lttng stop && lttng view。你将得到如下的结果:

    1
    2
    3
    [13:22:35.923931026] (+0.000001907) ceph-2 osd:do_osd_op_pre_copy_get: { cpu_id = 6 }, { oid = "XXXX", snap = 0 }
    [13:22:35.923937914] (+0.000006888) ceph-2 osd:do_osd_op_pre_copy_get: { cpu_id = 6 }, { oid = "XXXX", snap = 1 }
    [13:22:35.923938934] (+0.000001020) ceph-2 osd:do_osd_op_pre_copy_get: { cpu_id = 6 }, { oid = "XXXX", snap = 2 }

显示了每条trace事件产生的时间,名称,cpu,以及携带的输出信息。

高级功能

前面介绍channel的时候说过,用户可以通过参数的方式来控制channel的特性,如子缓冲区的大小和数量,共享buffer的模式(PUI还是PID)以及trace的loss mode等。
而前面使用lttng跟踪程序时,一旦指明要监控的trace事件的类型,lttng就会自动为你创建对应的channel,该channel使用的是默认配置(可以通过lttng list session_name来查看channel的配置信息)。你可以使用类似如下命令来创建你要需要的channel:

1
lttng enable-channel --userspace –buffers-pid –overwrite --num-subbuf 16 --subbuf-size 512k big-channel

该命令创建了一个名为big-channel的channel,它负责监控应用层的事件(一个channel不能同时监控不同domain的事件),采用PID模式,即每个进程一个buffer,使用overwrite的loss mode,拥有16个子缓冲区,每个缓冲区大小为512KB。

在产生trace的同时,还可以携带其它的信息,如处理器的信息,和性能计数器的信息等,通过如下命令可以指定输出trace的时候顺便携带的系统信息:

1
lttng add-context --userspace --type vpid --type perf:thread:cpu-cycles

该命令支持在输出trace的同时,还要输出产生trace的进程号和cpu-cycles,下面是对应的输出:

1
[14:10:34.553324304] (+0.000008726) ceph-2:app:7962 osd:do_osd_op_pre_copy_get: { cpu_id = 4 }, { vpid = 7962, perf_thread_cpu_cycles = 281474976719897 }, { oid = "XXXX", snap = 1 }