手艺人舍bpftrace而取systemtap的代价和思考
上個禮拜我就想噴eBPF了,由于周末時間實在太緊,就準備拖延一周,但還是立了個flag,先發了個朋友圈:
ebpf就像牛皮蘚一樣,已經遍布在linux內核的各個角落,每個調用點都看上去很隨意,毫無規劃,讓人覺得好像自己覺得哪里需要這么一個調用點并不很難…
但實際上如果你真的去嘗試在某處加一個ebpf調用點時,就會覺得這件事和清除牛皮蘚的過程非常類似,修改散落在各個目錄的多個文件,還得重新編譯,大概率失敗,還要重新做一次,很難一次做干凈,當你好不容易成功了,會有一種“不過如此”的嗟嘆…
我曾將ebpf比做擴散的癌細胞,這個比喻沒有給人密集恐懼的效果,所以我換成了牛皮癬。該存在ebpf調用點的地方一個也沒有,沒必要ebpf的地方到處都是,這些點還在持續增加,迄至5.11內核,ebpf已經有大三十個點了,依然在毫無規劃地瘋長著…
eBPF是個創新,但人們明顯狂熱過度了,ebpf增加調用點也過于隨意,太業務導向了,損壞了內核的內聚性,遠遠比不上當初netfilter的五個hook點以及qdisc這種經過良好設計的機制,另外還有一個問題,netfilter的五個hook點上如果部署了ebpf點,其實就能解決大部分性能問題,然而直到現在都沒有,感覺是社區矯枉過正了,真的徹底把netfilter當成了舊時代的象征,把馬殺掉的同時,輪子也不要了
人們都像蟲子一樣在這里你爭我搶,吃的都是良心,拉的全是思想。
終于到了周末,我終于還是不能說話,我甚至已經不知道該說些什么了。
昨天,我把自己血祭了,這種自我犧牲在古羅馬共和國是一種美德,執政官會把自己獻給神,以換取戰爭的勝利。
為了噴eBPF,在平時的工作和學習中,我積累了很多素材,eBPF的領地分為兩個部分:
- 網絡協議棧功能
- trace跟蹤
在網絡方面,我用牛皮癬的比喻來說明eBPF不斷瘋長的畫面是多么糟糕。而在trace跟蹤方面,我想拿性能和功能說事。
我本希望用這些素材來佐證自己的一些怪異的觀點,昨天,我準備用我的實際工作來作為我最后一個素材,結果它恰恰驗證了我的認知錯誤,它恰恰說明了eBPF作為trace跟蹤工具是多么的好用!
我的故事是這樣的。
在大流量的背景下,特別是如果你的代碼使用類似Bonding,tun/tap,GRE,IPIP等虛擬網卡,排查skb在哪里被drop一直都是一個很麻煩的事情。即便是你已經知道了一個特定的五元組,這件事也不會因此變得簡單。
抓包?抓包永遠是第一步的操作,但也只是第一步,它只能告訴你skb收到了或者沒有收到,如果沒有收到,進一步就需要確認skb到底在哪里丟了。當然了,如果最后實在是定位不了,一般會把鍋甩給運營商這個黑洞。
?
類似wireguard這種,其所有復雜的操作,包括加密,分發等邏輯在內均在wireguard虛擬網卡的xmit函數中完成,跟蹤wg_xmit的細節除了要求你對wireguard的代碼非常熟悉之外,還需要各種奇技淫巧的手藝。
?
以下面這個場景為例:
我要實錘skb在從1到11的哪一步被drop的,要怎么做?
?
這不正是stap的地盤嗎?哦,我不能說stap,在有了bpftrace可供選擇的時候,再說stap有點不正確,如果我堅持要用stap,會有一群人建議我用bpftrace,并說stap是多么的糟糕,過時,不友好。
那好,那就用bpftrace,下面的腳本可以對skb進行完美的全程trace:
#!/usr/local/bin/bpftrace ? #include <linux/skbuff.h> #include <linux/udp.h> #include <net/sock.h> ? k:encrypt_packet {$skb = (struct sk_buff *)arg0;// 這個skb的mark需要iptables來為特定的五元組標記上,但是encrypt_packet這里是可以使用mark的最后的地方。// 在encrypt_packet成功返回后,skb的幾乎所有附屬標記都會被reset,包括skb->mark。// 因此這里必須用另一個標記,以確保在encrypt_packet之后還能用此特征跟蹤到特定的skb。// 由于bpftrace只能讀不能寫,這里我選擇直接用skb的地址!if ($skb->mark == 1234) {printf("encrypt got %p\n", $skb);@addr = $skb;} } ? //k:send4 k:udp_tunnel_xmit_skb {$daddr = arg4;$saddr = arg3;$skb = (struct sk_buff *)arg2;// 這里除了match地址之外,是不是也要match一下其它字段呢?畢竟slub中的skb是可以重用的。// 如果mark 1234的skb在這個之前被drop & free了,它被重新alloc后依然會到這里,這就錯了!// 然而由于流量可控,且我是一個函數一個函數trace,上述概率極低。手藝人不求完美!if ($skb == @addr) {printf("---- skb:%p daddr:%08x saddr:%08x \n", @addr, $daddr, $saddr);} } ? k:iptunnel_xmit //k:dev_queue_xmit //k:dev_hard_start_xmit //k:dev_queue_xmit_nit {$skb = (struct sk_buff *)arg2;// 從裸包中取外層協議頭的內容。$udph = (struct udphdr *)($skb->head + $skb->transport_header);$sport = $udph->source;$dport = $udph->dest;if ($skb == @addr) {$port = (($sport & 0xff00) >>8) | (($sport & 0xff) << 8);$port2 = (($dport & 0xff00) >>8) | (($dport & 0xff) << 8);printf("sport:%d dport:%d\n", $port, $port2);// trace結束,重置全局變量。@addr = (struct sk_buff *)0;} }啊哈,我覺得這是一個讓人感覺很順暢的腳本,skb在進入wg_xmit前打上mark,在wg_xmit的過程中清除skb的mark之前將其地址保存,此后跟蹤該地址的skb。然而悲哀的是,skb順利發送出去了,我一無所獲,然而悲哀的是,內層的報文在對端wireguard的wg網卡上沒有抓到。
?
去對端反著來一遍且OK?思路是一回事,落地是另一回事。
?
怎么才能在對端繼續trace這個skb呢?
如果沒有辦法trace這個skb,你怎么區分這個報文是被中間網絡設備drop了還是被對端wireguard接收過程drop了呢?由于發送端已經可以獲取內層和外層的特定五元組,在接收端用外層五元組去match外層協議頭當然是一個正確的思路,問題是如果外層隧道的五元組被大流量復用,你又將如何在skb解密前去匹配內層五元組,流量實在太大了,就像很多抓包由于流量大無法進行一樣,你想要的信息幾乎會被瞬間淹沒!
?
我想知道的是,bpftrace怎么來做這件事。如果不能方便快捷地解決這個問題,我就有充分的理由使用舊時代的舊事物了。
?
問題是bpftrace不允許我修改skb啊!現在,我決定扔掉bpftrace,用stap來做正確的事。
?
我需要做的僅僅是,為特定的數據包打上一個標記,該標記必須在對端可以被識別。我決定使用無傷大雅的IP頭TTL字段,使用stap完成這件事非常簡單。順便地,我將為skb打mark這件事也用stap來做,于是我也刪掉了iptables規則:
如此一個腳本,在對端直接match外層五元組和TTL值就行了,我們只需要在匹配外層五元組的同時,匹配TTL值大于70的skb即可。bpftrace只能讀不能寫,為了讓這件事成為可能,我只能用stap。
?
以上就是我的態度,我不是不接受bpftrace,我更不是不接受新事物,我只是想說不能在接受新事物的時候把舊事物一棍子打死!不能因為eBPF的流行就選擇bpftrace而把stap丟進垃圾桶。
?
…
?
我正準備吐槽,然而我正準備噴bpftrace做不到某某事情的時候,我血祭了!
?
我明明可以在wireguard的數據接收端像數據發送端一樣用bpftrace來trace這個特定的skb,畢竟我只是想知道它在哪里被drop了,這完全沒有寫操作的必要。然而我為了一種假裝成格調的態度,死活非要用stap來完成這件事而放棄bpftrace。
?
雖然我自以為自己是stap的熟練工,但我卻幾乎都是在用-g的guru模式,不是因為我自信,而是因為我搞不清楚又記不住stap的語法。我幾乎只會C和匯編,我幾乎總是記不住任何其它語言的語法,包括Bash在內…
?
我在使用stap來trace內核或者模塊的函數前,我總會看下它的參數解析情況:
很不幸,無法使用任何參數,原因未知。因此當我希望使用它的參數的時候,我只能裸取寄存器了,像下面這樣:
... probe module("wireguard").function("wg_allowedips_lookup_src") {// 由于stap -L無法解析參數,只能用x86_64的調用規則直接取寄存器if (cmpskb(register("rsi"))) {a = 1;} }probe module("wireguard").function("wg_allowedips_lookup_src").return {if (a == 1) {printf("peer returned::%p\n", register("rax"));a = 0;} }? ...既然都用stap了,為何不讓事情簡單一些呢?于是我就開始了自信滿滿的寫操作。改skb內存,手工修改skb的data,以期望能bypass掉很多不必要的流程。
?
…
?
在我持續這么玩了大概幾個小時后,大概就是開著飛機修引擎的感覺,我有些疲勞,大概在某個精確的時間點,crash or soft lockup,完美的完成了血祭!
這是一次線上作業,鍋顯然是我的。
?
這不能說明我的手藝不精湛,在這種硬著陸之前,我畢竟還放飛了幾個小時呢,但這說明一個問題,bpftrace就是比stap好,至少安全。而這恰恰是我要反駁的觀點,卻被我證明了。
在穩定性方面,eBPF的兩個不允許就夠了:
- eBPF不允許你寫任何有潛在風險的代碼。
- eBPF不允許你寫任何復雜的代碼。
stap里你可以隨便一個while(true)而把系統鎖死,bpftrace中卻不行。
…
處理類似的問題時,其實我是帶有偏見的,我不喜歡使用工具特別是不喜歡使用新工具的原因背后更多的是因為我比較懶,我不喜歡面對和駕馭一大坨不相關的東西,比方說我明明知道有個dropwatch卻沒有使用,就是因為它太復雜了,還要去了解那么復雜的命令行,與其這樣,還不如我直接stap probe kfree_skb然后dump_stack呢。
一開始我對stap也是抵觸的,因為它也是足夠復雜,我寧愿裸寫ftrace函數,比方說手工把一個函數的頭5個字節替換成call stub_handler這種。如今即便我對stap已經輕車熟路了,我依然還是堅持只寫guru模式的腳本,我依然還是懶得去學習stap的語法。
使用工具提高效率那是針對熟悉這種工具的人來講的,對于不熟悉該工具的來講,比如我,花在學習這種工具的使用方法上的時間將讓我延遲對真正問題的處理。
同樣,磨刀不誤砍柴工,工欲善其事必先利其器,我并不贊同這是普適的,這種話術是針對頻繁解決同類問題的人來講的,他們需要的是總結出一種范式,錘子能釘釘子,如果你需要頻繁釘釘子,你當然需要買一把錘子,但如果你只需要釘一次釘子,隨手拿起邊上的一塊磚或許比去買一把錘子更方便。
如果你每次玩的都是新花樣,當然不需要工具了。
此外 “在路上” 的價值觀在我針對工具的觀點也起到了推波助瀾的作用。我一向覺得自己是在路上的人,因此我討厭任何累贅,我不回去攜帶,背負,記憶那些隨處可以得到的東西。杭州往上海搬家,既然上海可以買到被子,我何必要把被子寄回來,直接扔掉不是更好嗎?
在石器時代,人們就已經擁有了類別豐富的工具,但對于一個母系時代凈身出戶的男人而言,唯一可以帶走并且他們自愿選擇的工具就是弓箭,彈弓這種類似的遠程攻擊工具,可能就連刀,斧之類的,都屬于累贅,抓住問題的本質本身,這就足夠了。
浙江溫州皮鞋濕,下雨進水不會胖!
總結
以上是生活随笔為你收集整理的手艺人舍bpftrace而取systemtap的代价和思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python图像加密模块_使用Pycry
- 下一篇: java 压缩 乱码_如何解决java压