使用ebpf跟踪rpcx微服务

标签: dev | 发表时间:2022-05-24 00:00 | 作者:
出处:http://itindex.net/relian

ebpf是一种创新的革命性技术,它能在内核中运行沙箱程序, 而无需修改内核源码或者加载内核模块。将 Linux 内核变成可编程之后,就能基于现有的(而非增加新的)抽象层来打造更加智能、 功能更加丰富的基础设施软件,而不会增加系统的复杂度,也不会牺牲执行效率和安全性。

BPF的第一个版本在1994年问世。我们在使用tcpdump工具编写规则的时候其实就使用到它了,该工具用于查看或”嗅探”网络数据包。

使用ebpf技术,你可以从安全、跟踪&性能分析、网络、观测&监控等方向提供新的思路和技术:

  • 安全:可以从系统调用级、packet层、socket层进行安全检查,比如开发DDOS防护系统,编写防火墙程序。
  • 网络:可以开发内核层高性能包处理程序,比如Cilium提供内核层的负载均衡,把service mesh往更深层推进,解决sidecar的性能问题。
  • 跟踪&性能分析: Linux提供多种类型的探针点(probe point),比如Kernel probes、perf events、Tracepoints、User-space probes、User statically defined tracepoints、XDP等等,我们可以编写probe程序收集这些探针点的信息,所以我们可以通过这种方式跟踪程序,分析性能。
  • 观测&监控: 对这些探针点的持续观测和监控,我们可以丰富我们的trace程序。关键是,我们不需要更改既有的程序,而是通过ebpf方法从其它程序进行观测。2014年,著名的内核黑客Alexei Starovoitov对BPF的功能进行了扩展。他增加了寄存器的数量和程序允许的大小,增加了JIT编译,并创建了一个用于检查程序是否安全的程序。然而,最令人印象深刻的是,新的BPF程序不仅能够在处理数据包时运行,而且能够响应其他内核事件,并在内核和用户空间之间来回传递信息。Alexei Starovoitov的新版本的BPF被称为eBPF(e代表扩展:extended)。但现在,它已经取代了所有旧版的BPF用法,并且已经变得非常流行,为了简单起见,它仍然被称为BPF。

你可以自己编写bpf程序,进行定制化的逻辑处理和分析,也可以使用大神们写好的工具,利用这些工具对程序进行通用的性能分析和跟踪。本文主要介绍使用一些工具对rpcx微服务程序进行通用的分析,既然是通用的,你可以可以对其它的Go程序进行分析,而且不仅限于Go程序,其它应用程序甚至内核你可以进行分析和跟踪。

自己编写bpf程序我准备再新开一篇文章介绍。

这一次主要介绍 bcc提供的相关工具bpftrace

bcc是用于创建基于eBPF的高效内核跟踪和操作程序的工具包,其中包括一些有用的命令行工具和示例。 BCC简化了用C进行内核检测的eBPF程序的编写,包括LLVM的包装器以及Python和Lua的前端。它还提供了用于直接集成到应用程序中的高级库。

bpftrace是Linux eBPF的高级跟踪语言。它的语言受awk和C以及DTrace和SystemTap等以前的跟踪程序的启发。 bpftrace使用LLVM作为后端将脚本编译为eBPF字节码,并利用BCC作为与Linux eBPF子系统以及现有Linux跟踪功能和连接点进行交互的库。

简单的 rpcx 微服务程序

既然要使用ebpf分析程序,首先我们要有一个程序。这里我选取了 rpcx一个最简单的例子,实现一个乘法的最小的微服务。

这个程序的代码可以在 rpcx-examples-102basic下载到。

服务端的程序如下:

      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
      
packagemain
import(
"context"
"flag"
"fmt"
example"github.com/rpcxio/rpcx-examples"
"github.com/smallnest/rpcx/server"
)
var(
addr = flag.String("addr","localhost:8972","server address")
)
typeArithstruct{}
// 使用ebpf跟踪这个服务调用
func(t *Arith) Mul(ctx context.Context, args example.Args, reply *example.Reply) error {
reply.C = args.A * args.B
fmt.Println("C=", reply.C)
returnnil
}
funcmain() {
flag.Parse()
s := server.NewServer()
s.RegisterName("Arith",new(Arith),"")
err := s.Serve("tcp", *addr)
iferr !=nil{
panic(err)
}
}

使用 go build server.go编译出 server程序并运行( ./server)。

客户端程序如下:

      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
      
packagemain
import(
"context"
"flag"
"log"
"time"
"github.com/smallnest/rpcx/protocol"
example"github.com/rpcxio/rpcx-examples"
"github.com/smallnest/rpcx/client"
)
var(
addr = flag.String("addr","localhost:8972","server address")
)
funcmain() {
flag.Parse()
d, _ := client.NewPeer2PeerDiscovery("tcp@"+*addr,"")
opt := client.DefaultOption
opt.SerializeType = protocol.JSON
xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, opt)
deferxclient.Close()
args := example.Args{
A:10,
B:20,
}
for{
reply := &example.Reply{}
err := xclient.Call(context.Background(),"Mul", args, reply)
iferr !=nil{
log.Fatalf("failed to call: %v", err)
}
log.Printf("%d * %d = %d", args.A, args.B, reply.C)
time.Sleep(time.Second)
}
}

客户端每一秒会调用 Arith.Mul微服务一次,微服务的逻辑也很简单,就是执行乘法,并把结果返回给客户端。

跟踪和分析微服务

作为演示,本文只跟踪服务端 Arith.Mul调用情况。

bcc提供了很多的基于bpf的分析程序,如下图(大神Brendan Gregg整理的经典图)


这里我们会选取几个相关的工具演示如何使用这些工具分析运行中的程序。 注意是运行中的程序,我们并没有给程序添加额外的一些埋点。

bcc套件

首先你得安装bcc套件,而且你的Linux内核还要足够新,在一些大厂的机房内,还有一些内核版本的2.6.x服务器,这些老的内核服务器不能支持ebpf或者ebpf的新特性。

我是在我的阿里云的一台虚机上测试的,它的版本是:

  • Linux lab 4.18.0-348.2.1.el8_5.x86_64
  • CentOS Stream release 8

直接 yum install bcc-tools就可以安装这些工具。

如果你是其它的版本的操作系统,你可以参考bcc的安装文档进行安装: bcc/INSTALL

在使用工具分析之前,你首先要知道你的微服务 Arith.Mul在符号表中的名称,你可以使用objdump查询到:

      
1
2
      
[root@lab server]# objdump -t server|grep Mul|grep main
000000000075a5e0 g F .text00000000000000d0 main.(*Arith).Mul

它的名称是 main.(*Arith).Mul,下面我们会使用这个名称分析这个微服务。

确保刚才的服务器一直在运行中。

funccount

funccount 用来统计一段时间内某个函数的调用次数。

在server所在的目录下执行下面的命令(如果在不同的路径,你需要更改命令参数中程序的路径):

      
1
2
3
4
5
6
7
      
[root@lab server]# funccount -d 10 './server:main.*.Mul'
Tracing1functionsfor"b'./server:main.*.Mul'"...Hit Ctrl-C to end.
FUNC COUNT
b'main.(*Arith).Mul'10
Detaching...
[root@lab server]#

这里我们设置观察时间是10秒,可以看到在这10秒内,这个函数被调用了10次。

它包含几个参数,比如你可以持续观察,每5秒输出一次结果:

      
1
2
3
4
5
6
      
[root@lab server]# funccount -Ti 5 './server:main.*.Mul'
Tracing1functionsfor"b'./server:main.*.Mul'"... Hit Ctrl-C to end.
18:08:29
FUNC COUNT
b'main.(*Arith).Mul'5

我们甚至可以用它进行Go GC相关函数的跟踪:

      
1
2
3
4
5
6
      
[root@lab server]# funccount -d 10 './server:runtime.*.gc*'
Tracing21functionsfor"b'./server:runtime.*.gc*'"... Hit Ctrl-C to end.
FUNC COUNT
b'runtime.(*gcControllerState).update'2
b'runtime.mallocgc'250

抑或是跟踪Go运行时的调度:

      
1
2
3
4
5
6
      
[root@lab server]# funccount -d 10 './server:runtime.schedule'
Tracing1functionsfor"b'./server:runtime.schedule'"... Hit Ctrl-C to end.
FUNC COUNT
b'runtime.schedule'20
Detaching...

funclatency

funclatency统计函数的执行的耗时情况。
如果我们想分析 Arith.Mul方法执行的情况,我们可以使用下面的命令,它会用直方图的形式展示这个函数调用的耗时分布:

      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
      
[root@labserver]# funclatency -d 10 './server:main.*.Mul'
Tracing1functionsfor"./server:main.*.Mul"...Hit Ctrl-Ctoend.
Function= b'main.(*Arith).Mul'[359284]
nsecs:count distribution
0->1:0| |
2->3:0| |
4->7:0| |
8->15:0| |
16->31:0| |
32->63:0| |
64->127:0| |
128->255:0| |
256->511:0| |
512->1023:0| |
1024->2047:0| |
2048->4095:0| |
4096->8191:0| |
8192->16383:0| |
16384->32767:7|****************************************|
32768->65535:3|***************** |
avg =31978nsecs,total:319783nsecs,count:10

我们统计了10秒的数据。可以看到期间这个函数被调用了10次。平均耗时31微秒。

如果我们想检查线上的程序有没有长尾的现象,使用这个工具很容易分析统计。

funcslower

funcslower 这个工具可以跟踪内核和程序的执行慢的函数,比如使用下面的命令:

      
1
2
3
4
5
6
7
      
[root@lab server]# funcslower -u 10 './server:main.(*Arith).Mul'
Tracing function calls slower than10us... Ctrl+C to quit.
COMM PID LAT(us) RVAL FUNC
server35928444.750./server:main.(*Arith).Mul
server35928430.970./server:main.(*Arith).Mul
server35928433.380./server:main.(*Arith).Mul
server35928431.280./server:main.(*Arith).Mul

你甚至可以打印出堆栈信息:

      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
      
[root@lab server]funcslower -UK -u10'./server:main.(*Arith).Mul'
Tracing function calls slower than10us... Ctrl+C to quit.
COMM PID LAT(us) RVAL FUNC
server35928431.200./server:main.(*Arith).Mul
b'runtime.call64.abi0'
b'runtime.reflectcall'
b'reflect.Value.call'
b'reflect.Value.Call'
b'github.com/smallnest/rpcx/server.(*service).call'
b'github.com/smallnest/rpcx/server.(*Server).handleRequest'
b'github.com/smallnest/rpcx/server.(*Server).serveConn.func2'
b'runtime.goexit.abi0'
server35928432.230./server:main.(*Arith).Mul
b'runtime.call64.abi0'
b'runtime.reflectcall'
b'reflect.Value.call'
b'reflect.Value.Call'
b'github.com/smallnest/rpcx/server.(*service).call'

tcp 系列工具

bcc提供了一堆的对tcp的跟踪情况,我们可以针对不同的场景选择使用相应的工具。

  • tools/tcpaccept: 跟踪TCP被动连接 (accept()).
  • tools/tcpconnect: 跟踪TCP主动的连接 (connect()).
  • tools/tcpconnlat: 跟踪TCP主动连接的延迟(connect()).
  • tools/tcpdrop: 跟踪内核的TCP包的丢包细节.
  • tools/tcplife: 跟踪TCP session(生命周期指标汇总).
  • tools/tcpretrans: 跟踪TCP重传.
  • tools/tcprtt: 跟踪TCP来回的耗时.
  • tools/tcpstates: 跟踪TCP session状态的改变.
  • tools/tcpsubnet: 按子网汇总和聚合TCP发送情况.
  • tools/tcpsynbl: 显示TCP SYN backlog的情况.
  • tools/tcptop: 按主机汇总TCP send/recv吞吐情况.
  • tools/tcptracer: 跟踪TCP 建立/关闭连接的情况 (connect(), accept(), close()).
  • tools/tcpcong: 跟踪TCP套接字拥塞控制状态持续时间.

比如我们如果关注连接的建立情况,可以使用 tcptracer:

      
1
2
3
4
5
6
7
8
9
10
11
      
[root@lab lib]# tcptracer
Tracing TCP established connections. Ctrl-C to end.
T PID COMM IP SADDR DADDR SPORT DPORT
C360005client4127.0.0.1127.0.0.1431268972
X360005client6[::1] [::1]430108972
A359284server4127.0.0.1127.0.0.1897243126
X360005client4127.0.0.1127.0.0.1431268972
X359284server4127.0.0.1127.0.0.1897243126
C360009client4127.0.0.1127.0.0.1431308972
X360009client6[::1] [::1]430148972
A359284server4127.0.0.1127.0.0.1897243130

另外还有一堆的 xxxxsnoop程序,可以对特定的系统调用进行跟踪。

bpftrace

有时候,我们想使用脚本实现一些定制化的跟踪,比如类似awk这样的工具,可以提供简单的脚本编写。

bpftrace就是这样的工具, 它使用LLVM作为后端将脚本编译为eBPF字节码,并利用BCC作为与Linux eBPF子系统以及现有Linux跟踪功能和连接点进行交互的库。

bpftrace参考手册可以在 bpftrace reference_guide找到。

以我们的 Arith.Mul为例,我们可以使用下面的命令,在函数调用时加入探针,把输入的参数打印出来:

      
1
2
3
4
5
      
[root@lab server]# bpftrace -e'uprobe:./server:main.*.Mul {printf("%s - %s: arg1: %d, arg2: %d\n", comm, func, arg0, arg1)}'
Attaching1probe...
server - main.(*Arith).Mul: arg1:10, arg2:20
server - main.(*Arith).Mul: arg1:10, arg2:20
server - main.(*Arith).Mul: arg1:10, arg2:20

为什么arg0,arg1就能把参数打印出来呢?简单说,我们的微服务参数正好是两个int64的整数,正好对应arg0,arg1。

rpcx的服务返回值也是当做参数传入的,函数调用的时候还没有设置,所以你如果打印arg3并不是reply返回值。

这个时候我们需要移动探针,加一个偏移量,加多少的偏移量呢?通过反汇编我们看到加92时返回值已经赋值了,所以使用下面的命令就可以打印返回值了(这个时候第一个参数就被覆盖掉了):

      
1
2
3
4
5
      
[root@labserver]# bpftrace -e 'uprobe:./server:main.*.Mul+92 {printf("%s - %s: reply: %d\n", comm, func, arg0)}'
Attaching1probe...
server- main.(*Arith).Mul: reply:200
server- main.(*Arith).Mul: reply:200
server- main.(*Arith).Mul: reply:200

Go自1.17开始已经改成了基于寄存器的调用惯例,所以这里使用了内建的arg0、arg1、..., 如果你使用更早的Go版本,这里你可以换成sarg0,sarg1,...试试(stack arguments)。

参考文档

  1. https://www.brendangregg.com/blog/2017-01-31/golang-bcc-bpf-function-tracing.html
  2. https://golangexample.com/library-to-work-with-ebpf-programs-from-golang/
  3. https://tonybai.com/2020/12/25/bpf-and-go-modern-forms-of-introspection-in-linux/
  4. https://networkop.co.uk/post/2021-03-ebpf-intro/
  5. https://medium.com/bumble-tech/bpf-and-go-modern-forms-of-introspection-in-linux-6b9802682223#db17
  6. https://blog.px.dev/ebpf-http-tracing/
  7. https://www.ebpf.top/post/ebpf_and_go/
  8. https://www.ebpf.top/

相关 [ebpf 跟踪 rpcx] 推荐:

使用ebpf跟踪rpcx微服务

- - IT瘾-dev
ebpf是一种创新的革命性技术,它能在内核中运行沙箱程序, 而无需修改内核源码或者加载内核模块. 将 Linux 内核变成可编程之后,就能基于现有的(而非增加新的)抽象层来打造更加智能、 功能更加丰富的基础设施软件,而不会增加系统的复杂度,也不会牺牲执行效率和安全性. BPF的第一个版本在1994年问世.

eBPF编程指北

- - IT瘾-dev
这里以 Ubuntu 20.04 为例构建 eBPF 开发环境:. 主流的发行版在对 LLVM 打包的时候就默认启用了 BPF 后端,因此,在大部分发行版上安 装 clang 和 llvm 就可以将 C 代码编译为 BPF 对象文件了. 用 LLVM 将 C 程序编译成对象文件(ELF). 用户空间 BPF ELF 加载器(例如 iproute2)解析对象文件.

聊聊最近很火的eBPF - 知乎

- -
如果非要说当前计算机领域最有前途的两个基础软件技术,那非eBPF和wasm莫属了. Linux内核一直是实现监视/可观察性,网络和安全性的理想场所. 不幸的是,这通常是不切实际的,因为它需要更改内核源代码或加载内核模块,并导致彼此堆叠的抽象层. eBPF是一项革命性的技术,可以在Linux内核中运行沙盒程序,而无需更改内核源代码或加载内核模块.

深入浅出eBPF - 你要了解的7个核心问题

- -
过去一年,ARMS基于eBPF技术打造了Kubernetes监控,提供多语言无侵入的应用性能,系统性能,网络性能观测能力,验证了eBPF技术的有效性. eBPF技术和生态发展很好,未来前景广大,作为该技术的实践者,本文目标是通过回答7个核心问题介绍eBPF技术本身,为大家解开eBPF的面纱. eBPF是一个能够在内核运行沙箱程序的技术,提供了一种在内核事件和用户程序事件发生时安全注入代码的机制,使得非内核开发人员也可以对内核进行控制.

请暂时抛弃使用 eBPF 取代服务网格和 sidecar 模式的幻想

- - Jimmy Song - 宋净超的个人博客 – 博客
最近 eBPF 技术在云原生社区中持续火热,在我翻译了《 什么是 eBPF 》之后,当阅读“云原生环境中的 eBPF”之后就一直在思考 eBPF 在云原生环境中究竟处于什么地位,发挥什么样的作用. 当时我评论说“eBPF 开启了上帝视角,可以看到主机上所有的活动,而 sidecar 只能观测到 pod 内的活动,只要搞好进程隔离,基于 eBPF 的 proxy per-node 才是最佳选择”,再看到 William Morgan 的 这篇文章.

基于 eBPF 的开源项目 eCapture 介绍:无需 CA 证书抓 https 网络明文通讯

- - IT瘾-dev
eCapture 是一款基于 eBPF 技术实现的用户态数据捕获工具. 不需要 CA 证书,即可捕获 https/tls 的通讯明文. 项目在 2022 年 3 月中旬创建,一经发布,广受大家喜爱,至今不到两周已经 1200 多个 Star. 不需要 CA 证书,即可捕获 HTTPS/TLS 通信数据的明文.

Servlet – 会话跟踪

- - ImportNew
HTTP本身是 “无状态”协议,它不保存连接交互信息,一次响应完成之后即连接断开,下一次请求需要重新建立连接,服务器不记录上次连接的内容.因此如果判断两次连接是否是同一用户, 就需要使用 会话跟踪技术来解决.常见的会话跟踪技术有如下几种:. URL重写: 在URL结尾附加. 会话ID标识,服务器通过会话ID识别不同用户..

开源跟踪软件 Prey

- bin - 开源中国社区最新软件
Prey 可让你全时跟踪你的电话和笔记本,如果它丢了或者被偷了,可以帮你找到它们.

使用Google Analytics跟踪404页面

- dwfeng - 月光博客
  404页面是当访问者输入了错误的地址或者访问了被删除的页面时,服务器返回的错误页面(404 HTTP 状态代码). 这个页面除了告诉访问者页面不存在以外,不提供任何有价值的信息.   了解404页面的信息非常有用,可以发现访问者要查找的内容和推介来源,有助于网站补充新的内容并修复有问题的链接. 如何使用Google Analytics来追踪并显示404页面的情况.

美国如何跟踪本拉登

- Qike - Solidot
那是一栋价值百万美元的建筑物,建于2005年,位于巴基斯坦伊斯兰堡以北35英里的一个退役军官居住的富裕区,被认为专为本拉登建造. 它的围墙高18英尺高,没有外窗,有两道安全门,三楼阳台还有一道7英尺高的私人防护墙. 找到这座建筑物与本拉登最信任的一位信使有关. 美国拘留的恐怖分子嫌疑人提供了这位信使的假名;四年前他们获悉了他的真名;两年前,他们找到了他的居住之地.