首页 > 其他分享 >云原生haproxy 代理-ebpf

云原生haproxy 代理-ebpf

时间:2022-10-04 17:00:17浏览次数:82  
标签:haproxy 原生 bpf ebpf map BPF eBPF PROG TYPE

  在如下网络层面下,代理(比如Envoy nginx )执行额外的L7策略(Health checks, service discovery, load balancing, mutual TLS),其开销比较大,主要体现在传统的TCP/IP协议栈路径比较冗余,导致其开销比较大;就像同一主机上unix域 socket比 udp socket 快一样。

云原生haproxy 代理-ebpf_加载

 

为了解决此问题;目前最新内核中引入了ebpf-socket-map 来加速数据包的转发

最后实现类似下面数据包转发流程;非常类似于socketpair unix域这些设计理念

 

云原生haproxy 代理-ebpf_包过滤_02

什么是sockmap?

  • SOCKMAP or specifically "BPF_MAP_TYPE_SOCKMAP", is a type of an eBPF mapA sockmap is a BPF map type that holds references to sock structs. Then with a new sk redirect bpf helper BPF programs can use the map to redirect skbs between sockets,

  也就是sockmap是ebpf中BPF_PROG_TYPE_SK_SKB的一种, 是BPF_PROG_TYPE_SK_SKB程序类型中的一个应用BPF_MAP_TYPE_SOCKMAP;

  ​​https://lwn.net/Articles/731133/​​  functionality is used in conjunction with a sockmap - a special-purpose BPF map that contains references to socket structures and associated values. sockmaps are used to support redirection. The program is attached and the bpf_sk_redirect_map() helper can be used to carry out the redirection,

那怎么理解eBPF

  • (e)BPF能够在内核态执行用户提供的程序

 

The bpf() system call

使用bpf()这个系统调用函数配合​​BPF_PROG LOAD​​命令来加载程序。它的原型是:

 

int bpf(int cmd, union bpf_attr *attr, unsigned int

​bpf_attr union​​​ 允许在内核和用户空间之间传递数据;确切的格式取决于 ​​cmd​​ 这个参数。

​size​​​ 这个参数表示​​bpf_attr union​​ 这个对象以字节为单位的大小。

可以使用命令创建和修改eBPF maps数据结构,这个数据结构一个通用键值对数据结构,用于在eBPF程序和内核或用户空间之间通信。附加命令允许将eBPF程序附加到控制组目录或套接字文件描述符,遍历所有map键值对和程序,并将eBPF对象保存到文件中,以便加载它们的进程终止时,不会销毁它们(后者使用了分类器tc的代码,因此eBPF程序无需加载过程持续运行就可以持久化。、

 全部命令列表可以在bpf() man手册中找到。虽然有许多不同的命令,但它们可以被分成三类:

  • 使用eBPF程序的命令
  • 使用eBPF maps的命令
  • 同时使用程序和maps的命令(统称为对象)。

 

eBPF程序类型

函数​BPF_PROG_LOAD​加载的程序类型规定了四件事:

  • 程序可以附加在哪里
  • 验证器允许调用内核中的哪些帮助函数
  • 网络包的数据是否可以直接访问
  • 作为第一个参数传递给程序的对象类型实际上

程序类型本质上定义了一个API。甚至还创建了新的程序类型,以区分允许调用的不同的函数列表(比如​​BPF_PROG_TYPE_CGROUP_SKB​​​ 对比 ​​BPF_PROG_TYPE_SOCKET_FILTER​​)。

目前内核支持的eBPF程序类型列表如下所示:

BPF_PROG_TYPE_SOCKET_FILTER,                    

一种网络数据包过滤器;attach一个bpf程序到socket上,你可以获取到被socket处理的所有数据包。socket过滤不允许你修改这些数据包以及这些数据包的目的地。仅仅是提供给你观察这些数据包。在你的程序中可以获取到诸如protocol type类型等。

  • BPF_PROG_TYPE_KPROBE,

kprobes是内核提供的动态探测的功能;BPF kprobe program 类型允许你使用BPF程序作为一个kprobe的执行程序。定义为BPF_PROG_TYPE_KPROBE,BPF虚拟机确定你的kprobe程序是否合法。当你写一个kprobe的bpf程序类型时,你需要确定kprobe是在程序的第一条指令执行还是在最后完成时执行。例如:如果你想检查exec系统调用的参数,你就需要把它attach到程序的开始:SEC(“kprobe/sys_exec”)。当你需要检查exec的返回值时,你需要这样指定:SEC(“kretprobe/sys_exec”).。理论上/proc/kallsyms下面的方法都是可以被probe程序执行的。

BPF_PROG_TYPE_SCHED_CLS,                             一种网络流量控制分类器

BPF_PROG_TYPE_SCHED_ACT,          一种网络流量控制动作        

BPF_PROG_TYPE_TRACEPOINT,

bpf-tracepoint类型的程序attach到kernel预先定义好的traceponit上。相比于krobe,它是不灵活的,因为需要kernel预先定义好tracepoints。但是他们是很稳定的。所有的traceponits在内核的/sys/kernel/debug/tracing/events下面可以看到。比较有意思的是:BPF还定义了自己的tracepoints,因此你可以写BPF程序来检查另一个bpf程序的行为。bPF的tracepoints定义在/sys/kernel/debug/tracing/events/bpf下面。例如:有一个tracepoints叫做bpf_prog_load。这就意味着你可以写bpf的代码去检查bpf程序load的过程。

BPF_PROG_TYPE_XDP,从设备驱动程序接收路径运行的网络数据包过滤器

BPF_PROG_TYPE_PERF_EVENT,确定是否应该触发perf事件处理程序

BPF_PROG_TYPE_CGROUP_SKB,一种用于控制组的网络数据包过滤器

BPF_PROG_TYPE_CGROUP_SOCK,一种由于控制组的网络包筛选器,它被允许修改套接字选项

BPF_PROG_TYPE_LWT_IN,用于轻量级隧道的网络数据包过滤器

BPF_PROG_TYPE_LWT_OUT,用于轻量级隧道的网络数据包过滤器

BPF_PROG_TYPE_LWT_XMIT,用于轻量级隧道的网络数据包过滤器

BPF_PROG_TYPE_SOCK_OPS,

一个用于设置套接字参数的程序

BPF_PROG_TYPE_SK_SKB,

一个用于套接字之间转发数据包的网络包过滤器

BPF_PROG_TYPE_CGROUP_DEVICE,

确定是否允许设备操作

BPF_PROG_TYPE_SK_MSG

These types of programs let you controlwhether a message sent to a socket should be delivered 当内核创建了一个socket,它会被存储在前面提到的map中。当你attach一个程序到这个socket map的时候,所有的被发送到那些socket的message都会被filter.在filter message之前,内核拷贝了这些data,因此你可以读取这些message,而且可以给出你的决定:例如,SK_PASS和SK_DROP。

 

eBPF 数据结构

  eBPF程序使用的主要数据结构是eBPF map(键值对)数据结构,这是一种通用的数据结构,允许在内核内部或内核与用户空间之间来回传递数据。正如名称“map”所暗示的,数据是使用键存储和检索的。

  使用bpf()系统调用创建和操作map数据结构。成功创建map后,将返回与该map关联的文件描述符。每个map由四个值定义:类型、元素的最大个数、值大小(以字节为单位)和键大小(以字节为单位)。有不同的map类型,每种类型都提供不同的行为和一些权衡:

  • ​BPF_MAP_TYPE_HASH​​: 一种哈希表
  • ​BPF_MAP_TYPE_ARRAY​​: 一种为快速查找速度而优化的数组类型map键值对,通常用于计数器
  • ​BPF_MAP_TYPE_PROG_ARRAY​​: 与eBPF程序相对应的一种文件描述符数组;用于实现跳转表和处理特定(网络)包协议的子程序
  • ​BPF_MAP_TYPE_PERCPU_ARRAY​​: 一种基于每个cpu的数组,用于实现展现延迟的直方图
  • ​BPF_MAP_TYPE_PERF_EVENT_ARRAY​​​: 存储指向​​perf_event​​数据结构的指针,用于读取和存储perf事件计数器
  • ​BPF_MAP_TYPE_CGROUP_ARRAY​​: 存储指向控制组的指针
  • ​BPF_MAP_TYPE_PERCPU_HASH​​: 一种基于每个CPU的哈希表
  • ​BPF_MAP_TYPE_LRU_HASH​​: 一种只保留最近使用项的哈希表
  • ​BPF_MAP_TYPE_LRU_PERCPU_HASH​​: 一种基于每个CPU的哈希表,只保留最近使用项
  • ​BPF_MAP_TYPE_LPM_TRIE​​: 一个匹配最长前缀的字典树数据结构,适用于将IP地址匹配到一个范围
  • ​BPF_MAP_TYPE_STACK_TRACE​​: 存储堆栈跟踪信息
  • ​BPF_MAP_TYPE_ARRAY_OF_MAPS​​: 一种map-in-map数据结构
  • ​BPF_MAP_TYPE_HASH_OF_MAPS​​: 一种map-in-map数据结构
  • ​BPF_MAP_TYPE_DEVICE_MAP​​: 用于存储和查找网络设备的引用
  • ​BPF_MAP_TYPE_SOCKET_MAP​​: 存储和查找套接字,并允许使用BPF帮助函数进行套接字重定向

可以使用​​bpf_map_lookup_elem()​​​函数和​​bpf_map_update_elem()​​函数从eBPF程序或用户空间程序访问所有map对象

如何编写一个eBPF程序

目前可以将C语言写的程序通过​​LLVM Clang​​​编译器,编译成字节码。然后可以使用bpf()系统调用函数和​​BPF_PROG_LOAD​​命令,直接加载包含这个字节码的对象文件。

通过使用Clang编译器,配合​​-march=bpf​​​参数,就可以用C语言编写自己的eBPF程序了。在内核代码的 ​​samples/bpf/​​​ 目录下有很多eBPF程序的示例,它们的文件名称大部分都具有「​​_kern.c​​​」的后缀。Clang编译出来的目标文件(eBPF字节码),需要由在本机运行的一个程序进行加载(这些示例的文件名称中通常具有「​​_user.c​​」)。为了更容易地编写eBPF程序,内核提供了libbpf库,其中包括用于加载程序、创建和操作eBPF对象的帮助函数。举个例子,一个eBPF程序和使用libbpf库的用户程序的抽象的工作流程一般像如下这样的:

  • 读取eBPF字节码到用户应用程序中的缓冲区,并将其传递给​​bpf_load_program()​​函数
  • eBPF程序,当在内核运行时,它将调用​​bpf_map_lookup_elem()​​函数来查找map中的元素,并存储新值给这个元素。
  • 用户应用程序调用​​bpf_map_lookup_elem()​​函数来读取eBPF程序存储在内核中的值。

但是,上面提到的所有的样例代码都有一个主要缺点:您需要从内核源代码树中编译你的eBPF程序。幸运的是,BCC项目就是为了解决这个问题而诞生的。它包括一个完整的工具链,用于编写eBPF程序,并在不不要链接内核源代码树的情况下加载它们。

云原生haproxy 代理-ebpf_加载_03

 

 

简化版BPF Map创建方式

相对于直接使用bpf系统调用函数来创建BPF Map,在实际场景中常用的是一个简化版:

struct bpf_map_def SEC("maps") my_bpf_map = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(int),
.value_size = sizeof(int),
.max_entries = 100,
.map_flags = BPF_F_NO_PREALLOC,
};
这个简化版看起来就是一个BPF Map声明,它是如何做到声明即创建的呢?关键点就是SEC("maps"),学名ELF惯例格式(ELF convention),它的工作原理是这样的:
声明ELF Section属性 SEC("maps") (之前的博文里有对Section作用的描述)
内核代码bpf_load.crespect目标文件中所有Section信息,它会扫描目标文件里定义的Section,其中就有用来创建BPF Map的SEC("maps"),我们可以到相关代码里看到说明:
// https://elixir.bootlin.com/linux/v4.15/source/samples/bpf/bpf_load.h#L41
/* parses elf file compiled by llvm .c->.o
* . parses 'maps' section and creates maps via BPF syscall // 就是这里
* . parses 'license' section and passes it to syscall
* . parses elf relocations for BPF maps and adjusts BPF_LD_IMM64 insns by
* storing map_fd into insn->imm and marking such insns as BPF_PSEUDO_MAP_FD
* . loads eBPF programs via BPF syscall
*
* One ELF file can contain multiple BPF programs which will be loaded
* and their FDs stored stored in prog_fd array
*
* returns zero on success
*/
int load_bpf_file(char
  1. bpf_load.c扫描到SEC("maps")后,对BPF Map相关的操作是由load_maps函数完成,其中的bpf_create_map_node()和bpf_create_map_in_map_node()就是创建BPF Map的关键函数,它们背后都是调用了定义在内核代码tools/lib/bpf/bpf.c中的方法,而这个方法就是使用上文提到的BPF_MAP_CREATE命令进行的系统调用。
  2. 最后在编译程序时,通过添加​​bpf_load.o​​​作为依赖库,并合并为最终的可执行文件中,这样在程序运行起来时,就可以通过声明​​SEC("maps")​​即可完成创建BPF Map的行为了。

从上面梳理的过程可以看到,这个简化版虽然使用了“语法糖”,但最后还是会去使用bpf()函数完成系统调用。

如何操作BPF Map

BPF Map也有自己的CRUD,除了​​bpf_map_create​​是创建BPF Map操作之外,下面列出了其他主要操作,

  • ​bpf_map_lookup_elem(map, key)​​函数,通过key查询BPF Map,得到对应value
  • ​bpf_map_update_elem(map, key, value, options)​​函数,通过key-value更新BPF Map,如果这个key不存在,也可以作为新的元素插入到BPF Map中去
  • ​bpf_map_get_next_key(map, lookup_key, next_key)​​函数,这个函数可以用来遍历BPF Map,下文有具体的介绍。

参考文档:

​https://blogs.oracle.com/linux/notes-on-bpf-1​

​https://lwn.net/Articles/740157/​

​https://lwn.net/Articles/731133/​

​https://lwn.net/Articles/810297/​

​https://blog.cloudflare.com/sockmap-tcp-splicing-of-the-future/​

​https://www.ibm.com/developerworks/cn/linux/l-lo-eBPF-history/index.html​

​http://arthurchiao.art/blog/ebpf-and-k8s-zh/​

​ https://davidlovezoe.club/wordpress/archives/tag/ebpf​

http代理服务器(3-4-7层代理)-网络事件库公共组件、内核kernel驱动 摄像头驱动 tcpip网络协议栈、netfilter、bridge 好像看过!!!! 但行好事 莫问前程 --身高体重180的胖子



标签:haproxy,原生,bpf,ebpf,map,BPF,eBPF,PROG,TYPE
From: https://blog.51cto.com/u_15404950/5731392

相关文章