来自《用Linux内核的瑞士军刀-eBPF实现socket转发offload》,请阅读原文。


前面已经写了不少关于eBPF的demo,本文准备再写一个,以适应eBPF吞噬Linux内核的潮流或者说趋势,这个比殴打经理要爽太多了,且不用担负法律责任,万一打不过经理,也不用挨经理的打。

我们已经对eBPF将网络转发offload到XDP(eXpress Data Path)耳熟能详,作为Linux内核的一把 “瑞士军刀” ,eBPF能做的事情可不止一件,它是一个多面手。

socket数据offload问题

通过代理服务器在两个TCP接连之间转发数据是一个非常常见的需求,特别是在CDN的场景下,然而这个代理服务器也是整条路径中的瓶颈之所在,代理服务器的七层转发行为极大地消耗着单机性能,所以,通过代理服务器的七层转发的优化,是一件必须要做的事。

所以,问题来了, eBPF能不能将代理程序的数据转发offload到内核呢? 如果可以做到,这就意味着这个offload可以达到和XDP offload相近的功效:

  • 减少上下文切换,缩短转发逻辑路径,释放host CPU。

这个问题之所以很重要亟待解决,是因为现在的很多机制都不完美:

  • 传统的read/write方式需要两次系统调用和两次数据拷贝。
  • 稍微新些的sendfile方式不支持socket到socket的转发,且仍需要在唤醒的进程上下文中进行系统调用。
  • DPDK以及各种分散/聚集IO,零拷贝技术需要对应用进行比较大的重构,太复杂。

sockmap的引入

Linux 4.14内核带来了sockmap,详见下面的lwn:
BPF: sockmap and sk redirect support: https://lwn.net/Articles/731133/
还有下面的blog也很不错:
https://blog.cloudflare.com/sockmap-tcp-splicing-of-the-future/

又是eBPF!这意味着用sockmap做redirect注定简单,小巧!

我们先看下sockmap相对于上述的转发机制有什么不同,下面是个原理图:

sockmap的实现非常简单,它通过替换sk_data_ready回调函数的方式接管整个数据面的转发逻辑处理。

按照常规,sk_data_ready是内核协议栈和进程上下文的socket之间的数据通道接口,它将数据从内核协议栈交接给了持有socket的进程:

常规处理的sk_data_ready回调函数的控制权转移是通过一次wakeup操作来完成的,这意味着一次上下文的切换。

而sockmap的处理与此不同,sockmap通过一种称为 Stream Parser 的机制,将数据包的控制权转移到eBPF处理程序,而eBPF程序可以实现数据流的Redirect,这就实现了socket数据之间的offload短路处理:

关于 Stream Parser ,详情参见其内核文档:
https://www.kernel.org/doc/Documentation/networking/strparser.txt

实例演示

任何机制能实际run起来才是一个真正的起点,现在又到了实例演示的环节。

我们先从一个简单proxy程序开始,然后我们为它注入基于eBPF的sockmap逻辑,实现proxy的offload转发,从而理解整个过程。

我们的proxy程序非常简单,你可以将它理解成一个socket Bridge,它从一个连接接收数据并简单地将该数据转发到另一个连接,稍微修改一下即可实现socket Hub/Switch以及Service mesh。

socket Bridge代码如下:

// proxy.c
// gcc proxy.c -o proxy
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netdb.h>
#include <signal.h>#define MAXSIZE 100char buf[MAXSIZE];
int proxysd1, proxysd2;static void int_handler(int a)
{close(proxysd1);close(proxysd2);exit(0);
}int main(int argc, char *argv[])
{int ret;struct sockaddr_in proxyaddr1, proxyaddr2;struct hostent *proxy1, *proxy2;unsigned short port1, port2;fd_set rset;int maxfd = 10, n;if (argc != 5) {exit(1);}signal(SIGINT, int_handler);FD_ZERO(&rset);proxysd1 = socket(AF_INET, SOCK_STREAM, 0);proxysd2 = socket(AF_INET, SOCK_STREAM, 0);proxy1 = gethostbyname(argv[1]);port1 = atoi(argv[2]);proxy2 = gethostbyname(argv[3]);port2 = atoi(argv[4]);bzero(&proxyaddr1, sizeof(struct sockaddr_in));proxyaddr1.sin_family = AF_INET;proxyaddr1.sin_port = htons(port1);proxyaddr1.sin_addr = *((struct in_addr *)proxy1->h_addr);bzero(&proxyaddr2, sizeof(struct sockaddr_in));proxyaddr2.sin_family = AF_INET;proxyaddr2.sin_port = htons(port2);proxyaddr2.sin_addr = *((struct in_addr *)proxy2->h_addr);connect(proxysd1, (struct sockaddr *)&proxyaddr1, sizeof(struct sockaddr));connect(proxysd2, (struct sockaddr *)&proxyaddr2, sizeof(struct sockaddr));while (1) {FD_SET(proxysd1, &rset);FD_SET(proxysd2, &rset);select(maxfd, &rset, NULL, NULL, NULL);memset(buf, 0, MAXSIZE);if (FD_ISSET(proxysd1, &rset)) {ret = recv(proxysd1, buf, MAXSIZE, 0);printf("%d --> %d proxy string:%s\n", proxysd1, proxysd2, buf);send(proxysd2, buf, ret, 0);}if (FD_ISSET(proxysd2, &rset)) {ret = recv(proxysd2, buf, MAXSIZE, 0);printf("%d --> %d proxy string:%s\n", proxysd2, proxysd1, buf);send(proxysd1, buf, ret, 0);}}return 0;
}

我们来看一下它的工作过程。

首先起两个netcat,分别侦听两个不同的端口,然后运行proxy程序。在netcat终端敲入字符,就可以看到它被代理到另一个netcat终端的过程了:

我们看到,一次转发经过了两次系统调用(忽略select)和两次数据拷贝。

我们的demo旨在演示基于eBPF的sockmap对proxy转发的offload过程,所以接下来,我们对上述代码进行一些改造,即加入对sockmap的支持。

这意味着我们需要做两件事:

  1. 写一个在socket之间转发数据的eBPF程序,并编译成字节码。
  2. 在proxy代码中加入eBPF程序的加载代码,并编译成可执行程序。

首先,先给出ebpf程序的C代码:

// sockmap_kern.c
#include <uapi/linux/bpf.h>
#include "bpf_helpers.h"
#include "bpf_endian.h"struct bpf_map_def SEC("maps") proxy_map = {.type = BPF_MAP_TYPE_HASH,.key_size = sizeof(unsigned short),.value_size = sizeof(int),.max_entries = 2,
};struct bpf_map_def SEC("maps") sock_map = {.type = BPF_MAP_TYPE_SOCKMAP,.key_size = sizeof(int),.value_size = sizeof(int),.max_entries = 2,
};SEC("prog_parser")
int bpf_prog1(struct __sk_buff *skb)
{return skb->len;
}SEC("prog_verdict")
int bpf_prog2(struct __sk_buff *skb)
{__u32 *index = 0;__u16 port = (__u16)bpf_ntohl(skb->remote_port);char info_fmt[] = "data to port [%d]\n";bpf_trace_printk(info_fmt, sizeof(info_fmt), port);index = bpf_map_lookup_elem(&proxy_map, &port);if (index == NULL)return 0;return bpf_sk_redirect_map(skb, &sock_map, *index, 0);
}char _license[] SEC("license") = "GPL";

上述代码在内核源码树的 samples/bpf 目录下编译,只需要在Makefile中加入以下的行即可:

always += sockmap_kern.o

OK,下面我们给出用户态的测试程序,实际上就是将我们最初的 proxy.c 增加对ebpf/sockmap的支持即可:

// sockmap_user.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <unistd.h>
#include <netdb.h>
#include <signal.h>
#include "bpf_load.h"
#include "bpf_util.h"#define MAXSIZE  1024
char buf[MAXSIZE];
static int proxysd1, proxysd2;static int sockmap_fd, proxymap_fd, bpf_prog_fd;
static int progs_fd[2];
static int key, val;
static unsigned short key16;
static int ctrl = 0;static void int_handler(int a)
{close(proxysd1);close(proxysd2);exit(0);
}// 可以通过发送HUP信号来打开和关闭sockmap offload功能
static void hup_handler(int a)
{if (ctrl == 1) {key = 0;bpf_map_update_elem(sockmap_fd, &key, &proxysd1, BPF_ANY);key = 1;bpf_map_update_elem(sockmap_fd, &key, &proxysd2, BPF_ANY);ctrl = 0;} else if (ctrl == 0){key = 0;bpf_map_delete_elem(sockmap_fd, &key);key = 1;bpf_map_delete_elem(sockmap_fd, &key);ctrl = 1;}
}int main(int argc, char **argv)
{char filename[256];snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);struct bpf_object *obj;struct bpf_program *prog;struct bpf_prog_load_attr prog_load_attr = {.prog_type    = BPF_PROG_TYPE_SK_SKB,};int ret;struct sockaddr_in proxyaddr1, proxyaddr2;struct hostent *proxy1, *proxy2;unsigned short port1, port2;fd_set rset;int maxfd = 10;if (argc != 5) {exit(1);}prog_load_attr.file = filename;signal(SIGINT, int_handler);signal(SIGHUP, hup_handler);// 这部分增加的代码引入了ebpf/sockmap逻辑bpf_prog_load_xattr(&prog_load_attr, &obj, &bpf_prog_fd);sockmap_fd = bpf_object__find_map_fd_by_name(obj, "sock_map");proxymap_fd = bpf_object__find_map_fd_by_name(obj, "proxy_map");prog = bpf_object__find_program_by_title(obj, "prog_parser");progs_fd[0] = bpf_program__fd(prog);bpf_prog_attach(progs_fd[0], sockmap_fd, BPF_SK_SKB_STREAM_PARSER, 0);prog = bpf_object__find_program_by_title(obj, "prog_verdict");progs_fd[1] = bpf_program__fd(prog);bpf_prog_attach(progs_fd[1], sockmap_fd, BPF_SK_SKB_STREAM_VERDICT, 0);proxysd1 = socket(AF_INET, SOCK_STREAM, 0);proxysd2 = socket(AF_INET, SOCK_STREAM, 0);proxy1 = gethostbyname(argv[1]);port1 = atoi(argv[2]);proxy2 = gethostbyname(argv[3]);port2 = atoi(argv[4]);bzero(&proxyaddr1, sizeof(struct sockaddr_in));proxyaddr1.sin_family = AF_INET;proxyaddr1.sin_port = htons(port1);proxyaddr1.sin_addr = *((struct in_addr *)proxy1->h_addr);bzero(&proxyaddr2, sizeof(struct sockaddr_in));proxyaddr2.sin_family = AF_INET;proxyaddr2.sin_port = htons(port2);proxyaddr2.sin_addr = *((struct in_addr *)proxy2->h_addr);connect(proxysd1, (struct sockaddr *)&proxyaddr1, sizeof(struct sockaddr));connect(proxysd2, (struct sockaddr *)&proxyaddr2, sizeof(struct sockaddr));key = 0;bpf_map_update_elem(sockmap_fd, &key, &proxysd1, BPF_ANY);key = 1;bpf_map_update_elem(sockmap_fd, &key, &proxysd2, BPF_ANY);key16 = port1;val = 1;bpf_map_update_elem(proxymap_fd, &key16, &val, BPF_ANY);key16 = port2;val = 0;bpf_map_update_elem(proxymap_fd, &key16, &val, BPF_ANY);// 余下的proxy转发代码保持不变,这部分代码一旦开启了sockmap offload,将不会再被执行。while (1) {FD_SET(proxysd1, &rset);FD_SET(proxysd2, &rset);select(maxfd, &rset, NULL, NULL, NULL);memset(buf, 0, MAXSIZE);if (FD_ISSET(proxysd1, &rset)) {ret = recv(proxysd1, buf, MAXSIZE, 0);printf("%d --> %d proxy string:%s\n", proxysd1, proxysd2, buf);send(proxysd2, buf, ret, 0);}if (FD_ISSET(proxysd2, &rset)) {ret = recv(proxysd2, buf, MAXSIZE, 0);printf("%d --> %d proxy string:%s\n", proxysd2, proxysd1, buf);send(proxysd1, buf, ret, 0);}}return 0;
}

同样的,为了和eBPF程序配套,我们在Makefile中增加下面的行:

hostprogs-y += sockmap
sockmap-objs := sockmap_user.o

最后直接在 samples/bpf 目录下make即可生成下面的文件:

-rwxr-xr-x  1 root root 366840 12月 20 09:43 sockmap
-rw-r--r--  1 root root  12976 12月 20 11:14 sockmap_kern.o

为了验证效果,我们起五个屏,下面是一个演示的过程截图和步骤说明:

可见,proxy转发数据流的逻辑通过一个eBPF小程序从用户态服务进程中offload到了内核协议栈。用户态的proxy进程甚至不会由于数据的到来而被wakeup,这是比sendfile/splice高效的地方。

从上面的demo可以看到,sockmap顾名思义可以对接两个socket,这是eBPF这把 “瑞士军刀” 专门针对socket的一个小器件,这完美解决了sendfile的in_fd必须支持mmap的限制:

demo的代码和演示就到这里,我们再一次看到了eBPF之妙!

附:eBPF-可编程内核利器

我先说下为什么我把eBPF看作一把瑞士军刀:

瑞士军刀,包含小巧的圆珠笔、牙签、剪刀、平口刀、开罐器、螺丝刀、镊子等…

eBPF呢,它可以附着在xdp,kprobe,skb,socket lookup,trace,cgroup,reuseport,sched,filter等功能点,有人可能会说eBPF不如Nginx,不如OpenWRT,不如OVS,不如iptables/nftables…确实,但是这就好比说瑞士军刀不如AK47,不如东风-41洲际导弹,不如Zippo,不如张小泉王麻子,不如苏泊尔一样…

eBPF和瑞士军刀一样,小而全是它们的本色( eBPF严格限制指令数量 ),便携,功能丰富,手艺人离不开的利器。

eBPF让 内核可编程 变的可能!

内核可编程是一个很有意思的事情,它使得内核的一些关键逻辑不再是一成不变的,而是可以通过eBPF对其进行编程,实现更多的策略化逻辑。

目前,eBPF已经密密麻麻扎进了Linux的各个角落,eBPF的作用点还在持续增多,迄至Linux 5.3内核,Linux内核已经支持如下的eBPF程序类型:

enum bpf_prog_type {BPF_PROG_TYPE_UNSPEC,BPF_PROG_TYPE_SOCKET_FILTER,BPF_PROG_TYPE_KPROBE,BPF_PROG_TYPE_SCHED_CLS,BPF_PROG_TYPE_SCHED_ACT,BPF_PROG_TYPE_TRACEPOINT,BPF_PROG_TYPE_XDP,BPF_PROG_TYPE_PERF_EVENT,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,BPF_PROG_TYPE_RAW_TRACEPOINT,BPF_PROG_TYPE_CGROUP_SOCK_ADDR,BPF_PROG_TYPE_LWT_SEG6LOCAL,BPF_PROG_TYPE_LIRC_MODE2,BPF_PROG_TYPE_SK_REUSEPORT,BPF_PROG_TYPE_FLOW_DISSECTOR,BPF_PROG_TYPE_CGROUP_SYSCTL,BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE,BPF_PROG_TYPE_CGROUP_SOCKOPT,
};

一共26种类型,26个作用点。而在不久之前的Linux 4.19内核,这个数值也就22。可见eBPF吞噬内核的速度之快!

后面,我们还会看到eBPF在socket lookup机制所起的妙用。

经理还是要打的,等他穿上西装皮鞋,打上真丝领带的时候再打吧。


浙江温州皮鞋湿,下雨进水不会胖。

eBPF/sockmap实现socket转发offload相关推荐

  1. 用Linux内核的瑞士军刀-eBPF实现socket转发offload

    我们已经对eBPF将网络转发offload到XDP(eXpress Data Path)耳熟能详,作为Linux内核的一把 "瑞士军刀" ,eBPF能做的事情可不止一件,它是一个多 ...

  2. 利用socket转发和反弹端口技术突破防火墙进入内

    TCP端口反弹技术 反弹技术,该技术解决了传统的远程控制软件不能访问装有防火墙和控制局域网内部的远程计算机的难题.反弹端口型软件的原理是,客户端首先登录到FTP服务器,编辑在***软件中预先设置的主页 ...

  3. 实现基于XDP/eBPF的快速路由转发功能

    周末用eBPF实现了学习型网桥的XDP快速转发路径之后,再来用eBPF实现一个快速路由转发.同样很有意思. 关于eBPF和XDP的前置基础知识,我在前面实现网桥转发路径前已经概览过了,所以本文不再赘述 ...

  4. Socket编程(C语言实现)—— Nginx支持Socket转发

    搭建Nginx环境: [root@localhost /]# cd /usr/local/src [root@localhost src]# wget http://nginx.org/downloa ...

  5. eBPF学习仓库bpf_study-996station GitHub鉴赏官

    推荐理由:eBPF学习参考资料,Linux内核观测技术BPF免费下载(英文) "eBPF 是我见过的 Linux 中最神奇的技术,没有之一,已成为 Linux 内核中顶级子模块,从 tcpd ...

  6. 2021年九月上旬文章推荐

    <如何在 Linux 中实时监控日志文件 | Linux 中国> <宋宝华:为了不忘却的纪念,评Linux 5.13内核https://mp.weixin.qq.com/s/SLAl ...

  7. 使用eBPF将网络功能Offload到网卡

    安得广厦千万间,大庇天下寒士俱欢颜,风雨不动安如山! 近五六年,经济突飞猛进,互联网行业更是宇宙无敌,又是AI又是区块链的,如火如荼-人们纷纷从国外回到国内,希望过上神仙般的日子,出国瞬间就成了zz不 ...

  8. 【Java】Socket网络编程实现内网穿透、端口映射转发、内网穿透上网工具的编写,设置IP白名单防火墙

    这里写目录标题 简介 更新 一.背景 1.1 情景假设 1.2 想要达到的目的 1.3 局限 1.3 解决方案一(路由器NAT) 1.4 解决方案二(云服务器转发) 二.方案介绍 2.1 方案简介 2 ...

  9. 大规模微服务利器:eBPF + Kubernetes

    hi, 大家好,微服务,云原生近来大热,在企业积极进行数字化转型,全面提升效率的今天,几乎无人否认云原生代表着云计算的"下一个时代",IT大厂们都不约而同的将其视为未来云应用的发展 ...

最新文章

  1. 只知道GAN你就OUT了——VAE背后的哲学思想及数学原理
  2. 以色列农业奇迹-丰收节贸易会:谋定符合国情制度和方式
  3. java 回调函数很好懂
  4. NameError: name 'long' is not defined
  5. Chainlink预言机正式集成至币安智能链
  6. CODING 最佳实践:快课网研发效能提升之路 1
  7. VS2013用InstallShield生成安装包文件步骤
  8. 作为一个职业达人,你需要水滴石出的专注
  9. curviloft插件怎么用_Curviloft插件下载
  10. 快速了解SOLIDWORKS Simulation的有限元分析法
  11. 2021厦门湖滨中学高考成绩查询,厦门各高中本科上线率2020
  12. Excel公式中的LookUp三剑客(可以取代Vlookup的神秘公式)
  13. html div虚线背景,聊聊css绘制虚线
  14. c# - - - 使用Chloe框架连接PostgreSQL数据库
  15. 逐梦电竞:雷神“光追”游戏电脑新年首发
  16. 【Pygame实战】疫情期间给不能出门的你推荐一款爽游 《消灭病毒保卫城市》【强推】愿早日结束
  17. 1688关键词搜索api(附可用)
  18. 阿里云分布式关系型数据库(DRDS)
  19. this和super
  20. leetcode中等之1843.可疑银行账户

热门文章

  1. 虚拟机VMware和kali的简易安装-2022最新
  2. K-means++算法
  3. AI未来十年新范式,生成式人工智能的挑战与机遇
  4. popen 使用方法
  5. c语言unsigned char转换成unsigned short,有关c语言数据类型转换之char,unsigned char,unsigned short...
  6. c语言:结构体-计算平均成绩
  7. 计算机科学与技术科学的根本问题是,哪些问题是计算科学的基本问题和重大问题呢?...
  8. zbrush常用笔刷_1000款各种样式zbrush实用建模师笔刷包集合
  9. [Python爬虫]中国新说唱 Skr~ Skr~
  10. AI或其他应用软件界面文字太小的解决方案