TUN设备读写性能测试与优化方法

TUN调试 / 7人浏览

凌晨三点,老赵的交易所后台又炸了。

不是被黑,也不是行情剧烈波动导致撮合引擎崩溃,而是那套用来做“链上充值归集”的TUN隧道网络,在USDT突然放量涌入的时候,吞吐直接掉到了正常水平的三分之一。监控大屏上,入金确认延迟从平均2.3秒飙升到17秒,部分用户开始在小群里骂:“交易所是不是跑路了?提币不动,充值也不到账。”

老赵盯着Grafana面板,TUN设备tun0的TX队列像一条被掐住脖子的水管,ifconfig里dropped计数每秒跳几十次。他猛地想起三个月前,自己为了省事,把原本应该用DPDK或者XDP处理的高速数据面,直接塞进了一个基于TUN的轻量级转发程序里。当时测试环境里跑个几百Mbps没问题,可现在是真实牛市,比特币刚突破前高,链上转账像洪水一样涌来。

这不是老赵一个人的故事。任何做虚拟币基础设施的人——交易所、钱包服务商、矿池、跨链桥——迟早都会撞上TUN设备的性能墙。TUN很美好:用户态程序能像读写文件一样收发原始IP包,轻松实现自定义协议、加密隧道、流量整形。但TUN也很残酷:每一次read()和write()都是一次系统调用,数据要在内核和用户态之间来回拷贝,上下文切换的开销在PPS(每秒包数)上万之后变得不可忽视。

今天我们就跟着老赵的排查过程,把TUN设备读写性能测试与优化方法彻底讲清楚。文章会从一次真实的“事故现场”切入,逐步展开测试方法论、瓶颈定位、以及从应用到内核的优化手段。全程紧扣虚拟币场景——高频充值、大额提现、P2P节点通信、MEV机器人套利——这些业务对TUN性能的要求截然不同。

事故现场:当TUN成为充值归集的瓶颈

老赵的架构很简单:用户向交易所的充值地址打USDT(ERC-20),链上监听服务发现入账后,通过一个TUN隧道把交易数据转发到归集系统,归集系统再构造归集交易发到热钱包。TUN隧道跑在两台8核16G的云主机上,中间走WireGuard加密。

问题出在归集系统那端。它的TUN设备tun1配置如下:

ip tuntap add dev tun1 mode tun ip addr add 10.0.0.1/24 dev tun1 ip link set tun1 up

用户态程序用read(tun_fd, buf, 65536)读包,处理后再write(tun_fd, buf, len)写回。程序本身是单线程的,用epoll监听tun_fd。在测试环境,它跑到了1.2 Gbps,CPU占用不到30%。但线上流量峰值时,tun1的TX dropped一路飙升,RX方向也开始丢包。老赵用ethtool -S tun1看不到什么有用信息,因为TUN是虚拟设备,统计有限。他换成ip -s link show tun1,看到:

RX: bytes packets errors dropped overrun mcast 89234112 123456 0 2345 0 0 TX: bytes packets errors dropped carrier collsns 78123456 98765 0 8765 0 0

dropped在TX方向尤其严重。这意味着内核的qdisc(排队规则)在把包发给TUN设备时,因为用户态程序读取不够快,队列满了,直接丢包。对于充值归集,丢包意味着链上交易丢失,用户资金延迟到账——这在牛市里是致命的。

老赵的第一反应是加线程。他把单线程改成多线程,每个线程独立打开/dev/net/tun,用IFF_MULTI_QUEUE标志创建多队列TUN。代码大致如下:

c int fd = open("/dev/net/tun", O_RDWR); struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags = IFF_TUN | IFF_NO_PI | IFF_MULTI_QUEUE; strncpy(ifr.ifr_name, "tun1", IFNAMSIZ); ioctl(fd, TUNSETIFF, &ifr); // 每个线程重复open+ioctl,得到不同的fd,对应不同队列

多队列确实缓解了问题,但CPU使用率飙升到80%,而且延迟依然不稳定。更麻烦的是,归集系统还要做ECDSA签名和RLP编码,这些计算密集操作和TUN读写抢CPU。老赵意识到,光靠多队列不够,必须从测试方法入手,系统性地找到瓶颈。

如何科学测试TUN设备的读写性能

在虚拟币场景下,测试TUN性能不能只看吞吐量(Gbps),更要看PPS、延迟分布、以及丢包率。因为一笔USDT充值交易可能只有200字节,但每秒来一万笔,PPS就是一万,而带宽只有16 Mbps。反过来,一个大额提现批量交易可能几KB,PPS低但带宽高。所以测试要覆盖不同包长。

老赵搭建了如下测试环境:

  • 两台物理机,通过25G网卡直连。
  • 机器A运行iperf3服务端,机器B运行iperf3客户端,但流量强制走TUN隧道。
  • TUN隧道用最简单的点对点模式,不做加密,排除干扰。
  • 在机器B上,用taskset把TUN读写线程绑定到独立CPU核心。
  • 用perf和ftrace采集内核函数耗时。

测试工具方面,除了iperf3,老赵还用了pktgen(内核态发包)和tcpdump统计实际包数。对于延迟,他用sockperf或者自己写UDP echo程序,在TUN两端打时间戳。

第一轮测试:单队列TUN,单线程读写,包长64字节。结果:

  • 吞吐:0.45 Gbps
  • PPS:约730k
  • CPU:一个核心100%
  • 延迟:平均120微秒,P99 800微秒

这个PPS看似不错,但注意,这是纯读写,没有业务逻辑。老赵的程序还要做解析、签名,实际PPS不到200k。而链上充值峰值时,他需要处理至少500k PPS。

第二轮:多队列TUN,4个线程,每个线程独立队列。结果:

  • 吞吐:1.6 Gbps
  • PPS:约2.6M
  • CPU:4个核心各90%
  • 延迟:平均80微秒,P99 500微秒

提升明显,但CPU效率不高。老赵用perf top看到大量时间花在copy_user_enhanced_fast_string和__schedule上。前者是内核到用户态的数据拷贝,后者是上下文切换。

第三轮:改用recvmsg/sendmsg配合MSG_ZEROCOPY?不行,TUN不支持零拷贝。那用io_uring?Linux 5.1+支持,但TUN的io_uring支持有限,老赵试了,发现IORING_OP_READ在TUN上依然会拷贝。

第四轮:测试不同包长。老赵发现,当包长超过1500字节(MTU),TUN会自动分片,性能急剧下降。而虚拟币交易中,有些跨链消息可能超过MTU,比如包含多个签名。所以他后来把TUN的MTU调到9000(如果底层网络支持),减少分片。

测试结论:TUN设备的性能瓶颈主要在三处——系统调用开销、数据拷贝、以及单队列的锁竞争。测试时必须区分PPS和带宽,并且用多队列+多线程才能压出真实上限。

从内核到应用:TUN读写优化的七种武器

老赵根据测试结果,逐步实施优化。以下方法按投入产出比排序。

1. 启用多队列并合理绑定CPU

多队列是TUN性能的第一把钥匙。创建时加IFF_MULTI_QUEUE,然后每个线程open一次/dev/net/tun,用TUNSETIFF绑定到同一个接口。内核会为每个队列分配独立的tun_struct和等待队列,减少锁竞争。

但注意:多队列的队列数不是越多越好。老赵试过8队列,结果因为CPU核心不够,线程间切换反而增加延迟。最佳实践是队列数等于物理核心数,且每个线程绑定到独立核心(taskset -c或sched_setaffinity)。对于虚拟币归集这种延迟敏感场景,还要把TUN线程和签名线程分开,避免互相抢占。

2. 调整TUN设备的txqueuelen和qdisc

默认TUN的txqueuelen是500,对于高PPS场景太小。老赵用ip link set tun1 txqueuelen 10000增大队列。但队列太大又会导致缓冲区膨胀(bufferbloat),增加延迟。所以他同时把qdisc从默认的pfifo_fast改成fq_codel,用主动队列管理控制延迟。

tc qdisc replace dev tun1 root fq_codel limit 10000

对于纯吞吐场景,可以用pfifo并增大队列;对于延迟敏感(比如MEV套利),fq_codel或codel更好。

3. 使用批处理读写(readv/writev + 多包)

每次read只读一个包,系统调用开销太大。老赵改成一次read读多个包?TUN不支持一次读多个包,但可以用readv配合多个缓冲区?也不行,TUN的read每次返回一个包。不过,他可以在用户态攒批:用epoll等待可读,然后循环read直到EAGAIN,把多个包放入一个环形缓冲区,再批量处理。写方向同理,攒够一批再write,但TUN的write每次只能写一个包。所以批处理主要减少epoll和系统调用的次数,而不是单次读写多个包。

更激进的方法是使用AF_PACKET或XDP绕过TUN,但那会失去TUN的便利性。老赵后来在归集系统里用了io_uring的IORING_OP_READ和IORING_OP_WRITE,配合IOSQE_ASYNC,让内核异步处理读写,减少线程阻塞。但io_uring对TUN的支持需要内核5.10+,且性能提升有限,因为拷贝依然存在。

4. 减少数据拷贝:从TUN到零拷贝的尝试

TUN的本质是内核把网络包从协议栈卸下,通过字符设备交给用户态。这个“卸下”动作包含一次拷贝(skb到用户缓冲区)。用户态处理完再write,又拷贝一次。两次拷贝在10Gbps下就是内存带宽的浪费。

有没有办法避免?有,但代价高。一是用vhost-net或vhost-user,把TUN后端放到用户态DPDK或SPDK,但这对虚拟币交易所来说太重。二是用AF_XDP,它允许用户态直接访问网卡环形缓冲区,零拷贝。但AF_XDP需要网卡支持,且要自己实现协议栈。老赵的归集系统最终选择了AF_XDP作为TUN的替代,但只用于高PPS的充值监听,其他管理流量依然走TUN。

如果坚持用TUN,一个折中方案是:用sendfile或splice?不行,TUN不支持。所以实际生产中,如果PPS超过500k,建议直接上AF_XDP或DPDK。

5. 调整内核参数

几个关键参数:

  • net.core.rmem_max和net.core.wmem_max:增大TUN的读写缓冲区。老赵设成16MB。
  • net.core.netdev_max_backlog:增大网卡收包队列,避免在TUN之前丢包。
  • net.core.busy_poll和busy_read:对于延迟敏感场景,启用忙轮询,减少中断开销。但会增加CPU使用。
  • net.ipv4.tcp_rmem和tcp_wmem:如果TUN上跑TCP,调大窗口。

老赵还发现,tun模块有个参数txqueuelen可以在加载时设置,但运行时用ip link改更灵活。

6. 应用层优化:无锁环形缓冲区与内存池

用户态程序本身也要优化。老赵把原来的malloc/free每个包一次,改成内存池预分配。用无锁环形缓冲区(ring buffer)在TUN读线程和处理线程之间传递包,避免锁竞争。对于虚拟币签名,他用secp256k1的批量验证,但签名本身无法批量,所以他把签名操作放到单独的线程池,通过无锁队列通信。

另外,他关闭了TUN的IFF_NO_PI?不,IFF_NO_PI必须开,否则每个包前面多4字节的包信息,增加拷贝。他还关闭了IFF_VNET_HDR,因为不需要virtio头。

7. 监控与动态调优

优化不是一劳永逸。老赵在归集系统里加了Prometheus指标:tun_rx_packets、tun_tx_packets、tun_rx_dropped、tun_tx_dropped、tun_rx_latency。当tx_dropped持续大于0时,自动增加TUN队列数或调整txqueuelen。他还用eBPF挂了一个kprobe在tun_net_xmit上,统计每个队列的排队深度。

在虚拟币市场剧烈波动时,他还会临时把TUN线程的CPU调度策略改成SCHED_FIFO,优先级99,确保充值归集不被其他进程干扰。

虚拟币场景下的特殊考量

不同虚拟币业务对TUN性能的要求差异很大。老赵后来把系统拆成三个TUN隧道:

  • 充值监听隧道:高PPS、低延迟、小包为主。用多队列+AF_XDP,MTU 1500。
  • 提现归集隧道:高带宽、大包为主、允许一定延迟。用单队列TUN+fq_codel,MTU 9000。
  • P2P节点通信隧道:中等PPS、中等带宽、需要加密。用WireGuard over TUN,多队列,CPU绑定。

对于MEV机器人,TUN的延迟直接决定套利成败。老赵认识一个MEV团队,他们用io_uring+busy_poll+SCHED_FIFO,把TUN端到端延迟压到20微秒以内。代价是每个TUN线程独占一个核心,且CPU频率锁定在最高。

另一个坑是:TUN设备在容器或Kubernetes里性能会下降。因为/dev/net/tun的访问可能经过cgroup和namespace,增加开销。老赵后来把归集系统放到hostNetwork模式,或者用privileged容器直接操作TUN。

还有,当TUN上跑TCP时,要注意TCP_NODELAY和TCP_CORK的取舍。虚拟币交易中,很多协议是自定义的二进制协议,跑在UDP上,所以老赵主要优化UDP路径。

老赵的最终方案与效果

经过两周的折腾,老赵的归集系统最终架构如下:

  • 充值监听:AF_XDP + 多队列,8个线程绑定8个物理核心,PPS稳定在3M,延迟P99 200微秒。
  • 归集转发:TUN多队列(4队列)+ io_uring + 内存池,吞吐2.5 Gbps,CPU使用率60%。
  • 签名与构造:独立线程池,通过无锁队列接收包,签名后直接write到TUN。
  • 监控:eBPF统计每个队列的丢包和延迟,自动告警。

线上再次遇到USDT放量涌入时,tun1的tx_dropped为0,入金确认延迟稳定在3秒以内。老赵终于可以睡个安稳觉了。

但故事没有结束。就在他以为搞定的时候,以太坊的Dencun升级带来了Blob交易,每个Blob 128KB,直接冲击TUN的MTU和分片逻辑。老赵又得重新测试、重新优化。在虚拟币世界,性能优化永远是一场没有终点的赛跑。而TUN设备,这个看似简单的字符设备,背后藏着从系统调用到内存拷贝、从多队列到零拷贝的无数细节。理解它,就是理解现代金融基础设施的底层脉搏。

版权声明:

作者: 最新鸿蒙OS VPN免费节点分享

链接: https://harmonyosvpn.com/tun-debug/tun-rw-performance-benchmark.htm

来源: harmonyosvpn.com

文章版权归作者所有,未经允许请勿转载。

最新文章

归档

标签