July 18, 2019
本文由 Brendan Gregg 提供
在2019 Linux Storage, Filesystem, and Memory-Management Summit (LSFMM)峰会里,我有一个keynote介绍了我使用bpftrace在Netflix server上debug过程中,利用BPF查看各种信息如何方便。本文我会针对kernel开发者介绍利用bpftrace分析的过程,帮助他们掌握更便利的代码分析方法。
最近我在跟另一位开发者讨论tcp_sendmsg(),他比较担心过大的message size(例如超过100MB)可能会导致出错。100MB?? 我不太相信Netflix会在实际server上使用这么大的网络包。大家应该都很熟悉这个函数的定义,来自net/ipv4/tcp.c:
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size);
bpftrace在Netflix的在线服务器上已经安装好了(很多公司都自带安装好了),所以我直接ssh到一个繁忙的服务器上,想看看每10秒时间内tcp消息包的长度的分布:
最大在128~256KB之间。这里使用kproble(“k”)监控tcp_sendmsg(),然后保存参数2(size)的柱状图统计信息到名为@size的BPF map中,这里名字其实不重要,只是用来在输出时能更加易懂。并且配置了10秒钟的监控时长,时间到了后会退出,此时把所有BPF map都打印出来。
会不会有出错?:
可以看出没有出错过。这里我使用了kretprobe("kr"),统计retval各种值的发生次数,它要么是一个负数的错误之,要么是size。因为我不关心返回的size,所以使用了一个三元操作把所有正数都设为0。
bpftrace可以让你快速回答这些问题,并且是在真正运行的生产系统server上,不需要使用debuginfo(Netflix通常不会安装)。这个例子表明Netflix这种workload场景下不太可能碰到这里所说的tcp_sendmsg()过大的问题https://github.com/iovisor/bcc/issues/2440
此前关于tcp_sendmsg()分析的一行命令已经展示了bpftrace对kernel分析的能力。还有一些其他一行命令能完成很多功能。仍以tcp_sendmsg()为例(你使用的时候可以用在其他kernel function上)。
列出tcp_sendmsg() size超过8192字节的情况,每次都打印出来:
列出一个直方图来展示每个进程(包括PID和进程名comm)发出的size:
统计各种返回值的发生频次:
展示每秒钟的统计信息,包括调用次数,平均size,总字节数:
统计各种类型stack trace的次数:
统计各种类型的stack trace,并且回溯3层stack:
统计函数调用延迟,展示为纳秒数据的柱状图:
最后一个例子会对每个probe(利用thread ID做索引)保存时间戳,并且在另一次probe触发时提取前面的时间戳做计算。这种用法可以用在各种延迟测算上。
上述的一行命令例子里面都缺少一个重要的功能:查看struct。这里再列一下函数原型:
bpftrace可以利用arg0-argN用来指示kprobe函数参数,简单来说它们就是指调用函数的时候的参数寄存器(例如arg2就是x86_64平台上的%rdx寄存器的值)。
bpf有能力读取kernel头文件,这些头文件在生产系统上也都是安装好的,这样只要能包含正确的头文件并且转换好参数,就能够访问struct的成员数据了:
这个例子就是使用bpftrace打印出tcp_sendmsg()的地址信息,大小,返回值。相应的输出会是类似这样:
tcp_sendmsg.bt文件的源代码如下:
这里的kprobe中,sk和size都保存在一个per-thread-ID的maps里面,这样当tcp_sendmsg()返回时就可以从kretprobe里面访问到这些信息。kretprobe解析sk并且打印其中的信息。如果是IPv4包,就用bpftrace的ntop()函数来把地址转换成字符串。目标端口则会把网络字节序转换成主机字节序(译者注:这里应该是指big endian和little endian转换)。为了简化起见,这里我没有考虑IPv6,不过你应该可以自己添加IPv6支持(ntop()支持IPv6地址)。
现在有人在做工作让bpftrace支持BPF Type Format (BTF)信息,这样就能带来更多好处,例如kernel头文件里看不到的struct结构也能支持了。
Advance example
目前演示的都是tracing调试方法。在这个更高级的例子里面我会演示off-CPU profiling(指的是统计每次进程释放CPU切换出去再切换回来的时候的过程)。
对某个进程进行CPU profiling通常很容易:我只要对stack进行采样,检查performance-monitor counter(PMC)以及model相关的寄存器(MSRs),就能看到CPU上在运行的是什么东西,以及为什么这么慢了。不过Off-CPU profiling也有不少麻烦之处。我能在context switch(上下文切换)的时候显示stack被阻塞在哪个调用上,不过通常只能看到是阻塞在一些常见的调用API上(例如select(), epoll(), 或者某个锁),但我们其实想要更进一步的信息。
BPF最终也提供出了一个解决方案:保存stack trace以及后续提取出来分析的能力(我很希望DTrace有这个功能可惜一直没有)。下面就是bpftrace实现方案,能显示进程的名字,被block时的stack信息,唤醒者的stack,以及毫秒为单位的阻塞时间柱状图:
上面我只保留了一对stack输出信息。这里能看出ssh进程阻塞在select()上,而唤醒者的stack能看出它在等一个网络报文。柱状图可以看出这条切换出去再到被唤醒切换回CPU的过程中,通常都花费了多少毫秒的时间。
其中offwake.bt的源代码如下:
这里try_to_wake_up()的kernel stack会按照它的进程ID来保存起来,后面在finish_task_switch()的时候会提取这些信息来做计算和统计。这其实就是BCC里的offwaketime工具以及kernel代码里的samples/bpf/offwaketime*的简化实现,只要使用bpftrace就能做到了。
我之前所做的Performance@Scale talk演讲里面介绍过这个问题以及相应的BPF解决方案,当时还演示了这些stack信息如何被展示成火焰图(flame graph)。
有时你需要知道谁唤醒那个唤醒者,以及再之前是谁唤醒的,这么一串的过程。通过遍历wakeup chain,就能建立一个“chain graph”把这一路上的latency来自哪里都展示出来(通常是来自中断)。
Tracepoints
此前的例子都是使用kprobe,而kprobe都会随着kernel升级来更新的。
tracepoint比起kprobe来说更适合bpftrace程序,尽管它们其实也会随着kernel版本而改变。反正还是比kprobe要好,因为kprobe可能会被随意更改,甚至可能因为被inline处理了所以直接消失。不过有些kprobe要比其他的kprobe更加稳定一些,特别是kernel内部接口相关的,例如VFS,struct file_operations,struct proto等等。
下面给个简单的例子,如下是timer:hrtimer_start tracepoint能提供的参数:
那我们也来统计一下函数参数各个值的使用次数:
tracepoint的参数可以通过args来访问,这个例子里面我使用ksym() bpftrace函数来返回kernel里这个地址对应的符号名称。
More examples and information
我在LSFMM主题演讲里面逐步演示了Netflix系统服务器上的调试例子。大家还可以从bpftrace的git仓库里tools目录下找到更多的例子。在LSFMM会议上我还展示了很多我为后面Addison-Wesley出版社要出版的BPF分析书准备的示例工具,也都可以在这本书的网页上看到 http://www.brendangregg.com/bpf-performance-tools-book.html
建议你参照这里(https://github.com/iovisor/bpftrace/blob/master/INSTALL.md )的安装文档,获取最新的0.9.1或更新版本。针对各种Linux发行版也有相应的安装包。像Netflix和Facebook等公司都有他们自己的安装包,你也可以自己从代码编译。bpftrace目前使用LLVM和Clang来做依赖检测(BCC也是这么做的),不过今后版本里面可能就不是必须的了。
也请查看bpftrace Cheat Sheet(速查表,http://www.brendangregg.com/BPF/bpftrace-cheat-sheet.html )了解此语言的总结,还有完整的bpftrace Reference Guide参考手册(https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md )。
What about perf, ftrace?
工作中我会一直选择最好用的工具,并不会局限于bpftrace一个。很多时候会用perf或者ftrace。
举例来说:
要统计很多个函数的调用次数的时候:使用ftrace,因为它能快速初始化,BPF kprobe加载的时候也会更加快,特别是能用上perf_event_open()的multi-attach功能的时候。
函数流程tracing:使用ftrace function graphing功能。
PMC statistics:使用perf,统计CPU所提供的performance monitor counter。
对CPU周期采样并提供时间戳:使用perf。因为它的perf.data输出格式优化的非常棒。
就最后一个例子来说,有时候我需要每个采样点都要有时间戳,不过大多数时候并不需要,这样我就使用bpftrace。例如按照99Hz的频率对kernel stack进行采样:
Linux随着时间发展,变化非常大。现在它有了很强大的tracer,也就是bpftrace,它具有从底层开发出来的extend PBF和Linux支持,也能解决Netflix和其他公司在生产系统上的真正问题。使用仅仅一行命令,或者很短小的工具,就可以用各种特有方法来查看你的代码。本文里面已经提供了很多样例。
如果你想知道你的代码在Netflix生产环境服务器上运行的如何,可以给我发个email(译者注:这句话是针对kernel开发者说的),提供你的bpftrace program,我可以直接把相应的命令输出发回给你(必须是在不会暴露公司或者客户的具体信息的情况下)。如果能帮你查出来Linux执行我们Netflix的workload的时候有哪些低效的地方,并且你又能提供一个fix,那对Netflix更加有好处了。我的邮件是[email protected]。
[感谢Alastair Robertson 创建了 bpftrace, 感谢bpftrace, BCC, and BPF 社区过去5年的成果和贡献]
全文完
LWN文章遵循CC BY-SA 4.0许可协议。
极度欢迎将文章分享到朋友圈长按下面二维码关注:Linux News搬运工,希望每周的深度文章以及开源社区的各种新近言论,能够让大家满意~