讓golang程式生成coredump檔案並進行偵錯

2023-07-08 12:00:38

今天講講怎麼讓golang程式生成coredump檔案,並且進行偵錯的。

別看我寫了不少golang的部落格,其實我平時寫c++的時間更多,所以也算和coredump是老相識了。core dump檔案實際上是程序在某個時間點時的記憶體映像,當時程序使用的記憶體是啥樣就會被原樣儲存下來存在檔案系統的某個位置上,這個時間點一般是觸發了SIGSEGV或者SIGABRT這兩個訊號的時候,當程序的記憶體映像儲存完畢後進程就會異常終止,也就是大家喜聞樂見的「程式崩了」和「段錯誤:核心已轉儲」。

因此coredump就像是程式出錯崩潰後的「第一現場」,是用來排查錯誤的主要資源。

不過我很少在golang裡偵錯coredump檔案,通常來說可靠的紀錄檔和panic時列印的錯誤資訊加堆疊就足夠定位錯誤了。然而有時光靠這些資訊還不夠,不得不去求助老朋友coredump了。

下面我們主要針對這段程式碼偵錯,這只是個事例,所以你一眼看出問題在哪了也不要介意:

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	for {
		index := rand.Intn(11)
		fmt.Println(arr[index])
	}
}

編譯並執行這段程式碼,執行上一小會兒就會看到程式panic了。假設報錯資訊沒能幫助我們定位問題,接下來我們看看如何用coredump偵錯golang程式。

如何讓golang程式生成coredump

首先,如果你不做任何額外的設定,那麼golang程式崩潰的時候只會列印崩潰資訊和簡單的呼叫棧資訊,並不會生成coredump檔案。

想改變這個行為有兩種方式:設定環境變數和在程式碼裡呼叫相關的標準庫介面。

在這之前先用ulimit命令檢測下系統當前能不能生成coredump:

$ ulimit -c
unlimited

如果是unlimited就表示可以,如果是0那就不會生成,需要修改ulimit的設定。

修改GOTRACEBACK環境變數

我們先看修改環境變數的辦法。

GOTRACEBACK是用來控制panic發生時golang程式行為的,值是字串,具體內容如下:

行為
none 不列印任何堆疊跟蹤資訊,不過崩潰的原因和哪行程式碼觸發的panic還是會列印
single 只列印當前正在執行的觸發panic的goroutine的堆疊以及runtime的堆疊;如果panic是runtime裡發出的,則列印所有goroutine的堆疊跟蹤資訊
all 列印所有使用者建立的goroutine的堆疊資訊(不包含runtime的)
system 在前面all的基礎上把runtime相關的所有協程的堆疊資訊也一起列印出來
crash 列印的內容和前面system一樣,但還會額外生成對應作業系統上的coredump檔案

將這個環境變數設定成crash就可以獲得資訊最全面的coredump檔案。所以我們要做的就是像下面這樣:

go build main.go
GOTRACEBACK=crash ./main

或者你嫌麻煩,那就在伺服器系統裡做全域性設定,一般是修改/etc/profile:

# 其他內容
# 全域性設定,需要讓所有已登入的使用者登出對談重新登入或者乾脆重啟系統才會生效
export GOTRACEBACK=crash

上面的全域性設定是針對Linux的,Windows就按正常設定環境變數那樣操作,然後重新登入使用者即可。

這樣執行後就會生成coredump檔案了。一般會生成在當前的工作目錄裡。

還有一點要注意:如果你正在使用較新的linux發行版,那麼coredump檔案會被coredumpctl接管,並不會生成在當前目錄

可以看到coredump檔案被集中管理了,使用info子命令可以看到存放這些檔案的路徑和崩潰的程序的資訊:

其中的present表示coredump的檔案還儲存著,可以用來偵錯,missing的哪些就程式碼coredump檔案已經沒了。

想要用dlv來偵錯的話得用這樣的命令:

coredumpctl debug <list那給出的崩潰的程序的id> --debugger=<偵錯程式程式的名字或路徑> -A <傳給偵錯程式的引數>

填一下空就是這樣:

coredumpctl debug 156814 --debugger=dlv -A core ./main

這樣就能正常進行偵錯了。另外編譯main程式的時候記得把優化關了,以免程式碼被優化得和寫的不一樣導致沒法偵錯。

coredumpctl除了把coredump檔案壓縮了一下節約了一點硬碟空間之外沒有什麼優勢,整個就體現了systemd家族的臭毛病:多管閒事。

使用標準庫介面

沒有標準庫函數可以主動觸發coredump生成,但有可以在程式碼裡設定panic時候的行為的,使用的值和GPTRACEBACK一模一樣:

debug.SetTraceback

這個函數優先順序比環境變數高,但有個限制,它只能設定比環境變數的值列印更多資訊的值,也就是說如果環境變數是all,那麼這個函數就只能設定systemcrash,不能設定nonesingle

程式碼例子:

package main

import (
	"fmt"
	"math/rand"
+   "runtime/debug"
)

func main() {
+	debug.SetTraceback("crash")
	arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	for {
		index := rand.Intn(11)
		fmt.Println(arr[index])
	}
}

效果和設定環境變數一樣,這裡就不展示了。

我該用哪個

沒什麼特別的需求的話,我推薦你只用GOTRACEBACK環境變數。

環境變數可以在不修改程式碼或者組態檔的情況下控制程式的行為,不需要花時間改程式碼改設定然後再編譯執行。用標準庫的介面想達到類似效果就得寫不少程式碼了。

還有個好處是方便在容器裡管理,也符合雲原生十二要素。

偵錯coredump

coredump裡儲存了程式崩潰前的所有狀態,包括執行到哪行程式碼了,各個變數的值是什麼,還包含了runtime當前的狀態等等。

仔細檢查這些資訊就可以發現程式崩潰的原因。

還是用這條命令開啟偵錯程式:

coredumpctl debug 156814 --debugger=dlv -A core ./main

然後按下面的步驟檢視資訊:

  1. bt,檢視當前的呼叫堆疊,找到觸發panic的那行程式碼在哪個frame(棧幀)裡
  2. 看到是編號為10的frame,使用frame 10進入這個棧幀
  3. 使用locals檢視當前棧幀內變數的值
  4. p <變數名/表示式>檢視變數的具體內容,或者執行一些簡單的表示式
  5. 還可以修改變數的值,設定斷點後再次執行檢視結果,不過例子裡的問題到第四步就已經明瞭了。

這裡的問題很明顯:陣列長度是10,索引最大隻有9,而index變數的值是10。所以索引存取越界,導致了panic。

QA

Q: 上面只說了panic的時候生成coredump,如果我想要個程式正常執行時的快照該怎麼做?

A: Linux上有不少程序記憶體快照生成工具,不過delve內建的互動式命令dump就可以滿足需求。

具體方法是dlv attach <pid>之後直接執行dump <輸出coredump的檔名>命令,然後退出。或者還有全自動化的:

$ echo 'dump coredump'|dlv attach <pid> ./main --allow-non-terminal-interactive
$ ls -lh

總計 47M
-rw-r--r-- 1 a a  45M  7月 8日 00:34 coredump
-rw-r--r-- 1 a a   25  7月 8日 00:20 go.mod
-rwxr-xr-x 1 a a 1.8M  7月 8日 00:31 main
-rw-r--r-- 1 a a  141  7月 8日 00:30 main.go

可以看到當前目錄下生成了一個名為「coredump」的coredump檔案。

這個命令本身比較耗時,程序用的記憶體越多就越慢,請謹慎在生產環境使用

Q: 這個例子裡沒看出來有偵錯coredump的必要。

A: 是這個例子的問題,它不夠好。我可以簡單舉一個以前遇到的真實情況:

以前有個處理使用者輸入的程式,使用者可以輸入任何utf8字元,程式會簡單處理這些字元然後存到一塊記憶體裡,這東西上線後隔三差五就會panic,每次都是越界存取,但越界的值和發生的時間都沒有規律可言。

最後實在沒辦法,抓了一次coredump,仔細檢查了使用者的輸入,發現是我們的程式碼在處理某些特殊字元時想當然了,沒能正確處理資料的長度。如果光看程式碼本身的話這個問題很難排查。

至於為什麼不把使用者輸入打進紀錄檔,這涉及了隱私和權益問題,不能這麼做,但偵錯完coredump後刪除勉強能規避這些問題。

Q: 我有必要總是開啟coredump嗎?

A: 沒有。正如我前面所說,一般紀錄檔和panic列印的資訊就夠用了。coredump本身會佔據很多磁碟空間,而且在容器裡dump下來的東西容器重啟後就沒了,除非單獨設定資料卷但這非常複雜。

Q: 一些web框架會用recover處理panic,請問這時候還能獲得coredump嗎?

A: 不能。被recover的panic不會觸發coredump。這時候你得想想其他辦法了,比如用第一個QA那的辦法生成個實時快照。

總結

coredump對於golang來說並不常用,但技多不壓身,瞭解一下對以後處理各種問題總是有幫助的。

參考

https://github.com/go-delve/delve/blob/master/Documentation/usage/dlv_attach.md

https://pkg.go.dev/runtime

https://linderud.dev/blog/coredumpctl-delve-and-debug-packages-for-go/