天天看點

BPF和Go:Linux 中的現代内省形式

作者 | Marko Kevac

譯者 | 劉雅夢

策劃 | 辛曉亮

本文将向你介紹為什麼我們需要像 BPF 這樣的東西,并幫助你了解何時及如何使用它,以及它是如何幫助作為工程師的你改進你正在進行的項目的。我們還将研究它與 Go 相關的一些詳細資訊。

BPF和Go:Linux 中的現代内省形式

每個人都有自己最喜歡的魔術。對一個人來說他是托爾金(Tolkien),對另一個人來說是普拉切特(Pratchett),對于第三個人來說,比如我,是馬克斯·弗雷(Max Frei)。今天我要給大家講的是我最喜歡的 IT 魔術:BPF 以及圍繞它的現代基礎設施。

BPF 目前正處于流行的高峰期。這項技術正在飛速發展,深入到了意想不到的領域,并且越來越容易被普通使用者所接受。現在幾乎每個流行的會議都有關于這個主題的演講,早在 8 月份,我就應邀在 GopherCon Russia 上做了該主題相關的演講。

我在那裡有過非常好的體驗,是以我想與盡可能多的人分享一下。本文将向你介紹為什麼我們需要像 BPF 這樣的東西,并幫助你了解何時及如何使用它,以及它是如何幫助作為工程師的你改進你正在進行的項目的。我們還将研究它與 Go 相關的一些詳細資訊。

我真正希望的是,你讀完這篇文章後,就像第一次讀完《哈利波特》的小孩兒那樣,眼睛裡閃着光芒,并且希望你能夠親自去嘗試一下這個新“玩具”。

一些背景知識

好吧,讓一個 34 歲、留着大胡子、眼神灼熱的家夥來告訴你這個魔術是什麼?

我們生活在 2020 年。打開推特,你可以看到憤怒的技術人員發來的推文,他們都說今天編寫的軟體品質太差了,需要扔掉,我們需要重新開始。有些人甚至威脅說要徹底離開這個行業,因為他們無法忍受這些,一切都是如此的糟糕、不友善且緩慢。

BPF和Go:Linux 中的現代内省形式

他們可能是對的:如果不閱讀上千條評論,就無法确定原因。但有一點我絕對同意,那就是現代軟體堆棧比以往任何時候都要複雜:我們有 BIOS、EFI、作業系統、驅動程式、子產品、庫、網絡互動、資料庫、緩存、編排器(如 K8s)、Docker 容器,最後還有我們自己帶有運作時和垃圾收集器的軟體。

一個真正的專業人士可能會花上幾天的時間才能回答這樣一個問題:當在你的浏覽器中輸入 google.com 後會發生什麼。

要了解你的系統發生了什麼是非常複雜的,尤其是在目前情況下,出現了問題,你正在賠錢的時候。正是由于這個問題,才出現了能夠幫助你了解系統内部情況的企業。在大公司裡,有的整個部門都是像夏洛克·福爾摩斯(Sherlock holmes)那樣的偵探,他們知道在哪裡敲敲錘子,知道用什麼擰緊螺栓以節省數百萬美元。

我喜歡問人們如何在最短的時間内調試出突發問題。通常,人們最先想到的方法是分析日志。但問題在于,唯一可通路的日志是開發人員放在他們的系統中的日志,這是很不靈活的。

第二種最流行的方法是研究度量名額。最流行的三個度量名額處理系統都是用 Go 編寫的。度量名額非常有用,但是,雖然它們确實可以讓你看到症狀,但它們并不總是能夠幫助你定義出問題的根本原因。

第三種方法是所謂的“可觀察性”:你可以對系統的行為提出盡可能多的複雜問題,并獲得這些問題的答案。由于問題可能會非常複雜,是以答案可能會需要最廣泛的資訊,而在問題被提出之前,我們并不知道這些資訊是什麼,這意味着可觀察性絕對需要靈活性。

提供一個“動态”更改日志級别的機會怎麼樣呢?如果使用調試器,在程式運作時連接配接到程式,并在不中斷程式運作時執行某些操作,又會怎麼樣呢?了解哪些查詢會被發送到系統中,可視化慢查詢的來源,通過 pprof 檢視記憶體消耗情況,并擷取其随時間變化的曲線圖呢?如何測量一個函數的延遲以及延遲對參數的依賴性呢?我将所有這些方法都歸為“可觀測性”這一總稱。這是一套實用程式、方法、知識和經驗,它們結合在一起,共同為我們提供了機會,即使不能做到我們想做的任何事,至少在系統工作時,它可以在系統中做很多“現場”工作。它相當于現代 IT 界的一把瑞士軍刀。

BPF和Go:Linux 中的現代内省形式

但是我們怎樣才能做到這一點呢?市場上已經有很多現成的工具了:有簡單的、有複雜的、有危險的、也有緩慢的。但是今天這篇文章是關于 BPF 的。

Linux 核心是一個事件驅動系統。實際上,在核心以及整個系統中所發生的一切都可以看作是一組事件。中斷是一個事件;通過網絡接收資料包是一個事件;将處理器的控制權轉移到另一個程序是一個事件;運作函數也是一個事件。

是的,是以 BPF 是 Linux 核心的一個子系統,它使你有機會編寫一些由核心運作以響應事件的小程式。這些程式既可以幫忙你了解系統正在發生什麼,也可以用來控制系統。

現在讓我們來深入了解一下詳細細節。

什麼是 eBPF?

BPF 的第一個版本于 1994 年問世。有些人在為 tcpdump 實用程式編寫用于檢視或“嗅探”網絡資料包的簡單規則時,可能會遇到過它。你可以為 tcpdump 設定過濾器,這樣你就不必檢視所有的内容,隻需檢視你感興趣的資料包即可。例如,“隻檢視 TCP 協定和 80 端口”。對于每個傳遞的資料包,都會運作一個函數來确定其是否需要儲存有問題的特定資料包。可能會有很多資料包,是以我們的函數必須要很快。實際上,我們的 tcpdump 過濾器被轉換為 BPF 函數。下面是一個例子。

BPF和Go:Linux 中的現代内省形式

一個簡單的以 BPF 程式形式呈現的 tcpdump 過濾器

最初的 BPF 代表了一個非常簡單帶有多個寄存器的虛拟機。但是,盡管如此,BPF 還是大大加快了網絡資料包的過濾速度。在當時,這是一個很重要的進步。

BPF和Go:Linux 中的現代内省形式

在 2014 年,Alexei Starovoitov,一個非常著名的核心黑客,擴充了 BPF 的功能。他增加了寄存器的數量和程式允許的大小,添加了 JIT 編譯,并建立了一個用于檢查程式是否安全的檢查器。然而,最令人印象深刻的是,新的 BPF 程式不僅能夠在處理資料包時運作,而且還能夠響應其他核心事件,并能在核心和使用者空間之間來回傳遞資訊。

這些變化為使用 BPF 的新方法提供了機會。一些過去需要通過編寫複雜而危險的核心子產品來實作的功能,現在可以相對簡單地通過 BPF 來實作。為什麼能這麼好呢?是因為在編寫子產品時,任何錯誤通常都會導緻當機(panic),不是“溫和”Go 風格的當機,而是核心當機,一旦發生,我們唯一能做的就是重新開機。

普通的 Linux 使用者突然擁有了一項新的超能力:能夠檢視“引擎蓋下”的情況——這是以前隻有核心核心開發人員才能使用的東西,或者根本不會提供給任何人。這個選項可以與為 iOS 或 Android 編寫程式的能力相媲美:在舊手機上,這要麼是不可能的,要麼就是要複雜得多。

Alexei Starovoitov 新版本的 BPF 被稱為 eBPF(e 代表擴充,extended)。但是現在它已經取代了所有舊的 BPF 用法,并且已經變得非常流行了,為了簡單起見,它仍然被稱為 BPF。

BPF 可用于何處?

好了,你可以将 BPF 程式附加到哪些事件或觸發器上呢,人們又是如何開始使用它們以擷取新的能力的呢?

目前,主要有兩大組觸發器。

第一組用于處理網絡資料包和管理網絡流量。它們是 XDP、流量控制事件及其他幾個事件。

以下情況需要用到這些事件:

建立簡單但非常有效的防火牆。Cloudflare 和 Facebook 等公司使用 BPF 程式來過濾掉大量的寄生流量,并打擊最大規模的 DDoS 攻擊。由于處理發生在資料包生命的最早階段,直接在核心中進行(BPF 程式的處理有時甚至可以直接推送到網卡中進行),是以可以通過這種方式處理巨量的流量。這些事情過去都是在專門的網絡硬體上完成的。

建立更智能、更有針對性、但性能更好的防火牆——這些防火牆可以檢查通過的流量是否符合公司的規則、是否存在漏洞模式等。例如,Facebook 在内部進行這種審計,而一些項目則對外銷售這類産品。

建立智能負載均衡器。最突出的例子就是 Cilium 項目,它最常被用作 K8s 叢集中的網格網絡。Cilium 對流量進行管理、均衡、重定向和分析。所有這些都是在核心運作的小型 BPF 程式的幫助下完成的,以響應這個或那個與網絡資料包或套接字相關的事件。

這是第一組與網絡問題相關并能夠影響網絡通信行為的觸發器。第二組則與更普遍的可觀察性相關;在大多數情況下,這組的程式無法影響任何事件,而隻能“觀察”。這才是我更感興趣的。

這組的觸發器有如下幾個:

perf 事件(perf events)——與性能和 perf Linux 分析器相關的事件:硬體處理器計數器、中斷處理、小 / 大記憶體異常攔截等等。例如,我們可以設定一個處理程式,每當核心需要從 swap 讀取記憶體頁時,該處理程式就會運作。例如,想象有這樣一個實用程式,它顯示了目前所有使用 swap 的程式。

跟蹤點(tracepoints)——核心源代碼中的靜态(由開發人員定義)位置,通過附加到這些位置,你可以從中提取靜态資訊(開發人員先前準備的資訊)。在這種情況下,靜态似乎是一件壞事,因為我說過,日志的缺點之一就是它們隻包含了程式員最初放在那裡的内容。從某種意義上說,這是正确的,但跟蹤點有三個重要的優勢:

有相當多的跟蹤點散落在核心中最有趣的地方

當它們不“開啟”時,它們不使用任何資源

它們是 API 的一部分,它們是穩定的,不會改變。這非常重要,因為我們将提到的其他觸發器缺少穩定的 API。

例如,假設有一個關于顯示的實用程式,核心出于某種原因沒有給它時間執行。你坐着納悶為什麼它這麼慢,而 pprof 卻沒有顯示任何什麼有趣的東西。

USDT——與跟蹤點相同,但是它适用于使用者空間的程式。也就是說,作為程式員,你可以将這些位置添加到你的程式中。并且許多大型且知名的程式和程式設計語言都已經采用了這些跟蹤方法:例如 MySQL、或者 PHP 和 Python 語言。通常,它們的預設設定為“關閉”,如果要打開它們,需要使用 enable-dtrace 參數或類似的參數來重新建構解釋器。是的,我們還可以在 Go 中注冊這種類跟蹤。你可能已經識别出參數名稱中的單詞 DTrace。關鍵在于,這些類型的靜态跟蹤是由 Solaris) 作業系統中誕生的同名系統所推廣的。例如,想象一下,何時建立新線程、何時啟動 GC 或與特定語言或系統相關的其他内容,我們都能夠知道是怎樣的一種場景。

這是另一種魔法開始的地方:

Ftrace 觸發器為我們提供了在核心的任何函數開始時運作 BPF 程式的選項。這是完全動态的。這意味着核心将在你選擇的任何核心函數或者在所有核心函數開始執行之前,開始執行之前調用你的 BPF 函數。你可以連接配接到所有核心函數,并在輸出時擷取所有調用的有吸引力的可視化效果。

kprobes/uprobes 提供的功能與 ftrace 幾乎相同,但在核心和使用者空間中執行函數時,你可以選擇将其附加到任何位置上。如果在函數的中間,變量上有一個“if”,并且能為這個變量建立一個值的直方圖,那就不是問題。

kretprobes/uretprobes——這裡的一切都類似于前面的觸發器,但是它們可以在核心函數或使用者空間中的函數傳回時觸發。這類觸發器便于檢視函數的傳回内容以及測量執行所需的時間。例如,你可以找出“fork”系統調用傳回的 PID。

我再重複一遍,所有這些最奇妙之處在于,當我們的 BPF 程式為了響應這些觸發器而被調用之後,我們可以很好地“環顧四周”:讀取函數的參數,記錄時間,讀取變量,讀取全局變量,進行堆棧跟蹤,儲存一些内容以備後用,将資料發送到使用者空間進行處理,和 / 或從使用者空間擷取資料或一些其他控制指令以進行過濾。簡直不可思議!

我不知道你是怎麼想的,但對我來說,這個新的基礎設施就像是一個我很早之間就想要得到的玩具一樣。

API:怎麼使用它

好了,讓我們開看一下 BPF 程式由什麼組成的,以及如何與它互動。

BPF和Go:Linux 中的現代内省形式

首先,我們有一個 BPF 程式,如果它通過驗證,就會被加載到核心中。在那裡,它将被 JIT 編譯器編譯成機器碼,并在核心模式下運作,這時附加的觸發器将會被激活。

BPF 程式可以選擇與第二部分互動,即與使用者空間程式互動。有兩種方式可以做到這一點。我們可以向循環緩沖區寫入,而使用者空間程式可以從中讀取。我們也可以對鍵值映射(也就是所謂 BPF 映射)進行寫入和讀取,相應地,使用者空間程式也可以做同樣的事情,并且相應地,它們就可以互相傳遞資訊了。

基本用法

使用 BPF 最簡單的方法是用 C 語言編寫 BPF 程式,然後用 Clang 編譯器将相關的代碼編譯成虛拟機的代碼(在任何情況下都不應該采用這種從頭開始的方式)。然後,我們直接使用 BPF 系統調用加載該代碼,并同樣采用 BPF 系統調用的方式與我們的 BPF 程式進行互動。

第一種可用的簡化方法是使用 libbpf 庫。它是随核心源代碼一起提供的,允許我們直接使用 BPF 系統調用。基本上,它為加載代碼和使用 BPF 映射将資料從核心發送到使用者空間并傳回提供了友善的包裝器。

bcc

顯然,這對人們來說一點也不友善。幸運的是,在 iovizor 這個品牌下,BCC 項目出現了,這使我們的生活變得更加輕松了。

BPF和Go:Linux 中的現代内省形式

基本上,它為我們準備了整個建構環境,并允許我們編寫單個的 BPF 程式,其中С語言部分會被自動建構并加載到核心中,而使用者空間部分則可以用 Python 來實作,這既簡單又清晰明了。

bpftrace

然而,BCC 似乎在很多方面都很複雜。出于某些原因,人們特别不喜歡用С語言來寫核心的這部分。

同樣那些來自 iovizor 的人也提供了一個工具,bpftrace,它允許我們用類似于 AWK 這樣的簡單腳本語言(甚至是單行代碼)來編寫 BPF 腳本。

BPF和Go:Linux 中的現代内省形式

Brendan Gregg 是生産力和可觀察性領域的知名專家,他對 BPF 的可用工作方式進行了可視化,如下圖所示:

BPF和Go:Linux 中的現代内省形式

縱軸顯示的是給定工具的易用性,而橫軸顯示則是其功能。我們可以看到,BCC 是一個非常強大的工具,但它并不是一個超級簡單的工具。而 bpftrace 要簡單得多,但同時,它的功能則稍弱一些。

使用 BPF 的示例

現在,讓我們來看一些具體的例子,看看這些我們可以利用的神奇的力量。

BCC 和 bpftrace 都包含了一個“工具”目錄,其中包含了大量的有趣且有用的現成腳本。它們也可以用作本地的 Stack Overflow,你可以從中複制代碼塊以用于自己的腳本。

例如,下面是一個顯示 DNS 查詢延遲的腳本:

這是一個實時顯示 DNS 查詢完成時間的實用程式,是以,你可以捕獲一些意外的異常值(舉個例子)。

如下則是一個“監視”别人在終端上輸入的内容的腳本:

這種腳本可以用來捕獲“壞鄰居”,或者對公司的伺服器執行安全審計。

用于檢視進階語言調用流的腳本如下所示:

Brendan Gregg 同樣制作了一張圖檔,它彙集了所有相關的腳本,箭頭指向每個實用程式允許你觀察的子系統。正如你所看到的那樣,我們已經擁有了大量的随時可用的實用程式以供我們使用,實際上它們幾乎已經可以應對任何可能發生的情況了。

BPF和Go:Linux 中的現代内省形式

不要試圖閱讀圖檔上的任何内容。該 圖檔 僅供參考

那 Go 呢?

現在我們來談談 Go。我們有兩個基本問題:

你能用 Go 編寫 BPF 程式嗎?

我們能分析用 Go 編寫的程式嗎?

我們按順序來逐漸看下。

目前,唯一能夠編譯成 BPF 機器可以了解的格式的編譯器是 Clang。另一種流行的編譯器 GСС仍然沒有 BPF 後端。而能夠編譯成 BPF 的程式設計語言,隻有 C 語言的一個非常受限的版本。

然而,BPF 程式還有一個在使用者空間中的第二部分。這部分可以用 Go 來編寫。

正如我前面提到的那樣,BCC 允許你用 Python 編寫這一部分,而 Python 是該工具的主要語言。同時,在主庫中,BCC 還支援 Lua 和 C++,并且在輔庫中,它還支援 Go。

BPF和Go:Linux 中的現代内省形式

這個程式看起來和 Python 中的程式完全一樣。一開始,它有一個字元串,其中的 BPF 程式是 C 語言編寫的,然後我們通信将給定的程式附加到那裡,并以某種方式與它進行互動,例如,從 BPF 映射中提取資料。

基本上就是這樣。可以在 Github 上檢視這個例子的更多詳細資訊。

主要的缺點可能是,我們使用的是 C 庫、libbcc 或 libbpf,而用 C 庫建構 Go 程式遠非“在公園裡散步”那麼容易。

除了 iovisor/gobpf 之外,我還發現了其他三個最新的項目,它們允許你在 Go 中編寫使用者空間(userland)部分。

https://github.com/dropbox/goebpf

https://github.com/cilium/ebpf

https://github.com/andrewkroh/go-ebpf

Dropbox 的版本不需要任何 C 庫,但你需要自己使用 Clang 建構 BPF 的核心部分,然後使用 Go 程式将其加載到核心中。

Cilium 版本與 Dropbox 版本具有相同的細節。但值得一提的是,它是由來自 Cilium 項目的人制作的,這意味着它隻能成功。

出于完整性的考慮,我列出了第三個項目。就像前面兩個項目一樣,它沒有外部的 C 語言依賴項,需要用 C 語言手動建構 BPF 程式,但是,它的前途看起來并不是特别好。

事實上,我們還應該問一個問題:為什麼要用 Go 來編寫 BPF 程式?因為如果你看 BCC 或 bpftrace,那麼 BPF 程式隻占用不到 500 行代碼。但是,僅僅用 bpftrace 語言編寫一個小腳本,或者使用一點 Python,不是更簡單嗎?我有兩個理由不支援這樣做。

第一個原因是:你真的很喜歡 Go,并且更願意用 Go 來做所有的事情。此外,将 Go 程式從一台機器遷移到另一台機器可能會更簡單:靜态連結、簡單的二進制檔案等等。但事情遠沒有這麼簡單,因為我們被綁定到一個特定的核心上。我就講到這裡吧,否則,我的文章又要多 50 頁了。

第二個原因是:你編寫的不是一個簡單的腳本,而是一個大型的系統,它也在内部使用了 BPF。我甚至 在 Go 中看到過一個關于這樣的系統的例子:

BPF和Go:Linux 中的現代内省形式

Scope 項目看起來像是一個二進制檔案,當它在 K8s 或其他的雲基礎設施上運作時,它會分析周圍發生的一切,并顯示有哪些容器和服務,以及它們是如何互動的等等。這些很多都是用 BPF 來實作的。這是一個有趣的項目。

Go 程式分析

如果你還記得,我們還有一個問題:我們可以用 BPF 來分析用 Go 編寫的程式嗎?我們的第一反應是,“可以,我們可以!”程式用什麼語言編寫又有什麼差別呢?畢竟,它隻是編譯後的代碼,就像其他程式一樣,在處理器中計算一些東西,瘋狂地占用記憶體,并通過核心與硬體互動,通過系統調用與核心互動。原則上這是正确的,但也有一些具體問題——這些問題有不同程度的複雜性。

傳遞參數

其中一個問題是,Go 不使用大多數其他語言所使用的 ABI(Application binary interface,應用二進制接口)。結果是,這位“開創者”決定将 ABI 從它們所熟悉的 Plan 9 系統中移除。

與 API 一樣,ABI 也是一種接口約定——隻是在位、位元組和機器碼級别。

我們對 ABI 感興趣的點主要在于它的參數是如何傳遞給函數的,以及響應是如何從函數中傳回的。如果在标準的 ABI x86-64 中,處理器的寄存器是用于傳遞參數和響應的,而在 Plan 9 ABI 中,堆棧則是用于實作該目标。

Rob Pike 和他的團隊并沒有打算制定另一個标準;他們已經為 Plan 9 系統提供了一個幾乎随時可用的 C 編譯器,就像計算 2x2 一樣簡單。在很短的時間内,他們将其改造成了 Go 編譯器。這就是一個工程師的方法。

然而,事實上,這并不是一個關鍵問題。一方面,我們可能很快就會看到參數在 Go 中 通過寄存器來傳遞,其次,從 BPF 中擷取堆棧參數并不複雜:sargX 别名 已經添加到了 bpftrace 中,另一個别名 很可能會在不久的将來出現在 BCC 中。

更新:自從我做了演講之後,Go 官方的 詳細提案 甚至已經出台,并建議在 ABI 中使用寄存器。

唯一線程辨別符

第二個具體問題與 Go 的一個深受喜愛的特性相關,即 goroutines。度量函數延遲的方法之一是儲存函數被調用的時間,擷取函數的退出時間,并計算其內插補點。我們需要儲存開始時間以及一個包含函數名和 TID(線程 ID)的鍵。線程 ID 是必需的,因為同一個函數可以被不同的程式或者被同一個程式的不同線程同時調用。

BPF和Go:Linux 中的現代内省形式

但是,在 Go 中,goroutine 在系統線程之間移動:前一分鐘,goroutine 在一個線程上執行,下一分鐘,則在另一個線程上執行。而且,在 Go 的情況下,最好不要将 TID 放入鍵中,而是将 GID(即 goroutine 的 ID)放入其中,這對我們來說是有好處的,但不幸的是,我們無法獲得它。從純技術的角度來看,這個 ID 确實存在。你甚至可以使用肮髒的黑客攻擊來提取它,因為可以在堆棧中的某個位置找到它,但這樣做是被關鍵開發人員小組建議嚴格禁止的。他們認為這是我們永遠不需要的資訊。對于 goroutine 的本地存儲也是如此,但這是偏離主題的。

擴充棧

第三個問題是最嚴重的。它是如此嚴重,以至于即使我們以某種方式解決了第二個問題,也無法幫助我們度量 Go 函數的延遲。

大多數讀者可能已經對什麼是棧有了很好的了解。這是同一個棧中,與堆不同,你可以為變量配置設定記憶體,而不必考慮釋放。

但是對于 C 語言來說,在這種情況下,棧的大小是固定的。如果超出了這個固定的大小,就會出現衆所周知的 堆棧溢出 現象。

在 Go 中,棧是動态的。在舊版本中,它是通過記憶體塊的連結清單實作的。現在,它是一個大小動态變化的連續塊。這意味着,如果配置設定的記憶體塊對我們來說不夠,我們就擴充目前的記憶體塊。如果我們不能擴充它,我們就配置設定一個更大的,然後把所有的資料從舊的位置移動到新的位置。這一點非常吸引人,并且涉及了安全保證、cgo 和垃圾收集等問題,但這是另一篇文章的主題。

重要的是要知道,為了讓 Go 能夠移動棧,它必須能夠調用棧,并周遊棧中的所有指針。

而這就是基本問題的所在之處:uretprobes,用于将 BPF 探測附加到函數的傳回重,動态地改變棧,以內建對其處理程式的調用,也就是所謂的“蹦床”(trampoline)。而且,在大多數情況下,這會改變棧,這是 Go 不期望的事情,它會導緻程式的崩潰。糟糕!

BPF和Go:Linux 中的現代内省形式

順便說一句,這并不是 Go 所獨有的。當處理異常時,C++ 棧拆分器也會每隔一段時間就崩潰一次。

這個問題沒有解決的辦法。像往常一樣,在這種情況下,雙方都會互相指責對方,并各自都能提出完全有根據的論點。

但是,如果你真的需要設定一個 uretprobe,有一種方法可以繞過這個問題。怎麼用呢?不要設定 uretprobe。你可以在退出函數的所有位置設定一個 uprobe。可能隻有一個這樣的位置,也可能有五十個這樣的位置。

BPF和Go:Linux 中的現代内省形式

這就是 Go 的獨特之處。

通常情況下,這種伎倆是行不通的。一個足夠聰明的編譯器知道如何執行所謂的 尾部調用優化,當我們隻是簡單地跳到下一個函數的開始處,而不是從函數傳回并沿着調用棧傳回時。這種優化對于諸如 Haskell 這樣的函數式語言來說至關重要。如果沒有它,在不發生堆棧溢出的情況下,你就會寸步難行。然而,通過這種優化,我們根本不可能找到從函數傳回的所有位置。

具體來說,Go 1.14 版本的編譯器還不能執行尾部調用優化。這意味着,附加到函數的所有顯式退出的技巧都是有效的,即使它非常笨重。

示例

不要認為 BPF 對 Go 沒用。遠非如此。我們可以做所有其他不涉及上述問題的事情。而且我們也會這樣做的。

讓我們來看一些例子。

首先,讓我們來看一個簡單的程式。基本上,它是一個監聽 8080 端口的 Web 伺服器,并有一個 HTTP 查詢的處理程式。處理程式從 URL 中擷取名稱參數和年份參數,執行檢查,然後将所有這三個變量(名稱、年份和檢查狀态)發送到 prepareAnswer() 函數,然後該函數準備一個字元串形式的答案。

BPF和Go:Linux 中的現代内省形式

站點檢查(Site check)是一個 HTTP 查詢,通過通道和 goroutines 檢查會議站點是否正常工作。prepare 函數隻是簡單地将所有這些參數轉換為一個可讀的字元串。

我們将通過 curl 進行簡單的查詢來觸發我們程式:

BPF和Go:Linux 中的現代内省形式

對于我們的第一個示例,我們将使用 bpftrace 列印所有程式的函數調用。在這種情況下,我們将對“main”下的所有函數進行附加。在 Go 中,所有函數都有一個符号,其形式如下:包名. 函數名。我們的包是“main”,函數的運作時是“runtime”。

BPF和Go:Linux 中的現代内省形式

當我使用 curl 時,會執行處理程式、站點檢查函數和 goroutine 子函數,然後執行準備答案函數。太好了!

接下來,我不僅要導出那些正在執行的函數,還要導出它們的參數。讓我們以函數 prepareAnswer() 為例。它有三個參數。讓我們試着輸出兩個整數。

讓我們以 bpftrace 為例,隻不過這次不是執行單行代碼,而是執行一個腳本,我們将其附加到我們的函數上,并使用别名作為堆棧的參數,就像我所說的那樣。

在輸出中,我們可以看到,我們發送了 2020,擷取的狀态是 200,此外,還發送了一次 2021。

BPF和Go:Linux 中的現代内省形式

但是這個函數有三個參數。其中第一個參數是字元串。那它怎麼處理呢?

讓我們簡單地導出從 0 到 3 的所有堆棧參數。我們看到了什麼?一個很大的數字,一個稍小點的數字,還有我們原來的數字 2021 年和 200。開頭這些奇怪的數字是什麼呢?

BPF和Go:Linux 中的現代内省形式

這時,熟悉 Go 的内部結構是很有幫助的。如果在 C 語言中,字元串隻是一個以 0 結尾的位元組數組,那麼在 Go 中,字元串實際上是一個結構體,它由指向位元組數組的指針(順便說一句,這它不是以 0 結尾的)和長度組成。

BPF和Go:Linux 中的現代内省形式

但是 Go 編譯器,當它以參數的形式發送一個字元串時,會展開這個結構體,并将它作為兩個參數發送。是以,第一個奇怪的數字确實是一個指向我們數組的指針,第二個是長度。

果然:預期的字元串長度是 22。

相應地,我們稍微修正了一下我們的腳本,以便通過堆棧指針寄存器擷取問題中的兩個值及其正确的偏移量,并且在內建的 str() 函數的幫助下,我們将其導出為字元串。一切順利:

BPF和Go:Linux 中的現代内省形式

我們也來看看運作時。例如,我想知道我們的程式啟動了哪些 goroutines。我們知道 goroutines 是由函數 newproc() 和 newproc1() 啟動的。讓我們附着一下它們。指向函數結構的指針是 newproc1() 函數的第一個參數。它隻有一個字段,即指向函數的指針:

BPF和Go:Linux 中的現代内省形式

在本例中,我們将使用直接在腳本中定義結構的功能。這比玩偏移要簡單一些。我們已經導出了處理程式被調用時啟動的所有 goroutines。之後,如果我們想要擷取偏移量的符号名稱,那麼我們就可以在其中看到我們的 checkSite 函數了。歡呼!

BPF和Go:Linux 中的現代内省形式

就 BPF、BCC 和 bpftrace 的功能而言,這些示例隻是滄海一粟。隻要對内部工作原理有了足夠的了解和經驗,你就可以從一個正在運作的程式中獲得幾乎所有的資訊,而無需停止或更改它。

結 論

這就是我想告訴你的全部内容,我希望它對你有所啟發。

BPF 是 Linux 中最流行也是最有前途的領域之一。而且我相信,在未來的幾年裡,我們将會看到更多有趣的東西——不僅是技術本身,還有工具以及它的傳播。

現在開始還不算太晚,并不是每個人都知道 BPF,是以趕快開始學習吧,成為魔術師,解決問題,幫助你的同僚。他們都說魔術隻有一次效果。

當談到 Go 時,像往常一樣,我們最終會變得非常獨特。我們總是有一些怪癖,無論是不同的編譯器,還是 ABI,需要 GOPATH,有一個你無法 Google 的名字。但我認為,可以說我們已經成為一股不可忽視的力量,在我看來,事情隻會變得越來越好。

https://medium.com/bumble-tech/bpf-and-go-modern-forms-of-introspection-in-linux-6b9802682223#e0e4

繼續閱讀