龍蜥開源核心追蹤利器 Surftrace:協議包解析效率提升 10 倍! | 龍蜥技術

OpenAnolis小助手發表於2022-05-12


文/系統運維 SIG

Surftrace  是由系統運維 SIG 推出的一個 ftrace 封裝器和開發編譯平臺,讓使用者既能基於 libbpf 快速構建工程進行開發,也能作為  ftrace 的封裝器進行 trace 命令編寫。專案包含 Surftrace 工具集和 pylcc、glcc(python or  generic C language for libbpf Compiler Collection),提供遠端和本地 eBPF 的編譯能力。
通過對 krobe 和 ftrace 相關功能最大化抽象,同時對各種場景下的追蹤能力增強(比如網路協議抓包),使得使用者非常快速的上手, 對定位問題效率提升 10 倍以上 。另外,現如今火到天際的技術—— eBPF,Surftrace 支援通過 libbpf 及 CO-RE 能力,對 bpf 的 map 和 prog 等常用函式進行了封裝和抽象, 基於此平臺開發的 libbpf 程式可以無差別執行在各個主流核心版本上,開發、部署和執行效率提升了一個數量級。
Surftrace  最大的優勢在於將當前主流的 trace 技術一併提供給廣大開發者,可以通過 ftrace 也可以使用 eBPF,應用場景覆蓋記憶體、IO 等  Linux 各個子系統,特別是在網路協議棧跟蹤上面,對 skb  內部資料結構,網路位元組序處理做到行雲流水,把複雜留給自己,簡單留給你。今天就讓我們來見識一下 Surftrace 在網路領域的強勁表現吧。

一、理解 Linux 核心協議棧

定位網路問題是一個軟體開發者必備一項基礎技能,諸如 ping 連通性、tcpdump 抓包分析等手段,可以對網路問題進行初步定界。然而,當問題深入核心協議棧內部,如何將網路報文與核心協議棧清晰關聯起來,精準追蹤到關注的報文行進路徑呢?

1.1 網路報文分層結構

引用自《TCP/IP 詳解》卷一。
如上圖所示,網路報文對資料包文資料在不同層進行封裝。不同 OS 均採用一致的報文封裝方式,達到跨軟體平臺通訊的目的。

1.2 sk_buff 結構體

sk_buff 是網路報文在 Linux 核心中的實際承載者,它在 include/linux/skbuff.h 檔案中定義,結構體成員較多,本文不逐一展開。

使用者需要重點關注下面兩個結構體成員:

unsignedchar *head, *data;
其中  head 指向了緩衝區開始,data 指向了當前報文處理所在協議層的起始位置,如當前協議處理位於 tcp 層,data 指標就會指向  struct tcphdr。在 IP 層,則指向了struct iphdr。因此,data 指標成員,是報文在核心處理過程中的關鍵信標。

1.3 核心網路協議棧地圖

下圖是協議棧處理地圖,可以儲存後放大觀看(圖源網路)。

不難發現,上圖中幾乎所有函式都涉及到 skb 結構體處理,因此要想深入瞭解網路報文在核心的處理過程,skb->data 應該就是最理想的引路蜂。

二、Surftrace 對網路報文增強處理

Surftrace  基於 ftrace 封裝,採用接近於 C 語言的引數語法風格,將原來繁瑣的配置流程優化到一行命令語句完成,極大簡化了 ftrace  部署步驟,是一款非常方便的核心追蹤工具。但是要追蹤網路報文,光解析一個 skb->data 指標是遠遠不夠的。存在以下障礙:

  • skb->data 指標在不同網路層指向的協議頭並不固定;
  • 除了獲取當前結構內容,還有獲取上一層報文內容的需求,比如一個我們在 udphdr結構體中,是無法直接獲取到 udp 報文內容;
  • 源資料呈現不夠人性化。如 ipv4 報文 IP 是以一個 u32 資料型別,可讀性不佳,過濾器配置困難。

針對上述困難,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`
協議成員獲取方法為 @(struct iphdr *)l3%0->protocol
tips:

  • 可以跨協議層向上解析報文結構體,如在 l3 層去分析 struct icmphdr 中的資料成員
  • 不可以跨協議層向下解析報文結構體,如在 l4 層去分析 struct iphdr 中的成員

2.2 擴充下一層報文內容獲取方式

surftrace 為 ethhdr、iphdr、icmphdr、udphdr、tcphdr 結構體新增了 xdata 成員,用於獲取下一層報文內容。xdata 有以下 5 類型別:

資料
資料型別
資料長度(位元組)
cdata
unsgined char []
1
sdata
unsigned short []
2
ldata
unsigned int []
4
qdata
unsigned long long []
8
Sdata
char* []
字串

陣列下標是按照位寬進行對齊的,比如要提取 icmp 報文中的 2~3 位元組內容,組成一個 unsigned short 的資料,可以通過以下方法獲取:

data=@(struct icmphdr*)l3%0->sdata[1]

2.3 IP 和位元組序模式轉換

網路報文位元組序採取的是大端模式,而我們的作業系統一般採用小端模式。同時,ipv4  採用了一個 unsigned int 資料型別來表示一個 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 報文,它會往目標 ip 10.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.2 surftrace 跟蹤

在 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 查詢匹配。
至此可以明確是 htb qdisc 導致丟包。確認相關配置存在問題後,將相關配置回退,網路效能得以恢復。

四、總結

Surftrace 在網路層面的增強,使得使用者只需要有相關的網路基礎和一定的核心知識儲備,就可以用較低編碼工作量達到精準追蹤網路報文在 Linux 核心的完整處理過程。適合用於追蹤 Linux 核心協議棧程式碼、定位深層次網路問題。


參考文獻、相關連結及加入系統運維SIG方式、或程式碼不清楚均處可移步龍蜥公眾號(OpenAnolis龍蜥)2022年5月11日相同推送檢視。

—— 完 ——


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70004278/viewspace-2893682/,如需轉載,請註明出處,否則將追究法律責任。

相關文章