Go 高性能 - 打印調用堆棧

概述

在工程代碼中需要在異常場景打印相應的日誌,記錄重要的上下文信息。如果遇到 panic 或 error 的情況, 這時候就需要詳細的 堆棧信息 作爲輔助來排查問題,本小節就來介紹兩種常見的獲取 堆棧信息 方法, 然後對兩種方法進行基準測試,最後使用測試的結果進行性能對比並分析差異。

runtime.Stack

通過標準庫提供的 runtime.Stack 相關 API 來獲取。

示例

package main

import (
    "fmt"
    "runtime"
)

func main() {
    buf := make([]byte, 1024)
    n := runtime.Stack(buf, true)

    fmt.Printf("%s\n", buf[:n])
}
$ go run main.go

# 輸出如下 (你的輸出代碼路徑應該和這裏的不一樣)
goroutine 1 [running]:
main.main()
    /home/codes/go-high-performance/main.go:10 +0x45
...

測試代碼如下

package performance

import (
    "runtime"
    "testing"
)

func Benchmark_StackDump(b *testing.B) {
    for i := 0; i < b.N; i++ {
        buf := make([]byte, 1024)
        n := runtime.Stack(buf, true)

        _ = buf[:n]
    }
}

運行測試,並將基準測試結果寫入文件:

# 運行 1000 次,統計內存分配
$ go test -run='^$' -bench=. -count=1 -benchtime=1000x -benchmem > slow.txt

runtime.Caller

通過標準庫提供的 runtime.Caller 相關 API 來獲取。

示例

package main

import (
    "fmt"
    "runtime"
)

func main() {
    for i := 0; ; i++ {
        if _, file, line, ok := runtime.Caller(i); ok {
            fmt.Printf("file: %s, line: %d\n", file, line)
        } else {
            break
        }
    }
}
$ go run main.go

# 輸出如下 (你的輸出代碼路徑應該和這裏的不一樣)
file: /home/codes/go-high-performance/main.go, line: 10
file: /usr/local/go/src/runtime/proc.go, line: 250
file: /usr/local/go/src/runtime/asm_amd64.s, line: 1594
...

從輸出的結果中可以看到,runtime.Caller 的返回值包含了 文件名稱 和 行號,但是相比 runtime.Stack 的輸出而言, 缺少了 goroutine 和 調用方法 字段,我們可以通過 runtime.Callers 配合 runtime.CallersFrames 輸出和 runtime.Stack 一樣的結果。

package main

import (
    "fmt"
    "runtime"
    "strconv"
    "strings"
)

func main() {
    pcs := make([]uintptr, 16)
    n := runtime.Callers(0, pcs)

    frames := runtime.CallersFrames(pcs[:n])

    var sb strings.Builder
    for {
        frame, more := frames.Next()

        sb.WriteString(frame.Function)
        sb.WriteByte('\n')
        sb.WriteByte('\t')
        sb.WriteString(frame.File)
        sb.WriteByte(':')
        sb.WriteString(strconv.Itoa(frame.Line))
        sb.WriteByte('\n')

        if !more {
            break
        }
    }

    fmt.Println(sb.String())
}
$ go run main.go

# 輸出如下 (你的輸出代碼路徑應該和這裏的不一樣)
runtime.Callers
        /usr/local/go/src/runtime/extern.go:247
main.main
        /home/codes/go-high-performance/main.go:12
runtime.main
        /usr/local/go/src/runtime/proc.go:250
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1594
...

測試代碼

package performance

import (
    "runtime"
    "strconv"
    "strings"
    "testing"
)

func stackDump() string {
    pcs := make([]uintptr, 16)
    n := runtime.Callers(0, pcs)

    frames := runtime.CallersFrames(pcs[:n])

    var buffer strings.Builder
    for {
        frame, more := frames.Next()

        buffer.WriteString(frame.Function)
        buffer.WriteByte('\n')
        buffer.WriteByte('\t')
        buffer.WriteString(frame.File)
        buffer.WriteByte(':')
        buffer.WriteString(strconv.Itoa(frame.Line))
        buffer.WriteByte('\n')

        if !more {
            break
        }
    }

    return buffer.String()
}

func Benchmark_StackDump(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = stackDump()
    }
}

運行測試,並將基準測試結果寫入文件:

# 運行 1000 次,統計內存分配
$ go test -run='^$' -bench=. -count=1 -benchtime=1000x -benchmem > fast.txt

使用 benchstat 比較差異

$ benchstat -alpha=100 fast.txt slow.txt 

# 輸出如下
name          old time/op    new time/op    delta
_StackDump-8    2.28µs ± 0%   68.89µs ± 0%  +2926.85%  (p=1.000 n=1+1)

name          old alloc/op   new alloc/op   delta
_StackDump-8    1.36kB ± 0%    1.02kB ± 0%    -24.71%  (p=1.000 n=1+1)

name          old allocs/op  new allocs/op  delta
_StackDump-8      12.0 ± 0%       1.0 ± 0%    -91.67%  (p=1.000 n=1+1)

輸出的結果分爲了三行,分別對應基準測試期間的: 運行時間、內存分配總量、內存分配次數,可以看到:

性能分析

最根本的差異點在於 runtime.Stack 會觸發 STW 操作。

小結

本小節介紹了兩種獲取堆棧信息的方法,並通過基準測試來分析兩種方法的性能差異,讀者可以在此基礎上封裝自己的高性能組件類庫。

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