讓golang程序生成coredump文件並進行調試

今天講講怎麼讓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子命令可以看到存放這些文件的路徑和崩潰的進程的信息:

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. 還可以修改變量的值,設置斷點後再次運行查看結果,不過例子裏的問題到第四步就已經明瞭了。

coredump-debug

這裏的問題很明顯:數組長度是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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章