1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        如何保留 Go 程序崩潰現(xiàn)場

        共 6472字,需瀏覽 13分鐘

         ·

        2021-11-19 09:44

        沒有消滅一切的銀彈,也沒有可以保證永不出錯的程序。我們應當如何捕捉 Go 程序錯誤?我想同學們的第一反應是:打日志。

        但錯誤日志的能力是有限的。第一,日志是開發(fā)者在代碼中定義的打印信息,我們沒法保證日志信息能包含所有的錯誤情況。第二,在 Go 程序中發(fā)生 panic 時,我們也并不總是能通過 recover 捕獲(沒法插入日志代碼)。

        那線上 Go 程序突然莫名崩潰后,當日志記錄沒有覆蓋到錯誤場景時,還有別的方法排查嗎?

        core dump

        core dump 又即核心轉儲,簡單來說它就是程序意外終止時產生的內存快照。我們可以通過 core dump 文件來調式程序,找出其崩潰原因。

        在 linux 平臺上,可通過ulimit -c命令查看核心轉儲配置,系統(tǒng)默認為 0,表明未開啟 core dump 記錄功能。

        $?ulimit?-c
        0

        可以使用ulimit -c [size]命令指定記錄 core dump 文件的大小,即是開啟 core dump 記錄。當然,如果電腦資源足夠,避免 core dump 丟失或記錄不全,也可執(zhí)行ulimit -c unlimited而不限制 core dump 文件大小。

        那在 Go 程序中,如何開啟 core dump 呢?

        GOTRACEBACK

        我們在你真的懂string與[]byte的轉換了嗎一文中探討過 string 轉 []byte 的黑魔法,如下例所示。

        package?main

        import?(
        ?"reflect"
        ?"unsafe"
        )

        func?String2Bytes(s?string)?[]byte?{
        ?sh?:=?(*reflect.StringHeader)(unsafe.Pointer(&s))
        ?bh?:=?reflect.SliceHeader{
        ??Data:?sh.Data,
        ??Len:??sh.Len,
        ??Cap:??sh.Len,
        ?}
        ?return?*(*[]byte)(unsafe.Pointer(&bh))
        }

        func?Modify()?{
        ?a?:=?"hello"
        ?b?:=?String2Bytes(a)
        ?b[0]?=?'H'
        }

        func?main()?{
        ?Modify()
        }

        string 是不可以被修改的,當我們將 string 類型通過黑魔法轉為 []byte 后,企圖修改其值,程序會發(fā)生一個不能被 recover 捕獲到的錯誤。

        $?go?run?main.go
        unexpected?fault?address?0x106a6a4
        fatal?error:?fault
        [signal?SIGBUS:?bus?error?code=0x2?addr=0x106a6a4?pc=0x105b01a]

        goroutine?1?[running]:
        runtime.throw({0x106a68b,?0x0})
        ?/usr/local/go/src/runtime/panic.go:1198?+0x71?fp=0xc000092ee8?sp=0xc000092eb8?pc=0x102bad1
        runtime.sigpanic()
        ?/usr/local/go/src/runtime/signal_unix.go:732?+0x1d6?fp=0xc000092f38?sp=0xc000092ee8?pc=0x103f2f6
        main.Modify(...)
        ?/Users/slp/github/PostDemo/coreDemo/main.go:21
        main.main()
        ?/Users/slp/github/PostDemo/coreDemo/main.go:25?+0x5a?fp=0xc000092f80?sp=0xc000092f38?pc=0x105b01a
        runtime.main()
        ?/usr/local/go/src/runtime/proc.go:255?+0x227?fp=0xc000092fe0?sp=0xc000092f80?pc=0x102e167
        runtime.goexit()
        ?/usr/local/go/src/runtime/asm_amd64.s:1581?+0x1?fp=0xc000092fe8?sp=0xc000092fe0?pc=0x1052dc1
        exit?status?2

        這些堆棧信息是由 GOTRACEBACK 變量來控制打印粒度的,它有五種級別。

        • none,不顯示任何 goroutine 堆棧信息
        • single,默認級別,顯示當前 goroutine 堆棧信息
        • all,顯示所有 user (不包括 runtime)創(chuàng)建的 goroutine 堆棧信息
        • system,顯示所有 user + runtime 創(chuàng)建的 goroutine 堆棧信息
        • crash,和 system 打印一致,但會生成 core dump 文件(Unix 系統(tǒng)上,崩潰會引發(fā) SIGABRT 以觸發(fā)core dump)

        如果我們將 GOTRACEBACK 設置為 system ,我們將看到程序崩潰時所有 goroutine 狀態(tài)信息

        $?GOTRACEBACK=system?go?run?main.go
        unexpected?fault?address?0x106a6a4
        fatal?error:?fault
        [signal?SIGBUS:?bus?error?code=0x2?addr=0x106a6a4?pc=0x105b01a]

        goroutine?1?[running]:
        runtime.throw({0x106a68b,?0x0})
        ...

        goroutine?2?[force?gc?(idle)]:
        runtime.gopark(0x0,?0x0,?0x0,?0x0,?0x0)
        ...
        created?by?runtime.init.7
        ?/usr/local/go/src/runtime/proc.go:294?+0x25

        goroutine?3?[GC?sweep?wait]:
        runtime.gopark(0x0,?0x0,?0x0,?0x0,?0x0)
        ...
        created?by?runtime.gcenable
        ?/usr/local/go/src/runtime/mgc.go:181?+0x55

        goroutine?4?[GC?scavenge?wait]:
        runtime.gopark(0x0,?0x0,?0x0,?0x0,?0x0)
        ...
        created?by?runtime.gcenable
        ?/usr/local/go/src/runtime/mgc.go:182?+0x65
        exit?status?2

        如果想獲取 core dump 文件,那么就應該把 GOTRACEBACK 的值設置為 crash 。當然,我們還可以通過 runtime/debug 包中的 SetTraceback 方法來設置堆棧打印級別。

        delve 調試

        delve 是 Go 語言編寫的 Go 程序調試器,我們可以通過 dlv core 命令來調試 core dump。

        首先,通過以下命令安裝 delve

        go?get?-u?github.com/go-delve/delve/cmd/dlv

        還是以上文中的例子為例,我們通過設置 GOTRACEBACK 為 crash 級別來獲取 core dump 文件

        $?tree
        .
        └──?main.go
        $?ulimit?-c?unlimited
        $?go?build?main.go
        $?GOTRACEBACK=crash?./main
        ...
        Aborted?(core?dumped)
        $?tree
        .
        ├──?core
        ├──?main
        └──?main.go
        $?ls?-alh?core
        -rw-------?1?slp?slp?41M?Oct?31?22:15?core

        此時,在同級目錄得到了 core dump 文件 core(文件名、存儲路徑、是否加上進程號都可以配置修改)。

        通過 dlv 調試器來調試 core 文件,執(zhí)行命令格式 dlv core 可執(zhí)行文件名 core文件

        $?dlv?core?main?core
        Type?'help'?for?list?of?commands.
        (dlv)

        命令 goroutines 獲取所有 goroutine 相關信息

        (dlv)?goroutines
        *?Goroutine?1?-?User:?./main.go:21?main.main?(0x45b81a)?(thread?18061)
        ??Goroutine?2?-?User:?/usr/local/go/src/runtime/proc.go:367?runtime.gopark?(0x42ed96)?[force?gc?(idle)]
        ??Goroutine?3?-?User:?/usr/local/go/src/runtime/proc.go:367?runtime.gopark?(0x42ed96)?[GC?sweep?wait]
        ??Goroutine?4?-?User:?/usr/local/go/src/runtime/proc.go:367?runtime.gopark?(0x42ed96)?[GC?scavenge?wait]
        [4?goroutines]
        (dlv)

        Goroutine 1 是出問題的 goroutine (帶有 * 代表當前幀),通過命令 goroutine 1 切換到其棧幀

        (dlv)?goroutine?1
        Switched?from?1?to?1?(thread?18061)
        (dlv)

        執(zhí)行命令 bt(breakpoints trace) 查看當前的棧幀詳細信息

        (dlv)?bt
        0??0x0000000000454bc1?in?runtime.raise
        ???at?/usr/local/go/src/runtime/sys_linux_amd64.s:165
        1??0x0000000000452f60?in?runtime.systemstack_switch
        ???at?/usr/local/go/src/runtime/asm_amd64.s:350
        2??0x000000000042c530?in?runtime.fatalthrow
        ???at?/usr/local/go/src/runtime/panic.go:1250
        3??0x000000000042c2f1?in?runtime.throw
        ???at?/usr/local/go/src/runtime/panic.go:1198
        4??0x000000000043fa76?in?runtime.sigpanic
        ???at?/usr/local/go/src/runtime/signal_unix.go:742
        5??0x000000000045b81a?in?main.Modify
        ???at?./main.go:21
        6??0x000000000045b81a?in?main.main
        ???at?./main.go:25
        7??0x000000000042e9c7?in?runtime.main
        ???at?/usr/local/go/src/runtime/proc.go:255
        8??0x0000000000453361?in?runtime.goexit
        ???at?/usr/local/go/src/runtime/asm_amd64.s:1581
        (dlv)

        通過 5 0x000000000045b81a in main.Modify 發(fā)現(xiàn)了錯誤代碼所在函數(shù),執(zhí)行命令 frame 5 進入函數(shù)具體代碼

        (dlv)?frame?5
        >?runtime.raise()?/usr/local/go/src/runtime/sys_linux_amd64.s:165?(PC:?0x454bc1)
        Warning:?debugging?optimized?function
        Frame?5:?./main.go:21?(PC:?45b81a)
        ????16:?}
        ????17:
        ????18:?func?Modify()?{
        ????19:??a?:=?"hello"
        ????20:??b?:=?String2Bytes(a)
        =>??21:??b[0]?=?'H'
        ????22:?}
        ????23:
        ????24:?func?main()?{
        ????25:??Modify()
        ????26:?}
        (dlv)

        自此,破案了,問題就出在了擅自修改 string 底層值。

        Mac 不能使用

        有一點需要注意,上文 core dump 生成的例子,我是在 linux 系統(tǒng)下完成的,mac amd64 系統(tǒng)沒法弄(很氣,害我折騰了兩個晚上)。

        這是由于 mac 系統(tǒng)下的 Go 限制了生成 core dump 文件,這個在 Go 源碼 src/runtime/signal_unix.go 中有相關說明。

        //go:nosplit
        func?crash()?{
        ?//?OS?X?core?dumps?are?linear?dumps?of?the?mapped?memory,
        ?//?from?the?first?virtual?byte?to?the?last,?with?zeros?in?the?gaps.
        ?//?Because?of?the?way?we?arrange?the?address?space?on?64-bit?systems,
        ?//?this?means?the?OS?X?core?file?will?be?>128?GB?and?even?on?a?zippy
        ?//?workstation?can?take?OS?X?well?over?an?hour?to?write?(uninterruptible).
        ?//?Save?users?from?making?that?mistake.
        ?if?GOOS?==?"darwin"?&&?GOARCH?==?"amd64"?{
        ??return
        ?}

        ?dieFromSignal(_SIGABRT)
        }

        總結

        core dump 文件是操作系統(tǒng)提供給我們的一把利器,它是程序意外終止時產生的內存快照。利用 core dump,我們可以在程序崩潰后更好地恢復事故現(xiàn)場,為故障排查保駕護航。

        當然,core dump 文件的生成也是有弊端的。core dump 文件較大,如果線上服務本身內存占用就很高,那在生成 core dump 文件上的內存與時間開銷都會很大。另外,我們往往會布置服務守護進程,如果我們的程序頻繁崩潰和重啟,那會生成大量的 core dump 文件(設定了core+pid 命名規(guī)則),產生磁盤打滿的風險(如果放開了內核限制 ulimit -c unlimited)。

        最后,如果擔心錯誤日志不能幫助我們定位 Go 代碼問題,我們可以為它開啟 core dump 功能,在 hotfix 上增加奇兵。對于有守護進程的服務,建議設置好 ulimt -c 大小限制。



        推薦閱讀


        福利

        我為大家整理了一份從入門到進階的Go學習資料禮包,包含學習建議:入門看什么,進階看什么。關注公眾號 「polarisxu」,回復?ebook?獲?。贿€可以回復「進群」,和數(shù)萬 Gopher 交流學習。

        瀏覽 85
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            色天堂在线| www.黄色在线观看 | 做爱网站在线看入口免费 | 国产AV一区二区三区四区五区六区 | 女配穿书病娇被强啪h | 日韩一级免费播放 | 国产蓝光A级毛片 | 欧美成人一区二区三区片免费 | 国产又粗又猛又爽又黄91网站 | 日韩欧美在线免费观看 |