一、TUN/TAP设备原理:
Linux的TUN/TAP设备是一种可以使得应用层与TCP/IP协议栈交互的驱动模块,通常用于组建虚拟局域网中的点对点隧道(Tunnel),可以工作于2层(TAP设备)和3层(TUN设备)数据交换。其工作原理图如下:
1. 数据发送流程
-
应用程序通过socket向192.168.1.22地址发送数据{DATA}
-
数据进入内核协议栈,协议栈构造IP报文,生成{IP{DATA}}数据,并且分发到虚拟网卡
-
虚拟网卡将数据{IP{DATA}}转发到TUN驱动
-
TUN App从TUN驱动读取到{IP{DATA}}数据,
-
TUN App通过物理socket发送数据到内核协议栈,
-
内核协议栈对数据进行IP报文封装{IP{IP{DATA}}},传送到物理网卡
-
数据经过物理网卡送出MAC帧报文{MAC{IP{IP{DATA}}}}
上述流程中:
-
1为socket write操作(用户空间到内核空间的数据交换)
-
2,3均为内核数据交换
-
4为fd read操作(内核空间到用户空间数据交换)
-
5为socket write操作(用户空间到内核空间数据交换)
-
6,7均为内核数据交换
2. 数据接收流程
-
物理网卡收取到来自网络的MAC帧报文数据包{MAC{IP{IP{DATA}}}},解包
-
数据包{IP{IP{DATA}}}进入内核协议栈,进行IP层解包生成{IP{DATA}}数据
-
根据数据包地址,数据被分配到TUN App的物理socket
-
TUN App从socket读出数据{IP{DATA}},然后写入TUN 驱动
-
TUN 驱动根据写入的IP包目标地址将地址送入对应的虚拟网卡
-
虚拟网卡将{IP{DATA}}送入内核协议栈
-
协议栈解包成{DATA}送到应用程序的socket
上述流程中:
-
1,2均为内核数据交换
-
3为socket read操作(内核空间到用户空间数据交换)
-
4为fd write操作(用户空间到内核空间数据交换)
-
5,6均为内核数据交换
-
7为socket read操作(内核空间到用户空间数据交换)
为了对比,下图显示了直连情况下网络数据的收发流程:
3. 性能损耗点
对比直连网络,通过TUN隧道实现的数据流程中,发送流程中多出了一次TUN fd的读+一次socket写,接收流程中多出了一次socket的读+一次TUN fd的写,这些操作均由运行于用户态的TUN App完成,并且均涉及到用户态到内核态的数据交换,所以性能相比直连网络来说一定会有损耗。我们需要通过一定的测试方法来量化TUN隧道网络的性能跟直连网络的差距,并且找到瓶颈所在,然后提出可行的优化方法。
二、性能测试方法:
-
实现一个底层通过UDP建立隧道的TUN Proxy App(分别采用epoll和select模型实现)
-
在在两台机器上部署TUN Proxy App,组成VLAN
-
使用SCP程序在两台主机之间通过物理连接进行文件copy操作,记录传输性能参数
-
使用SCP程序在两台主机之间通过TUN隧道进行文件copy操作,记录传输性能参数
-
对比使用物理直连网络和TUN隧道网络传输的性能差异
采用epoll模型的TUN Proxy App的核心逻辑如下:
void packet_switch_epoll::process() { |
三、测试结果:
1. scp通过局域网直连,传输10次结果如下:
sample.txt 100% 512MB 47.9MB/s 00:10 |
2. scp通过vnet-agent(epoll实现),传输10次结果如下:
sample.txt 100% 512MB 27.3MB/s 00:18 |
3. scp通过vnet-agent(select实现),传输10次结果如下:
sample.txt 512MB 19.0MB/s 00:26 |
4. 综合对比
结论:
1. 通过TUN隧道会导致网络传输性能损耗超过50%,
2. TUN Proxy App由于只管理有限的socket数量,所以采用select和epoll模型并没有太大性能差异
四、瓶颈分析:
TUN设备驱动内核层的数据交换损耗相对网络IO和用户态和内核态的数据交换来说可以忽略,所以加入TUN隧道代理之后,整体网络性能损耗都是由于运行于应从层的TUN Proxy App引入的。
TUN Proxy App的主要工作为在TUN设备fd和物理连接socket之间进行数据转移:
-
从TUN设备fd读取要发送的数据到用户空间,然后写入物理连接socket(内核空间)
-
从物理连接socket读取要收取的数据到用户空间,然后写入TUN设备fd(内核空间)
可以看到以上性能损耗主要存在于数据在用户态和内核态之间的交换。
通过Profiler工具采集scp的发送端的TUN Proxy App分析可以验证如下结果:
耗时主要集中在转发数据的函数中:
int forward_with_rw(uint8_t* buf, int size, int in_fd, int out_fd) {
// paratmers
int rlen = 0;
int wlen = 0;
int left = 0;
int offset = 0;
// read data from source
rlen = ::read(in_fd, buf, size);
if (rlen <= 0) {
loge() << "failed to read data from in fd:" << strerror(errno);
return -1;
}
logv() << "<<<< read " << rlen << " bytes from in fd";
// write all source data to destination
left = rlen;
offset = 0;
while (left) {
wlen = ::write(out_fd, buf + offset, left);
if (wlen <= 0) {
loge() << "failed to write data to out fd:" << strerror(errno);
return (rlen - left);
}
logv() << ">>>> write " << wlen << " bytes to out fd";
offset += wlen;
left -= wlen;
}
return wlen;
}
五、解决方案:
TUN Proxy App的性能问题主要是由于在两个fd之间进行数据转移时候产生了两次跨用户/内核态的数据copy,优化方向就是使用更高效的方法来实现在不同fd之间进行zero-copy的数据转移。
1. Linux系统提供了以下技术来实现不同fd之间高效的数据转移方式:
1.sendfile
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)
sendfile可以直接在内核完成把文件内指定range的数据转移到目标fd内,无需在内核态和用户态之间进行切换。该技术的限制是out_fd必须为一个支持类似mmap功能的文件描述符,socket和tun fd均无法支持,所以此技术不适用。
2.splice
ssize_t splice(int fd_in, off64_t *off_in,
int fd_out, off64_t *off_out,
size_t len, unsigned int flags);
splice可以利用一个中间pipe实现把数据在fd_in和fd_out之间进行转移,数据操作均发生在内核态,无需切换。实现代码如下:
int forward_with_splice(int in_fd, int out_fd, int pipe[2]) {
int rc = 0;
// move data from in fd to pipe
rc = splice(in_fd, nullptr, pipe[1], nullptr, 4096, SPLICE_F_MOVE | SPLICE_F_MOVE);
if (rc <= 0) {
loge() << "failed to move data from in fd to pipe:" << strerror(errno);
return -1;
}
// move data from pipe to out fd
rc = splice(pipe[0], nullptr, out_fd, nullptr, rc, SPLICE_F_MOVE | SPLICE_F_MOVE);
if (rc <= 0) {
loge() << "failed to move data to pipe to out fd" << strerror(errno);
return -1;
}
return rc;
}
splice的要求是fd_in或者fd_out之一必须为pipe,而另一个fd的内核实现中必须支持splice_read, splice_write, sendpage三个文件操作,但是tun fd并没有实现这三个操作,所以目前slice无法支持splice操作。
tun驱动模块源码(linux/drivers/net/tun.c)
static const struct file_operations tun_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read_iter = tun_chr_read_iter,
.write_iter = tun_chr_write_iter,
.poll = tun_chr_poll,
.unlocked_ioctl = tun_chr_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = tun_chr_compat_ioctl,
#endif
.open = tun_chr_open,
.release = tun_chr_close,
.fasync = tun_chr_fasync,
#ifdef CONFIG_PROC_FS
.show_fdinfo = tun_chr_show_fdinfo,
#endif
};
2. 可行方案
由于TUN模块开发的时候linux还没有splice技术,所以直到如今,都没有开发者来支持该功能,为了优化TUN模块的性能,我们可以选择为TUN模块实现此功能,实现之后的应用方式可以有两种
-
直接修改tun.c然后,提供一个支持splice技术的tun模块
-
保持tun模块现有能力,基于tun.c实现一个新的私有模块并且支持splice,可以于现有tun共存于内核中
以上两种方式稳定之后均可以向linux内核提交patch。
支持splice操作之后,保守估计TUN隧道网络的性能至少能达到直连网络的90%。
六、补充测试数据
===================== Test UDP-Direct bandwidth ===================== Connecting to host 172.16.203.128, port 5201 [ 5] local 172.16.203.129 port 36058 connected to 172.16.203.128 port 5201 [ ID] Interval Transfer Bitrate Total Datagrams [ 5] 0.00-1.00 sec 53.3 MBytes 53.3 MBytes/sec 38602 [ 5] 1.00-2.00 sec 54.6 MBytes 54.6 MBytes/sec 39569 [ 5] 2.00-3.00 sec 54.9 MBytes 54.9 MBytes/sec 39761 [ 5] 3.00-4.00 sec 54.8 MBytes 54.8 MBytes/sec 39712 [ 5] 4.00-5.00 sec 54.8 MBytes 54.8 MBytes/sec 39712 [ 5] 5.00-6.00 sec 55.0 MBytes 55.0 MBytes/sec 39825 [ 5] 6.00-7.00 sec 55.5 MBytes 55.5 MBytes/sec 40203 [ 5] 7.00-8.00 sec 55.4 MBytes 55.4 MBytes/sec 40134 [ 5] 8.00-9.00 sec 55.5 MBytes 55.5 MBytes/sec 40196 [ 5] 9.00-10.00 sec 55.7 MBytes 55.6 MBytes/sec 40300 - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Jitter Lost/Total Datagrams [ 5] 0.00-10.00 sec 550 MBytes 55.0 MBytes/sec 0.000 ms 0/398014 (0%) sender [ 5] 0.00-9.98 sec 513 MBytes 51.4 MBytes/sec 0.014 ms 26226/398014 (6.6%) receiver iperf Done. ===================== Test UDP-over-TUN bandwidth ===================== Connecting to host 10.0.0.11, port 5201 [ 5] local 10.0.0.22 port 43678 connected to 10.0.0.11 port 5201 [ ID] Interval Transfer Bitrate Total Datagrams [ 5] 0.00-1.00 sec 78.1 MBytes 78.1 MBytes/sec 56524 [ 5] 1.00-2.00 sec 78.5 MBytes 78.5 MBytes/sec 56867 [ 5] 2.00-3.00 sec 78.8 MBytes 78.8 MBytes/sec 57060 [ 5] 3.00-4.00 sec 78.9 MBytes 78.9 MBytes/sec 57137 [ 5] 4.00-5.00 sec 78.7 MBytes 78.7 MBytes/sec 56962 [ 5] 5.00-6.00 sec 79.1 MBytes 79.1 MBytes/sec 57305 [ 5] 6.00-7.00 sec 79.0 MBytes 79.0 MBytes/sec 57197 [ 5] 7.00-8.00 sec 78.5 MBytes 78.5 MBytes/sec 56823 [ 5] 8.00-9.00 sec 78.7 MBytes 78.7 MBytes/sec 57017 [ 5] 9.00-10.00 sec 78.5 MBytes 78.5 MBytes/sec 56816 - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Jitter Lost/Total Datagrams [ 5] 0.00-10.00 sec 787 MBytes 78.7 MBytes/sec 0.000 ms 0/569708 (0%) sender [ 5] 0.00-10.19 sec 228 MBytes 22.4 MBytes/sec 0.180 ms 404614/569704 (71%) receiver iperf Done. ===================== Test TCP-Direct bandwidth ===================== Connecting to host 172.16.203.128, port 5201 [ 5] local 172.16.203.129 port 40554 connected to 172.16.203.128 port 5201 [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-1.00 sec 48.3 MBytes 48.3 MBytes/sec 0 2.31 MBytes [ 5] 1.00-2.00 sec 29.6 MBytes 29.6 MBytes/sec 0 2.70 MBytes [ 5] 2.00-3.00 sec 31.0 MBytes 31.0 MBytes/sec 0 2.70 MBytes [ 5] 3.00-4.00 sec 31.4 MBytes 31.4 MBytes/sec 0 2.70 MBytes [ 5] 4.00-5.00 sec 31.0 MBytes 31.0 MBytes/sec 0 2.70 MBytes [ 5] 5.00-6.00 sec 31.1 MBytes 31.1 MBytes/sec 0 2.70 MBytes [ 5] 6.00-7.00 sec 31.8 MBytes 31.8 MBytes/sec 0 2.70 MBytes [ 5] 7.00-8.00 sec 31.0 MBytes 31.0 MBytes/sec 0 2.70 MBytes [ 5] 8.00-9.00 sec 31.8 MBytes 31.7 MBytes/sec 0 2.70 MBytes [ 5] 9.00-10.00 sec 31.5 MBytes 31.5 MBytes/sec 0 2.70 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-10.00 sec 328 MBytes 32.8 MBytes/sec 0 sender [ 5] 0.00-10.04 sec 328 MBytes 32.7 MBytes/sec receiver iperf Done. ===================== Test TCP-over-TUN bandwidth ===================== Connecting to host 10.0.0.11, port 5201 [ 5] local 10.0.0.22 port 32876 connected to 10.0.0.11 port 5201 [ ID] Interval Transfer Bitrate Retr Cwnd [ 5] 0.00-1.00 sec 19.6 MBytes 19.6 MBytes/sec 152 127 KBytes [ 5] 1.00-2.00 sec 20.4 MBytes 20.4 MBytes/sec 0 218 KBytes [ 5] 2.00-3.00 sec 18.8 MBytes 18.8 MBytes/sec 68 147 KBytes [ 5] 3.00-4.00 sec 20.1 MBytes 20.1 MBytes/sec 0 229 KBytes [ 5] 4.00-5.00 sec 20.4 MBytes 20.4 MBytes/sec 1 233 KBytes [ 5] 5.00-6.00 sec 19.8 MBytes 19.8 MBytes/sec 70 158 KBytes [ 5] 6.00-7.00 sec 19.6 MBytes 19.6 MBytes/sec 47 140 KBytes [ 5] 7.00-8.00 sec 20.0 MBytes 20.0 MBytes/sec 23 148 KBytes [ 5] 8.00-9.00 sec 18.6 MBytes 18.6 MBytes/sec 8 150 KBytes [ 5] 9.00-10.00 sec 19.6 MBytes 19.6 MBytes/sec 19 189 KBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate Retr [ 5] 0.00-10.00 sec 197 MBytes 19.7 MBytes/sec 388 sender [ 5] 0.00-9.99 sec 195 MBytes 19.5 MBytes/sec receiver iperf Done. |
七、应用层读写操作API与TUN FD的兼容性
支持 | ||
read/write | ✅ | |
readv/writev | ✅ | |
recv/send | ❌ | ENOTSOCK(88)Socket operation on non-socket |
recvmsg/sendmsg | ❌ | ENOTSOCK(88)Socket operation on non-socket |
recvmmsg/sendmmsg | ❌ | ENOTSOCK(88)Socket operation on non-socket |