Please enable Javascript to view the contents

程序员的平行宇宙 —— eBPF 系统级跟踪技术简单入门

 ·  ☕ 5 分钟

image-20210310223153844

Linus Torvalds in 1991

程序员的平行宇宙

程序员有两个世界:

  1. 一个是编码世界,我们很容易认为,我们考虑了一切,也完成了一切的代码。
  2. 然后是运行世界,我们发现,无论我们多么的严谨和考虑一切,世界总是有异常。异常就好像电磁波,一开始,我们只能通过它引发的结果而发现它,而不能直接观察。因为它总出现在那些黑暗的角落,如果我们手中没有电简和电磁波示波器,一切只能依靠猜测和迷信。最后,我们进入了一个用猜测驱动的世界。有时候,我们运气好,猜中了。有时候,我们只能在 release 前多上香。

It is a capital mistake to theorize before one has data. Insensibly one begins to twist facts to suit theories, instead of theories to suit facts.

– Sherlock Holmes in “A Scandal in Bohemia” by Sir Arthur Conan Doyle

在获得数据之前先进行理论分析是一个重大错误。 荒谬的是,人们开始扭曲事实以适应理论,而不是理论去适应事实。

—— 福尔摩斯(Sherlock Holmes)在亚瑟·柯南·道尔爵士(Arthur Conan Doyle)创作的《波西米亚丑闻》

系统跟踪技术

当我们发现应用程序的功能或性能不如预期时,常用应用或操作系统跟踪的方法来定位问题。这里的跟踪包括以下方法:

  1. 非生产环境的 Debug —— 如 java remote debug/gdb/go-delve
  2. 生产环境可用的跟踪 —— 通过 probe(探针)等式方法监控程序内部,对应用的性能影响有限

本文讨论第二种方法 。

一直以来,如何在资源受限的生产环境,或性能测试环境中跟踪有问题的应用程序都是一个难题。最常见的一个题目是,我的程序慢在哪个地方?聪明的程序员和 DevOps 们不断发明了各种工具:DTrace / SystemTAP / perf ,今天我要介绍的是 eBPF 技术和它的工具栈。

一切从函数说起

大家知道,即使的编程语言从汇编发展到现在的Java/GO,有一个基本的编程单位的不变的 —— 函数(方法)。即使我们以为已经用了 OOP(面向对象)、AOP、Serverless ,函数依然是编译器、CPU 指令(X86下的 callq)内部的单位,只是内部的函数名字和我们在源码中看到的名字有一定的前后缀等映射关系。

一般,应用或内核中会存在一个符号表(Symbol Tables)记录了应用中的函数和其在应用中的地址

通过跟踪这些函数的调用信息,如调用入参、返回值、调用次数,响应时间分布,就可以为问题定位提供明确和可信的方向。下面是一个符号表的例子:

1
2
3
$ readelf --dyn-syms /usr/bin/bash | grep 'readline$'
   882: 00000000000b8520   154 FUNC    GLOBAL DEFAULT   16 readline
  1239: 00000000000873a0  1963 FUNC    GLOBAL DEFAULT   16 initialize_readline

发现,bash 有一个内部函数readline。bash 通过它读取每个行 shell 命令。只要监控这个函数的入参,就可以监控系统中的所有 bash 发起的命令。

如何监听函数

在 Java 世界,可以通过 java instrument 来实现函数的修改或监听。在更广义的世界 CPU 指令世界,有相近的方法去修改或监听CPU指令。之前说了,一般,源码中的函数,是会编译为一个X86下的 callq 指令。只要我们替换函数的入口为一个断点指令(int3);然后在断点处理程序中调用定制的监听程序;之后再调用实际的原程序。

如 trace 前的 bash 进程 :

# gdb -p 31817
[...]
(gdb) disas readline
Dump of assembler code for function readline:
0x000055f7fa995610 <+0>:
<rl_pending_input> cmpl $0xffffffff,0x2656f9(%rip) # 0x55f7fabfad10
0x000055f7fa995617 <+7>: push %rbx
0x000055f7fa995618 <+8>: je 0x55f7fa99568f <readline+127>
0x000055f7fa99561a <+10>: callq 0x55f7fa994350 <rl_set_prompt>
0x000055f7fa99561f <+15>: callq 0x55f7fa995300 <rl_initialize>
0x000055f7fa995624 <+20>: mov
<rl_prep_term_function>
0x000055f7fa99562b <+27>: test
0x261c8d(%rip),%rax
# 0x55f7fabf72b8
%rax,%rax
[...]

用 uprobes 技术 trace 了 readline 函数后:

# gdb -p 31817
[...]
(gdb) disas readline
Dump of assembler code for function readline:
>>>> 0x000055f7fa995610 <+0>: int3 0x000055f7fa995611 <+1>: cmp $0x2656f9,%eax <<<<
0x000055f7fa995616 <+6>: callq *0x74(%rbx)
0x000055f7fa995619 <+9>: jne 0x55f7fa995603 <rl_initialize+771>
0x000055f7fa99561b <+11>: xor
%ebp,%ebp
0x000055f7fa99561d <+13>: (bad)
0x000055f7fa99561e <+14>: (bad)
0x000055f7fa99561f <+15>: callq 0x55f7fa995300 <rl_initialize>
0x261c8d(%rip),%rax
# 0x55f7fabf72b8
[...]

Linux 的历史悠久。有好几种的 trace 技术:

技术 需源程序预留 用户进程/内核
kprobes NO 内核
uprobes NO 用户进程
Tracepoints YES 内核
USDT YES 用户进程

而 eBPF 就是使用上面的技术,监听函数的调用,然后抽象为一个事件源(Event Sources):

eBPF Datasource,来源: [BPF Performance Tools]

工具集

eBPF 打开了跟踪的大門。但門槛太高,于是,我们需要一些封装好的库或工具集。比较成熟的有 BCCebpftrace。如果你的的 Linux 发行版本中足够新,这两个工具都可以直接安装。

例子

作为一个自吹为干货,实为抄袭😅的文章,还是上点例子好。

监听整个系统的 bash 执行的命令

假设我们是 bash 的开发,或者,我们了解 bash 的源码,已经知道,bash 通过自己的 readline 函数读取终端的命令。这时,只需要用 uprobe 方法监听 readline 函数的返回值。

以下假设我们在开始监听后,在其它终端中输入了ls /find / 命令。

1
2
3
4
5
6
$ bpftrace -e 'uretprobe:/bin/bash:readline { printf("%s\n", str(retval)); }'
Attaching 1 probe...
ls /
find /
...
^C

你可以通过 bpftrace -l 'uprobe:/bin/bash'readelf 了解一个可执行文件或 so 文件的函数列表。

监听系统的缓存的命中率

BCC 的 cachestat 脚本,通过 kprobe 技术,监听内核的 add_to_page_cache_lru等函数,可以计算出缓存命中率。这个功能在定位数据库 IO 问题时,由为实用(我曾经用它定位 Cassandra 数据库的IO问题)。

1
2
3
4
5
6
7
8
# cachestat
HITS MISSES BUFFERS_MB CACHED_MB
53401 2755
DIRTIES HITRATIO
20953 95.09% 14 90223
49599 4098 21460 92.37% 14 90230
16601 2689 61329 86.06% 14 90381
15197 2477 58028 85.99% 14 90522

监听 TCP 丢包(重传)

无论现代网络硬件和软件如何发展,丢包分析是个永远逃不了的 DevOps 工作。对于 TCP,部分丢包(不是全部对应)可以直接反映在 TCP 重传上。这时,监听内核的 TCP 重传对由为重要。

1
2
3
4
5
6
7
8
# tcpretrans
TIME
PID
IP LADDR:LPORT T> RADDR:RPORT STATE
01:55:05 0 4 10.153.223.157:22 R> 69.53.245.40:34619 ESTABLISHED
01:55:05 0 4 10.153.223.157:22 R> 69.53.245.40:34619 ESTABLISHED
01:55:17 0 4 10.153.223.157:22 R> 69.53.245.40:22957 ESTABLISHED
[...]

可见,列出的发生重传时,丢包的 TCP 连接的双端的 IP 和端口。

最后

以上是最简单的例子。eBPF 世界,才刚开始。后面,我计划说说 eBPF 的应用场景,和它与其它工具如 perf 的比较。再见!Keep tracing !

参考

[BPF Performance Tools]

分享

Mark Zhu
作者
Mark Zhu
An old developer