Surftrace是由系统运维SIG推出的一个ftrace封装器和开发编译平台linux 调用内核函数,让用户既能基于libbpf快速建立工程进行开发,也能作为ftrace的封装器进行trace命令编撰。
项目包含Surftrace工具集和pylcc、glcc(pythonorgenericClanguageforlibbpfCompilerCollection),提供远程和本地eBPF的编译能力。通过对krobe和ftrace相关功能最大化具象linux设置默认网关,同时对各类场景下的追踪能力提高(例如网路合同抓包),促使用户特别快速的上手,对定位问题效率提高10倍以上。
另外,现现在火到天际的技术——eBPF,Surftrace支持通过libbpf及CO-RE能力,对bpf的map和prog等常用函数进行了封装和具象,基于此平台开发的libbpf程序可以无差异运行在各个主流内核版本上,开发、部署和运行效率提高了一个数目级。
Surftrace最大的优势在于将当前主流的trace技术一并提供给广大开发者,可以通过ftrace也可以使用eBPF,应用场景覆盖显存、IO等Linux各个子系统,非常是在网路合同栈跟踪里面,对skb内部数据结构,网路字节序处理做到行云流水。
一、理解Linux内核合同栈
定位网路问题是一个软件开发者必备一项基础技能,例如ping连通性、tcpdump抓包剖析等手段,可以对网路问题进行初步定界。但是,当问题深入内核合同栈内部,怎样将网路报文与内核合同栈清晰关联上去,精准追踪到关注的报文行进路径呢?
1.1网路报文分层结构
引用自《TCP/IP解读》卷一。
如上图所示,网路报文对数据报文数据在不同层进行封装。不同OS均采用一致的报文封装形式,达到跨软件平台通信的目的。
1.2sk_buff结构体
sk_buff是网路报文在Linux内核中的实际承载者,它在include/linux/skbuff.h文件中定义,结构体成员较多,本文不逐一展开。
用户须要重点关注下边两个结构体成员:
unsignedchar *head, *data;
复制代码
其中head指向了缓冲区开始,data指向了当前报文处理所在合同层的起始位置,如当前合同处理坐落tcp层,data表针都会指向structtcphdr。在IP层,则指向了structiphdr。为此,data表针成员,是报文在内核处理过程中的关键信标。
1.3内核网路合同栈地图
右图是合同栈处理地图,可以保存后放大观看。
(图源网路)
不难发觉,上图中几乎所有函数都涉及到skb结构体处理,因而要想深入了解网路报文在内核的处理过程,skb->data应当就是最理想的引路蜂。
二、Surftrace对网路报文提高处理
Surftrace基于ftrace封装,采用接近于C语言的参数句型风格,将原先繁杂的配置流程优化到一行命令句子完成LINUX虚机,极大简化了ftrace布署步骤,是一款十分便捷的内核追踪工具。并且要追踪网路报文,光解析一个skb->data表针是远远不够的。存在以下障碍:
针对上述困难,Surftrace对skb传参做了相应的特殊处理,以达到便捷易用的疗效。
2.1网路合同层标记处理
以追踪网合同栈报文接收的入口__netif_receive_skb_core函数为例,函数原型定义:
staticint__netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc, struct packet_type **ppt_prev);
复制代码
解析每位skb对应报文三层合同成员的方式:
surftrace 'p __netif_receive_skb_core proto=@(struct iphdr *)l3%0->protocol`
复制代码
合同成员获取方式为@(structiphdr*)l3%0->protocol。
tips:
2.2扩展下一层报文内容获取方法
surftrace为ethhdr、iphdr、icmphdr、udphdr、tcphdr结构体添加了xdata成员,用于获取下一层报文内容。xdata有以下5类类型:
数据
数据类型
数据宽度(字节)
cdata
unsginedchar[]
1
sdata
unsignedshort[]
2
ldata
unsignedint[]
4
qdata
unsignedlonglong[]
8
Sdata
char*[]
字符串
链表下标是根据主频进行对齐的,例如要提取icmp报文中的2~3字节内容,组成一个unsignedshort的数据,可以通过以下方式获取:
data=@(struct icmphdr*)l3%0->sdata[1]
复制代码
2.3IP和字节序模式转换
网路报文字节序采取的是大端模式,而我们的操作系统通常采用小端模式。同时,ipv4采用了一个unsignedint数据类型来表示一个IP,而我们一般习惯采用1.2.3.4的形式来表示一个ipv4地址。上述差别造成直接去剖析网路报文内容的时侯十分吃力。surftrace通过往变量降低前缀的方法,在数据呈现以及过滤的时侯,将原始数据按照前缀命名规则进行转换,提高可读性和便利性。
前缀名
数据输出方式
数据宽度(字节)
ip_
a.b.c.d
ip字符串
b16_
10补码
2
b32_
10补码
4
b64_
10补码
8
B16_
16补码
2
B32_
16补码
4
B64_
16补码
8
2.4牛刀小试
我们在一个实例上抓到一个非预期的udp报文,它会往目标ip10.0.1.221端标语9988发送数据,如今想要确定这个报文的发送进程。因为udp是一种面向无联接的通信合同,难以直接通过netstat等方法锁定发送者。
用Surftrace可以在ip_output函数处中下钩子:
intip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
复制代码
追踪表达式:
surftrace 'p ip_output proto=@(struct iphdr*)l3%2->protocol ip_dst=@(struct iphdr*)l3%2->daddr b16_dest=@(struct udphdr*)l3%2->dest comm=$comm body=@(struct udphdr*)l3%2->Sdata[0] f:proto==17&&ip_dst==10.0.1.221&&b16_dest==9988'
复制代码
追踪结果:
surftrace 'p ip_output proto=@(struct iphdr*)l3%2->protocol ip_dst=@(struct iphdr*)l3%2->daddr b16_dest=@(struct udphdr*)l3%2->dest comm=$comm body=@(struct udphdr*)l3%2->Sdata[0] f:proto==17&&ip_dst==10.0.1.221&&b16_dest==9988' echo 'p:f0 ip_output proto=+0x9(+0xe8(%dx)):u8 ip_dst=+0x10(+0xe8(%dx)):u32 b16_dest=+0x16(+0xe8(%dx)):u16 comm=$comm body=+0x1c(+0xe8(%dx)):string' >> /sys/kernel/debug/tracing/kprobe_events echo 'proto==17&&ip_dst==0xdd01000a&&b16_dest==1063' > /sys/kernel/debug/tracing/instances/surftrace/events/kprobes/f0/filter echo 1 > /sys/kernel/debug/tracing/instances/surftrace/events/kprobes/f0/enable echo 0 > /sys/kernel/debug/tracing/instances/surftrace/options/stacktrace echo 1 > /sys/kernel/debug/tracing/instances/surftrace/tracing_on -2733784 [014] .... 12648619.219880: f0: (ip_output+0x0/0xd0) proto=17 ip_dst=10.0.1.221 b16_dest=9988 comm="nc" body="Hello World! @"
复制代码
通过上述命令,可以确定报文的发送的pid为2733784,进程名为nc。
三、实战:定位网路问题
接出来我们从一个实际网路网路问题出发,述说怎么采用Surftrace定位网路问题。
3.1问题背景
我们有两个实例通信存在性能问题,经抓包排查,确认性能上不去的根因是存在丢包造成的。辛运的是,该问题可以通过ping对端复现,确认丢包率在10%左右。
通过进一步抓包剖析,可以明晰报文遗失在实例B内部。
通过检测/proc/net/snmp以及剖析内核日志,没有发觉可疑的地方。
3.2surftrace跟踪
在1.1节的地图中,我们可以查到网路报文是内核由dev_queue_xmit函数将报文推送到网卡驱动。因而,可以在这个出口先进行probe,过滤ping报文,加上-s选项,打出调用栈:
surftrace 'p dev_queue_xmit proto=@(struct iphdr *)l2%0->protocol ip_dst=@(struct iphdr *)l2%0->daddr f:proto==1&&ip_dst==192.168.1.3' -s
复制代码
可以获取到以下调用栈:
因为问题复现机率比较高,我们可以将怀疑的重点方向先置于包发送流程中,即从icmp_echo函数往上,用Surftrace在每一个符号都加一个trace点,追踪下回包究竟消失在那里。
3.3锁定丢包点
问题追踪到了这儿,对于经验丰富的朋友应当是可以猜出丢包缘由。我们不妨纯粹从代码角度出发,再找一下确切的丢包位置。结合代码剖析,我们可以在函数内部找到以下两处drop点:
通过Surftrace函数内部追踪功能,结合汇编代码信息,可以明晰丢包点是出在了qdisc->enqueue钩子函数中。
rc = q->enqueue(skb, q, &to_free) & NET_XMIT_MASK;
复制代码
此时,可以结合汇编信息:
找到钩子函数存入的寄存名为bx,之后通过surftrace复印下来。
surftrace 'p dev_queue_xmit+678 pfun=%bx'
复制代码
之后将pfun值在/proc/kallsyms查找匹配。
至此可以明晰是htbqdisc造成丢包。确认相关配置存在问题后,将相关配置回退linux 调用内核函数,网路性能得以恢复。
四、总结
Surftrace在网路层面的提高,致使用户只须要有相关的网路基础和一定的内核知识储备,就可以用较低编码工作量达到精准追踪网路报文在Linux内核的完整处理过程。适宜用于追踪Linux内核合同栈代码、定位深层次网路问题。
参考文献:
【1】《TCP/IP解读》
【2】《Linux内核设计与实现》
【3】《深入理解Linux网路技术黑幕》
【4】surftracereadmde:
【5】
了解更多软件开发与相关领域知识,点击访问InfoQ官网:,获取更多精彩内容!