今天講講怎麼讓golang程式生成coredump文件,並且進行調試的。 別看我寫了不少golang的博客,其實我平時寫c++的時間更多,所以也算和coredump是老相識了。`core dump`文件實際上是進程在某個時間點時的記憶體映像,當時進程使用的記憶體是啥樣就會被原樣保存下來存在文件系統的某個 ...
今天講講怎麼讓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
一模一樣:
這個函數優先順序比環境變數高,但有個限制,它只能設置比環境變數的值列印更多信息的值,也就是說如果環境變數是all
,那麼這個函數就只能設置system
和crash
,不能設置none
和single
。
代碼例子:
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
然後按下麵的步驟查看信息:
bt
,查看當前的調用堆棧,找到觸發panic的那行代碼在哪個frame(棧幀)里- 看到是編號為10的frame,使用
frame 10
進入這個棧幀 - 使用
locals
查看當前棧幀內變數的值 p <變數名/表達式>
查看變數的具體內容,或者執行一些簡單的表達式- 還可以修改變數的值,設置斷點後再次運行查看結果,不過例子里的問題到第四步就已經明瞭了。
這裡的問題很明顯:數組長度是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://linderud.dev/blog/coredumpctl-delve-and-debug-packages-for-go/