如何使用 Delve 和 eBPF 更快地調試 Go 程序

前言

此文章將解釋如何使用 Delve[1] 跟蹤 Go 程序,以及 Delve 如何利用 eBPF 在後臺優化效率和速度。Delve 的目標是爲開發人員提供愉快且高效的 Go 調試體驗。因此,本文重點介紹了我們如何優化函數跟蹤子系統,以便您可以更快地檢查程序並找到根本原因分析。Delve 的跟蹤實現有兩個不同的後端,一個是基於 ptrace 的,另一個使用 eBPF。如果您不熟悉任何這些術語,請不要擔心,我會隨着解釋一起說明。

什麼是程序跟蹤?

跟蹤是一種允許開發人員在執行時看到程序正在做什麼的技術。與典型的調試技術相反,這種方法不需要直接用戶交互。最知名的跟蹤工具之一是 strace[2],它允許開發人員在執行期間查看程序的系統調用。

儘管上述的 strace 工具對於瞭解系統調用很有用,但 Delve trace 命令允許您洞察 "用戶空間" 中 Go 程序的情況。這種 Delve 跟蹤技術允許您跟蹤程序中的任意函數,以便查看這些函數的輸入和輸出。

此外,您還可以使用此工具瞭解程序的控制流,而無需交互式調試會話的開銷,因爲它還會顯示正在執行該函數的 Goroutine。對於高度併發的程序,這可能是獲得程序執行洞察力的更快方法,而無需啓動完整的交互式調試會話。

如何使用 Delve 跟蹤 Go 程序

Delve 允許您通過調用 dlv trace 子命令來跟蹤 Go 程序。該子命令接受一個正則表達式,並將執行您的程序,在與正則表達式匹配的每個函數上設置跟蹤點,並實時顯示結果。

以下是示例程序:

package main


import "fmt"

func foo(x, y int) (z int) {
        fmt.Printf("x=%d, y=%d, z=%d\n", x, y, z)
        z = x + y

        return
}

func main() {
        x := 99
        y := x * x
        z := foo(x, y)

        fmt.Printf("z=%d\n", z)
}

程序跟蹤將給出以下輸出:

$ dlv trace foo

> goroutine(1): main.foo(99, 9801)

x=99, y=9801, z=0

>> goroutine(1)=(9900)

z=9900

Process 583475 has exited with status 0

如您所見,我們在正則表達式中提供了 foo,它在這種情況下與主包中同名的函數匹配。以>爲前綴的輸出表示被調用的函數,並顯示調用函數的參數,而以>>爲前綴的輸出表示從函數返回並與其相關聯的返回值。所有輸入和輸出行均以在該時刻執行的 Goroutine 作爲前綴。

默認情況下,dlv trace 命令使用基於 ptrace 的後端,但添加 --ebpf 標誌將啓用基於 eBPF 的實驗性後端。使用上面的示例,如果我們要像以下方式調用 trace 子命令:

$ dlv trace –ebpf foo

我們將收到類似的輸出。但是,背後發生的情況要大大不同並且更加高效。

ptrace 低效率

默認情況下,Delve 會使用 ptrace 系統調用來實現跟蹤功能。ptrace 是一個系統調用,允許程序觀察和操縱同一臺機器上的其他程序。實際上,在 Unix 系統上,Delve 使用這個 ptrace 功能來實現調試器提供的許多低級功能,例如讀寫內存、控制執行等。

雖然 ptrace 是一個有用和強大的機制,但它存在固有的效率低下。首先,ptrace 是一個系統調用,意味着我們必須跨越用戶空間 / 內核空間邊界,這增加了每次使用函數時的開銷。這是由於我們必須調用 ptrace 的次數越多,開銷就越大。考慮前面的示例,以下是使用 ptrace 實現跟蹤的大致步驟概述:

  1. 使用 ptrace(PT_ATTACH) 啓動程序並附加調試器。

  2. 使用 ptrace 在匹配所提供的正則表達式的每個函數處設置斷點,並在被跟蹤的進程的可執行內存中插入斷點指令。

  3. 另外,在該函數的每個返回指令處設置斷點。

  4. 再次使用 ptrace(PT_CONT) 繼續程序。

  5. 此步驟可能涉及多次ptrace調用,因爲我們需要讀取函數入口的 CPU 寄存器、堆棧上的內存以及如果必須取消指針引用的堆上的內存。

  6. 再次使用ptrace(PT_CONT)繼續程序。

  7. 在函數返回時遇到斷點,通過讀取變量,可能涉及到更多的ptrace調用,以讀取寄存器和內存。

  8. 再次使用ptrace(PT_CONT)繼續程序。

  9. 直到程序結束。

顯然,函數的參數和返回值越多,每次停止就越昂貴。所有調試器花費在進行 ptrace 系統調用的時間,我們跟蹤的程序都處於暫停狀態,沒有執行任何指令。從用戶的角度來看,這使得程序的運行速度比原本要慢得多。現在,對於開發和調試來說,這也許不是什麼大問題,但是時間是寶貴的,我們應該儘量快速地完成事情。程序在跟蹤過程中的運行速度越快,你就能越快找到問題的根本原因。

現在的問題是,我們如何使其更好呢?在下一節中,我們將討論新的基於 eBPF 的後端,以及它如何改進這種方法。

eBPF 爲何比 ptrace 更快

一個最大的速度和效率改進是避免大量的系統調用開銷。這是 eBPF 發揮作用的地方,因爲我們可以在函數入口和出口設置 uprobes,並將小 eBPF 程序附加到它們上。Delve 使用 Cilium eBPF Go 庫加載和與 eBPF 程序交互。

每次觸發 probe 時,內核將調用我們的 eBPF 程序,然後在它完成後繼續主程序。我們編寫的小 eBPF 程序將處理函數入口和出口中列出的所有步驟,但不會有所有的系統調用上下文切換,因爲程序直接在內核空間中執行。我們的 eBPF 程序可以通過 eBPF 環形緩衝區和映射數據結構與用戶空間中的調試器通信,使 Delve 能夠收集所需的所有信息。

這種方法的優點是,我們正在跟蹤的程序需要暫停的時間大大減少。在觸發 probe 時運行我們的 eBPF 程序比在函數入口和出口處調用多個系統調用要快得多。

使用 eBPF 調試與跟蹤步驟

這裏再概括一遍使用 eBPF 跟蹤調試的流程:

  1. 啓動程序並使用 ptrace(PT_ATTACH) 附加到進程上。

  2. 在內核中加載所有需要跟蹤的函數的 uprobes。

  3. 使用 ptrace(PT_CONT) 繼續執行程序。

  4. 在函數入口和出口觸發 uprobes。每當 probe 被觸發,內核部分將運行我們的 eBPF 程序,該程序獲取函數的參數或返回值,並將其發送回用戶空間。在用戶空間中,從 eBPF 環形緩衝區讀取函數參數和返回值。

  5. 重複此過程直到程序結束。

通過使用這種方法,Delve 可以比使用默認的 ptrace 實現更快地跟蹤程序。現在,你可能會問,爲什麼不將這種方法默認使用?事實上,未來很有可能會成爲默認方法。但目前,仍在進行開發,以改進這種基於 eBPF 的後端並確保它與基於 ptrace 的後端具有平衡性。然而,您仍然可以在執行 dlv trace 時使用 --ebpf 標誌來使用它。

爲了給出一個使用不同跟蹤方法的程序的效率差異的大致數字,我測量了另一個程序的運行情況,如下所示:

Program execution: 23.7µs

With eBPF trace: 683.1µs

With ptrace tracing: 2.3s

數字本身就是最好的證明!

爲什麼不使用 uretprobe

如果您熟悉 eBPF、uprobes / uretprobes,您可能會問爲什麼我們對一切都使用 uprobes,而不是僅使用 uretprobes 捕獲返回參數。關於此的解釋相當複雜,但簡短版本是,Go 運行時在執行 Go 程序過程中需要多次檢查調用堆棧。當 uretprobes 附加到函數時,它們將該函數的返回地址覆蓋在堆棧上。當 Go 運行時檢查堆棧時,它會找到該函數的意外返回地址,最終會導致程序致命退出。爲了解決這個問題,我們只需使用 uprobes,並利用 Delve 的能力檢查程序的機器指令來在每個函數的返回指令處設置探測器。

Delve 使用 eBPF 更快地調試 Go 代碼

Delve 的總體目標是幫助開發人員儘快地找到 Go 代碼中的錯誤。爲此,我們利用最新的方法和技術,並試圖推動調試器可以完成的範圍。Delve 在內部利用 eBPF 來最大化效率和速度。用戶空間跟蹤是任何工程師工具箱中的重要工具,我們的目標是使其高效易用。

原文:

How debugging Go programs with Delve and eBPF is faster[3]

參考資料

[1]

Delve: https://github.com/go-delve/delve

[2]

strace: https://strace.io/

[3]

How debugging Go programs with Delve and eBPF is faster: https://developers.redhat.com/articles/2023/02/13/how-debugging-go-programs-delve-and-ebpf-faster

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/W38uermEy5jilyT4Yp9PKQ