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

     

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

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

     

    什么是sockmap?

    • SOCKMAP or specifically "BPF_MAP_TYPE_SOCKMAP", is a type of an eBPF map ,A 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 size); 

    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程序,并在不不要链接内核源代码树的情况下加载它们。

    简化版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 *path);
    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

    https://blog.csdn.net/hbhgyu/article/details/108854003

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

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

  • 相关阅读:
    原生时代 来看看十年前李彦宏、马化腾和马云对云计算的评价
    .NET 在云原生时代的蜕变,让我在云时代脱颖而出
    cordov vue项目中调用手机原生api
    Nginx(三)nginx 反向代理
    实现一个简单的Http代理服务器
    微软的反向代理库YARP
    一件代发发货人怎么写?淘宝代理发货流程
    JavaScript常用,继承,原生JavaScript实现classList
    Caddy一个强大的web服务器和代理服务器
    启动tomcat时 错误: 代理抛出异常 : java.rmi.server.ExportException: Port already in use: 1099的解决办法
  • 【推广】 阿里云小站-上云优惠聚集地(新老客户同享)更有每天限时秒杀!
    【推广】 云服务器低至0.95折 1核2G ECS云服务器8.1元/月
    【推广】 阿里云老用户升级四重礼遇享6.5折限时折扣!
  • 原文地址:https://www.cnblogs.com/codestack/p/13938135.html
走看看 - 开发者的网上家园