使用ftrace追踪TUN设备内核事件

TUN调试 / 3人浏览

凌晨三点,交易所的 K 线还在跳动。BTC 刚插针到 67200,又弹回 68150,手机上的爆仓提示像雪片一样刷屏。你盯着监控面板,发现节点上的 TUN 设备吞吐突然掉了一半,P2P 连接数从 8000 跌到 3200,但 CPU 和带宽都没跑满。运维群里有人喊:“是不是内核把包丢了?”你登上跳板机,准备用 tcpdump 抓包,却发现 TUN 设备是 tun0,流量已经被 WireGuard 加密,抓出来全是 UDP。你需要的不是用户态抓包,而是直接看内核里 TUN 驱动发生了什么。这时候,ftrace 就是那把能切开内核黑盒的手术刀。

为什么是 TUN,为什么是 ftrace

虚拟币交易系统里,TUN 设备几乎无处不在。跨机房专线用 WireGuard 走 TUN,链下撮合引擎和节点之间用 TUN 做点对点隧道,甚至某些 MEV 机器人为了隐藏流量特征,也会把 RPC 请求塞进自定义的 TUN 隧道。TUN 的特点是:内核里它是个网络接口,但用户态有个进程通过 /dev/net/tun 读写它。包从协议栈进入 TUN,会被送到用户态;用户态写回 TUN,包又进入协议栈。这个“半内核半用户态”的结构,一旦出现丢包、延迟、卡顿,排查起来非常难受。

传统工具在这里有盲区。tcpdump 抓的是网络接口上的包,但 TUN 的包在进入用户态之前,可能已经被内核的 qdisc、netfilter 或者 TUN 驱动本身处理过一轮。strace 能看用户态进程的 read/write,但看不到内核协议栈为什么没把包送上来。perf 能采样,但很难精确回答“第 1432 号包在 TUN 的 ndo_start_xmit 里待了多久”。

ftrace 不一样。它是内核自带的跟踪框架,不需要额外编译模块,不需要重启机器,挂上 kprobe 或者 tracepoint 就能看函数调用、参数、时间戳。对于 TUN 设备,内核里有现成的 tracepoint,也有稳定的函数符号。你可以把它想象成给内核装了一个高速摄像机,专门拍 TUN 收发包的那几微秒。

场景一:凌晨的丢包疑云

你首先想知道,包到底有没有进 TUN。内核里 TUN 的接收路径大致是:网卡收到包,协议栈路由到 tun0,调用 tun_net_xmit,然后唤醒用户态进程。发送路径是:用户态 write 到 /dev/net/tun,内核 tun_chr_write_iter 处理,再调用 netif_rx_ni 把包注入协议栈。

先看接收。ftrace 可以挂 kprobe 到 tun_net_xmit:

bash cd /sys/kernel/debug/tracing echo 'p:tun_xmit tun_net_xmit skb=%di' > kprobe_events echo 1 > events/kprobes/tun_xmit/enable echo 1 > tracing_on

等几秒,看 trace:

bash cat trace | head -50

如果 tun_net_xmit 根本没被调用,说明包在到达 TUN 之前就被丢了。这时候要去看路由表、iptables 或者 qdisc。如果调用了,但用户态进程没收到,那问题在唤醒机制或者用户态读得太慢。

你还可以用 tracepoint 看更底层的 netif_receive_skb,但那个太吵,全系统包都进来了。更好的办法是过滤设备名。ftrace 支持 filter:

bash echo 'dev == "tun0"' > events/kprobes/tun_xmit/filter

这样只留下 tun0 的事件。你发现 tun_net_xmit 每秒被调用 8000 次,但用户态进程的 read 次数只有 3200 次。差距在哪?继续挂 kprobe 到 tun_chr_read_iter,看用户态到底读了多少。

bash echo 'p:tun_read tun_chr_read_iter file=%di buf=%si count=%dx' >> kprobe_events

结果发现 tun_chr_read_iter 每秒只有 3200 次。说明内核确实把包送到了 TUN 的接收队列,但用户态进程没有及时读。为什么?可能是进程被调度走了,或者 poll 没被唤醒。再挂 kprobe 到 tun_chr_poll,看 poll 的调用频率和返回值。最后定位到:用户态进程的 CPU 被一个高优先级的 MEV 机器人抢占了,导致 TUN 读线程饿死。把 TUN 读线程绑到隔离核,吞吐立刻恢复。

场景二:发送路径上的延迟黑洞

接收问题解决后,你发现发送方向也有蹊跷。用户态写回 TUN 的包,有时候延迟高达 200ms。tun_chr_write_iter 到 netif_rx_ni 之间,到底发生了什么?

挂 kprobe 到 tun_chr_write_iter 和 netif_rx_ni,并且用 ftrace 的 function_graph 看调用链:

bash echo 'tun_chr_write_iter' > set_graph_function echo function_graph > current_tracer echo 1 > tracing_on

function_graph 会输出每个函数的耗时。你看到 tun_chr_write_iter 里调用了 tun_get_user,然后 tun_put_user,再 netif_rx_ni。其中 tun_get_user 花了 180ms。为什么?继续展开,发现它调用了 copy_from_iter,而 copy_from_iter 又触发了缺页异常。原来用户态传进来的 buffer 是 mmap 的,但内存被 swap 出去了。虚拟币交易系统里,内存紧张是常态,尤其是行情剧烈波动时,节点进程的内存被挤到 swap。TUN 发送路径上的 copy_from_iter 一碰 swap,延迟就爆炸。

解决方案很简单:把 TUN 用户态进程的 buffer 锁在内存里,用 mlock 或者 madvise(MADV_DONTNEED) 的反向操作。但如果没有 ftrace,你根本不知道延迟发生在 copy_from_iter 里。tcpdump 只会告诉你包发出去了,但不知道内核里卡了 180ms。

场景三:用 tracepoint 看 TUN 的队列深度

除了 kprobe,内核还给 TUN 准备了 tracepoint。在 /sys/kernel/debug/tracing/events/tun/ 下,通常有 tun_net_xmit、tun_net_receive 之类的点。这些 tracepoint 是内核开发者埋好的,格式稳定,参数清晰。

比如:

bash echo 1 > events/tun/tun_net_xmit/enable cat trace_pipe

输出里会带 skbaddr、len、dev 等信息。你可以在 trace_pipe 里实时看 TUN 的吞吐。如果发现 len 都是 0,说明用户态写入了空包,可能是程序 bug。如果 len 突然从 1500 变成 64,说明发生了分片或者 MTU 问题。

更妙的是,你可以把 tracepoint 和 perf 结合,统计 TUN 的包大小分布:

bash perf record -e tun:tun_net_xmit -a sleep 10 perf script | awk '{print $NF}' | sort -n | uniq -c

这样能在几秒内看出 TUN 上跑的是什么流量。虚拟币交易系统里,如果发现大量 64 字节的小包,那可能是心跳或者 P2P 握手;如果全是 1500 字节,那可能是区块同步。根据分布调整 TUN 的 txqueuelen 和 qdisc,能显著降低延迟。

场景四:ftrace 与 eBPF 的配合

ftrace 很强,但它有个缺点:不能做复杂的聚合和过滤。比如你想统计“每个源 IP 的 TUN 丢包率”,ftrace 的 filter 只能做简单比较。这时候可以上 eBPF,但 eBPF 的编写和加载比 ftrace 重。一个实用的组合是:用 ftrace 快速定位问题函数,然后用 eBPF 在那个函数上挂 kprobe,做精细统计。

比如,先用 ftrace 发现 tun_net_xmit 被调用但 tun_chr_read_iter 没跟上,然后写一个 eBPF 程序,在 tun_net_xmit 里记录 skb->len 和 skb->hash,在 tun_chr_read_iter 里记录读取的字节数,最后用 map 聚合。这样既能快速定位,又能长期监控。

在虚拟币场景里,这种组合特别适合做“TUN 隧道健康度”监控。你可以在每个节点上跑一个轻量级 eBPF 程序,把 TUN 的收发速率、丢包率、延迟直方图推到 Prometheus,一旦 BTC 价格剧烈波动,就能提前发现 TUN 隧道拥塞。

场景五:当 TUN 遇上 XDP

有些高性能交易系统会把 TUN 和 XDP 结合,在网卡驱动层就把包重定向到 TUN。这时候 ftrace 依然有用,但要看的是 xdp_do_redirect 和 tun_net_xmit 之间的调用关系。挂 kprobe 到 xdp_do_redirect,过滤 dev == "tun0",可以看到 XDP 程序把包扔给 TUN 的频率。如果 XDP 程序里做了 XDP_DROP,那 tun_net_xmit 根本不会被调用。ftrace 能帮你确认这一点。

另外,XDP 和 TUN 之间有个坑:XDP 重定向到 TUN 时,skb 的 cb 字段可能被覆盖。如果你在 tun_net_xmit 里看到奇怪的 skb->cb 值,可以用 ftrace 打印 skb->cb 的前 8 个字节:

bash echo 'p:tun_cb tun_net_xmit skb=%di cb=+0(%di):x64' >> kprobe_events

这样能直接看到 cb 里的内容,判断是否被 XDP 改过。

实战:一次真实的 TUN 丢包排查

回到凌晨三点的那次故障。你按照上面的步骤,先挂 tun_net_xmit,发现调用次数正常。再挂 tun_chr_read_iter,发现读取次数只有一半。然后挂 tun_chr_poll,发现 poll 返回了 POLLIN,但用户态进程没有及时 read。最后用 function_graph 看用户态进程的 read 系统调用,发现它在 copy_to_iter 里卡了 50ms。原因是用户态 buffer 是 mmap 的,但被 madvise(MADV_DONTNEED) 释放了,导致每次 read 都触发缺页。把 madvise 改成 MADV_WILLNEED,问题消失。

整个过程没用 tcpdump,没用 strace,只用 ftrace 就定位到了内核里的具体函数和行号。这就是 ftrace 在 TUN 排查中的价值:它不给你看包的内容,但给你看包在内核里的路径和时间。

一些容易踩的坑

ftrace 虽然强大,但在生产环境用要注意几点。第一,function_graph 开销很大,不要长时间开着,定位完就关。第二,kprobe 的 filter 里字符串比较要用 ==,不要用 =。第三,TUN 的 tracepoint 在不同内核版本里名字可能不一样,先用 ls events/tun/ 确认。第四,如果 TUN 设备是 tun0,但你的 filter 写的是 dev == "tun0",注意 dev 字段可能是 net_device 指针,不是字符串。这时候要用 dev->name 或者直接用 kprobe 打印 %di 然后手动解析。

另外,虚拟币交易系统里经常有多个 TUN 设备,比如 tun0 走 WireGuard,tun1 走自定义协议。ftrace 的 filter 可以同时匹配多个:

bash echo 'dev == "tun0" || dev == "tun1"' > events/kprobes/tun_xmit/filter

这样能一次看两个设备的收发情况。

把 ftrace 变成日常工具

故障解决后,你把常用的 ftrace 命令写成了脚本,放在每个节点的 /usr/local/bin/tun_trace.sh 里。脚本里预置了 tun_net_xmit、tun_chr_read_iter、tun_chr_write_iter 三个 kprobe,并且用 trace_pipe 实时输出。每次行情剧烈波动,你就跑一下脚本,看 TUN 的收发是否平衡。如果发现 tun_net_xmit 比 tun_chr_read_iter 多出 20%,就知道用户态进程又饿了,赶紧调整 CPU 亲和性。

ftrace 不是万能的,它不能替代 tcpdump 看包内容,也不能替代 perf 做全系统性能分析。但在 TUN 设备的内核事件追踪上,它是最轻量、最直接、最不需要额外依赖的工具。尤其是在虚拟币这种对延迟极度敏感的场景里,能少一次用户态和内核态的切换,就能少一次爆仓的风险。

凌晨四点,BTC 站上 68500,你的 TUN 吞吐恢复到 8000 pps,延迟稳定在 0.3ms。你关掉 ftrace,把脚本存进知识库,然后给自己倒了杯咖啡。下一次插针,你知道该看哪里了。

版权声明:

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

链接: https://harmonyosvpn.com/tun-debug/ftrace-tun-kernel-events.htm

来源: harmonyosvpn.com

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

最新文章

归档

标签