使用ebpf分析网络报文传输时延

最近需要分析某个链路上的单向网络延迟,数据的发送端使用一个用户态协议栈,而接收端则使用linux内核协议栈。获取延迟的方式是在发送端在发送报文的尾部添加一个发送时间戳,在接收端获取报文后将接收端时间戳与发送时间戳进行对比,从而获得中间的延迟时间。由于发送端和接收端位于同一设备(使用不同的网卡),又分别处于用户态和内核态,因此使用了两端都能高性能访问的tsc时钟作为时间戳来源。

为了在接收端尽可能早的处理报文,减少软件协议栈的执行时间影响,需要在内核处理报文时就进行报文时间戳的解析和延迟计算。要做到这一点有几种方法:

1. 修改驱动或协议栈代码,直接在其中加入处理逻辑;
2. 使用netfilter框架,在hook点加入自己的处理函数;
3. 使用kprobe,插入自己的处理函数;
4. 使用ebpf,插入自己的处理函数。

上面几种方式中,其他几种方式都需要修改或开发内核代码,在线上服务器上操作风险较大。而ebpf可以将用户态程序直接插入内核中运行,内核会对这些代码进行严格检查,便捷性和安全性上都更好一些。

ebpf的使用过程和原理比其他几种方式略复杂一些,简单介绍如下:

1. 编写ebpf处理逻辑。这个处理逻辑一般是用C(restricted C)写的一个函数,函数参数与函数的插入位置有关。
2. 使用clang+llvm编译成BPF指令。插入内核的函数并不是能直接运行的x86指令,而是一种中间码——BPF指令。
3. 使用bpf系统调用将处理函数的中间码插入到内核指定位置。这里的位置有很多种类型:kprobe、xdp、socket_filter等等,但大体上可以分为两类:一类是xdp这种在特定位置专门提供了hook接口的,另一类是kprobe这种通过特定机制附加到内核函数指定位置的。使用时不同类型的位置能使用的函数接口也会有所不同。
4. 内核在执行到插入函数的位置时,通过JIT虚拟机执行BPF指令。
5. 内核态插入的BPF代码可以和用户态代码通过专门的数据结构BPF_MAP进行通信。用户态代码可以将新的工作和配置传递给内核代码,内核代码也可以将处理的结果和状态传递给用户态做进一步处理和展示。

ebpf最大的优点在于能够通过BPF_MAP在用户态程序和插入内核的bpf函数间进行异步通信,这使得ebpf程序能够完成许多使用内核模块很难完成的功能。

具体开发ebpf程序时,由于自己编译和插入ebpf代码较为复杂,可以通过bcc工具来简化开发工作。bcc是一个python框架,可以在其中编写ebpf函数后通过python封装的接口完成编译、插入和MAP交互等操作。

更详细的ebpf/bcc介绍可以参考如下文献:

[1] https://blog.cyru1s.com/posts/ebpf-bcc.html,最好的(可能也是唯一的)中文ebpf/bcc教程,其中详细介绍了使用bcc开发ebpf程序的方法,并且介绍了不少基础但却关键的(也是别的文献很少提及的)注意点。非常实用。
[2] https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/,ebpf文献大全,从ebpf的历史发展到未来的发展方向,都能在其中找到。
[3] https://www.linuxplumbersconf.org/event/2/contributions/71/attachments/17/9/presentation-lpc2018-xdp-tutorial.pdf,面向XDP开发的介绍,其中也介绍了ebpf的原理和xdp的原理与开发方法。

解析报文、获取时间戳并比较时间差值的逻辑非常简单,但让这不到100行代码正确运行却没有想象中能够简单。ebpf代码虽然可以使用C语言编写,并运行于内核态,但是开发时与开发内核代码有非常大的差异。笔者遇到的问题和建议总结如下:

1. ebpf代码中不能调用内核导出函数接口,只能调用ebpf子系统提供的指定bpf_helper系列函数。因此在ebpf代码中无法通过rdtsc()获取TSC时钟,也就无法和报文中携带的tsc时间戳直接比较。只能通过bpf_ktime_get_ns()接口获取内核ktime,再通过tsc时钟频率换算成tsc时间。
同样的,在ebpf代码中很难去访问skb数据结构的非线性数据段。笔者尝试通过kprobe方式将ebpf函数插入协议栈入口位置,但是通过skb无法访问128字节之后的非线性存储的数据,因为页地址转换等函数都是无法使用的。之后只能使用socket_filter或xdp模式。在这些专门处理网络数据的模式中,系统提供了将数据线性化的功能,从而支持对整个数据包内容的访问。例如在socket_filter模式中可以通过load_byte()接口来读取指定偏移位置的报文内容。
2. 内核加载ebpf代码时会进行严格的校验,对于不符合要求的代码会拒绝载入。最常遇到的例子是通过指针访问数据前没有检查指针合法性。在访问BPF_MAP中的数据元素时,即使我们确定查询的元素一定存在,也必须加入检查指针是否为空的代码,否则无法载入。
3. ebpf代码使用的编译器和编译生成的目标都和普通的x86代码大不相同。因此同样的C语法,在ebpf中的表现可能就和在普通的x86代码中不一样。例如:skb->head[skb->transport_header+1]和*(skb->head+skb->transport_header+1)在普通的内核代码中是等价的,但在ebpf中只有后一种写法是有效的。在ebpf的C代码中写一些比较复杂的代码往往会产生和预期不同的结果。因此编写ebpf的C代码时语法要尽可能简单,不要在一句语句中写太多的类型转换和指针引用之类的操作,复杂逻辑尽量拆成多行简单代码实现。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值