Go Wiki:偵錯 Go 程式中的效能問題

– 最初由 Dmitry Vyukov 撰寫

假設您有一個 Go 程式,並想要提升其效能。有許多可用的工具可以協助您完成這項任務。這些工具可以協助您找出各種類型的熱點(CPU、IO、記憶體),熱點是您需要專注改善以顯著提升效能的地方。不過,還有一種可能結果,這些工具可以協助您找出程式中明顯的效能缺陷。例如,您在每次查詢前準備一個 SQL 陳述式,而您可以在程式啟動時準備一次。另一個範例是,一個 O(N^2) 演算法不知何故出現在一個顯然存在且預期的 O(N) 演算法中。為了找出此類情況,您需要對您在剖析中看到的情況進行健全檢查。例如,對於第一個情況,花費在 SQL 陳述式準備上的大量時間會是一個警訊。

了解效能的各種限制因素也很重要。例如,如果程式透過 100 Mbps 網路連結進行通訊,而且已經使用 >90Mbps,您無法對程式做太多事情來提升其效能。磁碟 IO、記憶體使用量和運算任務也有類似的限制因素。考量到這一點,我們可以檢視可用的工具。

注意:這些工具可能會互相干擾。例如,精確的記憶體剖析會扭曲 CPU 剖析,goroutine 阻擋剖析會影響排程器追蹤等。請單獨使用工具以取得更精確的資訊。

CPU 剖析器

Go 執行時期包含內建 CPU 剖析器,它會顯示哪些函式消耗多少 CPU 時間。您可以透過 3 種方式取得存取權

  1. 最簡單的方式是使用「go test」的 -cpuprofile 旗標(https://pkg.go.dev/cmd/go/#hdr-Description_of_testing_flags)指令。例如,下列指令

    $ go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprof net/http
    

    會剖析指定的基準,並將 CPU 剖析寫入「cprof」檔案。然後

    $ go tool pprof --text http.test cprof
    

    會列印最熱門函式的清單。

    有數種可用的輸出類型,最實用的為:--text--web--list。執行 go tool pprof 以取得完整清單。此選項的明顯缺點是它僅適用於測試。

  2. net/http/pprof:這是網路伺服器的理想解決方案。您只需匯入 net/http/pprof,並使用下列方式收集剖析資料

    $ go tool pprof --text mybin  http://myserver:6060:/debug/pprof/profile
    
  3. 手動剖析資料收集。您需要匯入 runtime/pprof,並將下列程式碼新增至 main 函式

    if *flagCpuprofile != "" {
        f, err := os.Create(*flagCpuprofile)
        if err != nil {
            log.Fatal(err)
        }
        pprof.StartCPUProfile(f)
        defer pprof.StopCPUProfile()
    }
    

    剖析資料將寫入指定的檔案,並以與第一個選項相同的方式視覺化。

以下是使用 --web 選項視覺化的剖析資料範例:[cpu_profile.png]

您可以使用 --list=funcname 選項來調查單一函式。例如,下列剖析資料顯示時間花費在 append 函式中

 .      .   93: func (bp *buffer) WriteRune(r rune) error {
 .      .   94:     if r < utf8.RuneSelf {
 5      5   95:         *bp = append(*bp, byte(r))
 .      .   96:         return nil
 .      .   97:     }
 .      .   98:
 .      .   99:     b := *bp
 .      .  100:     n := len(b)
 .      .  101:     for n+utf8.UTFMax > cap(b) {
 .      .  102:         b = append(b, 0)
 .      .  103:     }
 .      .  104:     w := utf8.EncodeRune(b[n:n+utf8.UTFMax], r)
 .      .  105:     *bp = b[:n+w]
 .      .  106:     return nil
 .      .  107: }

剖析器在無法解開堆疊時,還會使用 3 個特殊項目:GC、System 和 ExternalCode。GC 表示垃圾回收期間花費的時間,請參閱下方的記憶體剖析器和垃圾回收器追蹤區段,以取得最佳化建議。System 表示花費在 goroutine 排程器、堆疊管理程式碼和其他輔助執行時間程式碼的時間。ExternalCode 表示花費在原生動態函式庫的時間。

以下是關於如何詮釋剖析資料中所見內容的一些提示。

如果您看到大量時間花費在 runtime.mallocgc 函式中,則程式可能會過度配置大量小記憶體。剖析資料將告訴您配置的來源。請參閱記憶體剖析器區段,以取得如何最佳化此案例的建議。

如果在通道操作、sync.Mutex 程式碼和其他同步原語或系統元件中花費大量時間,程式可能遭受競爭。考慮重新建構程式以消除頻繁存取的共用資源。常見的技術包括分片/分割、區域快取/批次處理和寫入時複製技術。

如果在 syscall.Read/Write 中花費大量時間,程式可能會進行過多的小量讀寫。os.File 或 net.Conn 周圍的 Bufio 封裝可在此情況中提供協助。

如果在 GC 元件中花費大量時間,程式可能會配置過多暫時物件,或堆積大小過小,因此垃圾收集過於頻繁。請參閱垃圾收集器追蹤器和記憶體分析器部分,以取得最佳化建議。

注意:對於 darwin CPU 分析器,目前僅 適用於 El Capitan 或更新版本

注意:在 Windows 上,您需要安裝 Cygwin、Perl 和 Graphviz 才能產生 svg/web 分析檔。

記憶體分析器

記憶體分析器會顯示哪些函式配置堆積記憶體。您可以透過類似 CPU 分析檔的方式收集它:使用 go test --memprofile (https://pkg.go.dev/cmd/go/#hdr-Description_of_testing_flags),使用 net/http/pprof 透過 http://myserver:6060:/debug/pprof/heap 或呼叫 runtime/pprof.WriteHeapProfile

您只能視覺化在收集分析檔時存在的配置(--inuse_space 旗標傳遞給 pprof,預設),或自程式啟動以來發生的所有配置(--alloc_space 旗標傳遞給 pprof)。前者適用於在執行中應用程式上使用 net/http/pprof 收集的分析檔,後者適用於在程式結束時收集的分析檔(否則您將看到幾乎為空的分析檔)。

注意:記憶體剖析器是採樣,也就是說,它只收集記憶體配置某個子集的資訊。採樣物件的機率與其大小成正比。您可以使用 go test --memprofilerate 旗標或在程式啟動時設定 runtime.MemProfileRate 變數來變更採樣率。1 的速率將導致收集所有配置的資訊,但它可能會減慢執行速度。預設採樣率為每 512KB 配置記憶體 1 個樣本。

您也可以視覺化配置的位元組數或配置的物件數(分別為 --inuse/alloc_space--inuse/alloc_objects 旗標)。剖析器在剖析期間傾向於更多地採樣較大的物件。但重要的是要了解,大型物件會影響記憶體消耗和 GC 時間,而大量微小配置會影響執行速度(以及在某種程度上也會影響 GC 時間)。因此,查看兩者可能很有用。

物件可以是持久的或暫時的。如果您在程式啟動時配置了幾個大型持久物件,剖析器很可能會對它們進行採樣(因為它們很大)。此類物件確實會影響記憶體消耗和 GC 時間,但它們不會影響正常的執行速度(不會對它們執行記憶體管理操作)。另一方面,如果您有大量生命週期非常短的物件,它們幾乎無法在剖析中表示(如果您使用預設的 --inuse_space 模式)。但它們確實會顯著影響執行速度,因為它們會不斷地配置和釋放。因此,再次強調,查看兩種類型的物件可能很有用。因此,一般來說,如果您想要減少記憶體消耗,您需要查看在正常程式操作期間收集的 --inuse_space 剖析。如果您想要提高執行速度,請查看在大量執行時間後或在程式結束時收集的 --alloc_objects 剖析。

有數個旗標可控制報告詳細程度。--functions 使 pprof 在函式層級上報告(預設)。--lines 使 pprof 在原始碼行層級上報告,這在熱門函式在不同行上配置時很有用。還有 --addresses--files 分別用於精確的指令位址和檔案層級。

記憶體剖析有一個有用的選項 – 你可以在瀏覽器中直接查看它(前提是你已匯入 net/http/pprof)。如果你開啟 http://myserver:6060/debug/pprof/heap?debug=1,你應該會看到堆疊剖析,類似於

heap profile: 4: 266528 [123: 11284472] @ heap/1048576
1: 262144 [4: 376832] @ 0x28d9f 0x2a201 0x2a28a 0x2624d 0x26188 0x94ca3 0x94a0b 0x17add6 0x17ae9f 0x1069d3 0xfe911 0xf0a3e 0xf0d22 0x21a70
#   0x2a201 cnew+0xc1   runtime/malloc.goc:718
#   0x2a28a runtime.cnewarray+0x3a          runtime/malloc.goc:731
#   0x2624d makeslice1+0x4d             runtime/slice.c:57
#   0x26188 runtime.makeslice+0x98          runtime/slice.c:38
#   0x94ca3 bytes.makeSlice+0x63            bytes/buffer.go:191
#   0x94a0b bytes.(*Buffer).ReadFrom+0xcb       bytes/buffer.go:163
#   0x17add6    io/ioutil.readAll+0x156         io/ioutil/ioutil.go:32
#   0x17ae9f    io/ioutil.ReadAll+0x3f          io/ioutil/ioutil.go:41
#   0x1069d3    godoc/vfs.ReadFile+0x133            godoc/vfs/vfs.go:44
#   0xfe911 godoc.func·023+0x471            godoc/meta.go:80
#   0xf0a3e godoc.(*Corpus).updateMetadata+0x9e     godoc/meta.go:101
#   0xf0d22 godoc.(*Corpus).refreshMetadataLoop+0x42    godoc/meta.go:141

2: 4096 [2: 4096] @ 0x28d9f 0x29059 0x1d252 0x1d450 0x106993 0xf1225 0xe1489 0xfbcad 0x21a70
#   0x1d252 newdefer+0x112              runtime/panic.c:49
#   0x1d450 runtime.deferproc+0x10          runtime/panic.c:132
#   0x106993    godoc/vfs.ReadFile+0xf3         godoc/vfs/vfs.go:43
#   0xf1225 godoc.(*Corpus).parseFile+0x75      godoc/parser.go:20
#   0xe1489 godoc.(*treeBuilder).newDirTree+0x8e9   godoc/dirtrees.go:108
#   0xfbcad godoc.func·002+0x15d            godoc/dirtrees.go:100

每個條目的開頭數字 ("1: 262144 [4: 376832]") 分別代表目前存在的物件數目、存活物件佔用的記憶體量、配置的總數目和所有配置佔用的記憶體量。

最佳化通常取決於應用程式,但以下是一些常見建議。

  1. 將物件合併成較大的物件。例如,用 bytes.Buffer 取代 *bytes.Buffer 結構成員(稍後你可以呼叫 bytes.Buffer.Grow 來預先配置寫入的緩衝區)。這將減少記憶體配置的次數(較快),並降低垃圾收集器的負擔(較快的垃圾收集)。

  2. 超出宣告範圍的局部變數會提升為堆疊配置。編譯器通常無法證明數個變數具有相同的生命週期,因此會個別配置每個此類變數。因此你也可以對局部變數使用上述建議。例如,用

    for k, v := range m {
        k, v := k, v   // copy for capturing by the goroutine
        go func() {
            // use k and v
        }()
    }
    

    取代

    for k, v := range m {
        x := struct{ k, v string }{k, v}   // copy for capturing by the goroutine
        go func() {
            // use x.k and x.v
        }()
    }
    

    這會用單一配置取代兩個記憶體配置。然而,此最佳化通常會對程式碼可讀性產生負面影響,因此請合理使用。

  3. 配置組合的一個特殊案例是切片陣列預配置。如果您知道切片的典型大小,則可以為其預先配置一個備份陣列,如下所示

    type X struct {
        buf      []byte
        bufArray [16]byte // Buf usually does not grow beyond 16 bytes.
    }
    
    func MakeX() *X {
        x := &X{}
        // Preinitialize buf with the backing array.
        x.buf = x.bufArray[:0]
        return x
    }
    
  4. 如果可能,請使用較小的資料類型。例如,使用 int8 而不是 int

  5. 不包含任何指標的物件(請注意字串、切片、映射和通道包含隱含指標),不會被垃圾收集器掃描。例如,1GB 位元組切片實際上不會影響垃圾收集時間。因此,如果您從活動使用的物件中移除指標,它可以對垃圾收集時間產生正面的影響。一些可能性是:用索引取代指標,將物件分割成兩部分,其中一部分不包含指標。

  6. 使用自由清單來重複使用暫態物件並減少配置數量。標準程式庫包含 sync.Pool 類型,允許在垃圾收集之間重複使用同一個物件多次。但是,請注意,與任何手動記憶體管理方案一樣,不正確使用 sync.Pool 可能會導致使用後釋放錯誤。

您還可以使用垃圾收集器追蹤(見下文)來深入了解記憶體問題。

TODO(dvyukov):提到統計資料會以遞延方式更新:「Memprof 統計資料會以遞延方式更新。這是為了在持續配置和隨後分批釋放的情況下,呈現一致的畫面。數個連續的 GC 會推動更新管線向前。這是您觀察到的情況。因此,如果您剖析即時伺服器,任何範例都會提供一致的快照。然而,如果程式完成某項活動,而您想在活動後收集快照,則需要在收集前執行 2 或 3 個 GC。」

區塊剖析器

區塊剖析器會顯示 goroutine 在同步原語(包括計時器頻道)上區塊等待的位置。您可以用類似於 CPU 剖析的方式收集它:使用 go test --blockprofile (https://pkg.go.dev/cmd/go/#hdr-Description_of_testing_flags),使用 net/http/pprof 透過 http://myserver:6060:/debug/pprof/block 或呼叫 runtime/pprof.Lookup(“block”).WriteTo

但有一個重要的注意事項 - 區塊剖析器並未預設啟用。go test --blockprofile 會自動為您啟用它。然而,如果您使用 net/http/pprofruntime/pprof,您需要手動啟用它(否則剖析檔會是空的)。若要啟用區塊剖析器,請呼叫 runtime.SetBlockProfileRate。SetBlockProfileRate 控制區塊剖析檔中報告的 goroutine 區塊事件比例。剖析器的目標是對每指定數量的奈秒區塊時間平均抽樣一個區塊事件。若要將每個區塊事件包含在剖析檔中,請將比率設定為 1。

如果函式包含數個區塊作業,而且不清楚哪一個導致區塊,請對 pprof 使用 --lines 旗標。

請注意,並非所有封鎖都是不好的。當 goroutine 遭封鎖時,底層工作執行緒只會切換到另一個 goroutine。因此,在協同 Go 環境中封鎖與在非協同系統中封鎖互斥鎖大不相同(例如典型的 C++ 或 Java 執行緒函式庫,其中封鎖會導致執行緒閒置和昂貴的執行緒內容切換)。為了讓您有些概念,我們來考慮一些範例。

封鎖在 time.Ticker 通常是沒問題的。如果一個 goroutine 在 Ticker 上封鎖 10 秒,您會在剖析中看到 10 秒的封鎖,這完全沒問題。封鎖在 sync.WaitGroup 通常是沒問題的。例如,如果一個工作需要 10 秒,goroutine 在 WaitGroup 上等待完成會在剖析中造成 10 秒的封鎖。封鎖在 sync.Cond 可能沒問題,也可能會有問題,這取決於情況。使用者封鎖在通道表示生產者速度過慢或缺乏工作。生產者封鎖在通道表示使用者速度較慢,但這通常沒問題。封鎖在基於通道的旗標顯示有多少 goroutine 在旗標上受到限制。封鎖在 sync.Mutex 或 sync.RWMutex 通常是不好的。您可以在視覺化過程中使用 --ignore 旗標對 pprof 排除已知不感興趣的封鎖。

goroutine 的封鎖可能導致兩個負面後果

  1. 由於缺乏工作,程式無法隨著處理器擴充。排程器追蹤有助於找出這種情況。

  2. 過度的 goroutine 封鎖/解除封鎖會消耗 CPU 時間。CPU 分析器有助於找出這種情況(查看系統元件)。

以下是幾個有助於減少 goroutine 封鎖的常見建議

  1. 在生產者-使用者場景中使用足夠緩衝的通道。未緩衝的通道會大幅限制程式中可用的平行處理。

  2. 對於讀取為主的負載,請使用 sync.RWMutex 代替 sync.Mutex。在 sync.RWMutex 中,讀取器絕不會阻擋其他讀取器,即使是在實作層級上也是如此。

  3. 在某些情況下,可以透過使用寫入時複製技術來完全移除互斥鎖。如果受保護的資料結構修改頻率不高,而且可以複製,則可以如下所示進行更新

    type Config struct {
        Routes   map[string]net.Addr
        Backends []net.Addr
    }
    
    var config atomic.Value  // actual type is *Config
    
    // Worker goroutines use this function to obtain the current config.
      // UpdateConfig must be called at least once before this func.
    func CurrentConfig() *Config {
        return config.Load().(*Config)
    }
    
    // Background goroutine periodically creates a new Config object
    // as sets it as current using this function.
    func UpdateConfig(cfg *Config) {
      config.Store(cfg)
    }
    

    此模式可防止寫入器在更新期間阻擋讀取器。

  4. 分割是減少共用可變資料結構上競爭/阻擋的另一種一般技術。以下是分割雜湊映射的範例

    type Partition struct {
        sync.RWMutex
        m map[string]string
    }
    
    const partCount = 64
    var m [partCount]Partition
    
    func Find(k string) string {
        idx := hash(k) % partCount
        part := &m[idx]
        part.RLock()
        v := part.m[k]
        part.RUnlock()
        return v
    }
    
  5. 本機快取和批次更新有助於減少不可分割資料結構上的競爭。以下是批次傳送至通道的方式

    const CacheSize = 16
    
    type Cache struct {
        buf [CacheSize]int
        pos int
    }
    
    func Send(c chan [CacheSize]int, cache *Cache, value int) {
        cache.buf[cache.pos] = value
        cache.pos++
        if cache.pos == CacheSize {
            c <- cache.buf
            cache.pos = 0
        }
    }
    

    此技術不限於通道。它可用於批次更新映射、批次配置等。

  6. 使用 sync.Pool 作為免費清單,而不是基於通道或受互斥鎖保護的免費清單。sync.Pool 內部使用智慧技術來減少阻擋。

Goroutine Profiler

goroutine profiler 只會提供程序中所有執行中 goroutine 的目前堆疊。它可用於除錯負載平衡問題(請參閱下方的排程器追蹤區段),或除錯死結。此剖析僅對執行中的應用程式有意義,因此 go test 沒有公開它。您可以使用 net/http/pprof 透過 http://myserver:6060:/debug/pprof/goroutine 收集剖析,並將它視覺化為 svg/pdf 或呼叫 runtime/pprof.Lookup(“goroutine”).WriteTo。但最實用的方式是在瀏覽器中輸入 http://myserver:6060:/debug/pprof/goroutine?debug=2,這將提供與程式崩潰時所見類似的符號化堆疊。請注意,「syscall」狀態中的 goroutine 會消耗作業系統執行緒,其他 goroutine 則不會(呼叫 runtime.LockOSThread 的 goroutine 除外,不幸的是,這在剖析中不可見)。請注意,「IO 等待」狀態中的 goroutine 也不會消耗執行緒,它們會停放在非阻擋網路輪詢器上(稍後使用 epoll/kqueue/GetQueuedCompletionStatus 取消停放 goroutine)。

垃圾收集器追蹤

除了剖析工具外,還有另一種可用的工具,稱為追蹤器。它們允許追蹤垃圾收集、記憶體配置器和 goroutine 排程器狀態。若要啟用垃圾收集器 (GC) 追蹤,請使用環境變數 GODEBUG=gctrace=1 執行程式

$ GODEBUG=gctrace=1 ./myserver

然後,程式會在執行期間列印類似下列的輸出

gc9(2): 12+1+744+8 us, 2 -> 10 MB, 108615 (593983-485368) objects, 4825/3620/0 sweeps, 0(0) handoff, 6(91) steal, 16/1/0 yields
gc10(2): 12+6769+767+3 us, 1 -> 1 MB, 4222 (593983-589761) objects, 4825/0/1898 sweeps, 0(0) handoff, 6(93) steal, 16/10/2 yields
gc11(2): 799+3+2050+3 us, 1 -> 69 MB, 831819 (1484009-652190) objects, 4825/691/0 sweeps, 0(0) handoff, 5(105) steal, 16/1/0 yields

讓我們來探討這些數字的意義。每一個 GC 會列印一行。第一個數字(「gc9」)是 GC 的編號(這是程式啟動以來的第 9 個 GC)。括號中的數字(「(2)」)是參與 GC 的工作執行緒數量。接下來的 4 個數字(「12+1+744+8 us」)分別表示暫停世界、掃描、標記和等待工作執行緒完成的時間(以微秒為單位)。接下來的 2 個數字(「2 -> 10 MB」)表示前一個 GC 之後的存活堆大小和目前的 GC 之前(包括垃圾)的完整堆大小。接下來的 3 個數字(「108615 (593983-485368) objects」)是堆中物件的總數(包括垃圾)和記憶體配置和釋放操作的總數。接下來的 3 個數字(「4825/3620/0 sweeps」)描述掃描階段(前一個 GC):共有 4825 個記憶體區間,3620 個依需求或在背景中掃描,0 個在暫停世界階段掃描(其餘為未使用的區間)。接下來的 4 個數字(「0(0) handoff, 6(91) steal」)描述並行標記階段的負載平衡:共有 0 個物件移交操作(0 個物件移交),和 6 個竊取操作(91 個物件被竊取)。最後 3 個數字(「16/1/0 yields」)描述並行標記階段的效率:在等待另一個執行緒時,共有 17 個 yield 操作。

GC 是 標記和掃描類型。GC 總計可以表示為

Tgc = Tseq + Tmark + Tsweep

其中 Tseq 是停止使用者 goroutine 和一些準備活動(通常很小)的時間;Tmark 是堆標記時間,標記在所有使用者 goroutine 停止時發生,因此可能會顯著影響處理的延遲;Tsweep 是堆清除時間,清除通常與正常程式執行同時發生,因此對延遲不太重要。

標記時間可以近似表示為

Tmark = C1*Nlive + C2*MEMlive_ptr + C3*Nlive_ptr

其中 Nlive 是 GC 期間堆中活物件的數量,MEMlive_ptr 是有指標的活物件佔用的記憶體量,Nlive_ptr 是活物件中的指標數量。

清除時間可以近似表示為

Tsweep = C4*MEMtotal + C5*MEMgarbage

其中 MEMtotal 是堆記憶體的總量,MEMgarbage 是堆中的垃圾量。

下一次 GC 發生在程式配置與已使用量成正比的額外記憶體量之後。比例由 GOGC 環境變數控制(預設為 100)。如果 GOGC=100 且程式使用 4M 堆記憶體,則當程式達到 8M 時,執行時間將再次觸發 GC。這使 GC 成本與配置成本成線性比例。調整 GOGC 會改變線性常數以及使用的額外記憶體量。

只有清除取決於堆的總大小,且清除與正常程式執行同時發生。因此,如果你能負擔額外的記憶體消耗,則可以將 GOGC 設定為較高的值(200、300、500 等)。例如,GOGC=300 可以將垃圾收集開銷減少多達 2 倍,同時保持延遲不變(以堆大 2 倍為代價)。

GC 是並行的,通常可以很好地擴充硬體並行性。因此,即使對於順序程式,設定 GOMAXPROCS 為較高的值也可能是有意義的,只是為了加速垃圾收集。但是,請注意垃圾收集器執行緒的數量目前限制為 8。

記憶體配置器追蹤

記憶體配置器追蹤會將所有記憶體配置和釋放操作傾印到主控台上。它會透過環境變數 GODEBUG=allocfreetrace=1 來啟用。輸出會類似於

tracealloc(0xc208062500, 0x100, array of parse.Node)
goroutine 16 [running]:
runtime.mallocgc(0x100, 0x3eb7c1, 0x0)
    runtime/malloc.goc:190 +0x145 fp=0xc2080b39f8
runtime.growslice(0x31f840, 0xc208060700, 0x8, 0x8, 0x1, 0x0, 0x0, 0x0)
    runtime/slice.goc:76 +0xbb fp=0xc2080b3a90
text/template/parse.(*Tree).parse(0xc2080820e0, 0xc208023620, 0x0, 0x0)
    text/template/parse/parse.go:289 +0x549 fp=0xc2080b3c50
...

tracefree(0xc208002d80, 0x120)
goroutine 16 [running]:
runtime.MSpan_Sweep(0x73b080)
        runtime/mgc0.c:1880 +0x514 fp=0xc20804b8f0
runtime.MCentral_CacheSpan(0x69c858)
        runtime/mcentral.c:48 +0x2b5 fp=0xc20804b920
runtime.MCache_Refill(0x737000, 0xc200000012)
        runtime/mcache.c:78 +0x119 fp=0xc20804b950
...

追蹤包含記憶體區塊的位址、大小、類型、goroutine ID 和堆疊追蹤。它可能對除錯更有用,但也可以提供非常精細的資訊,用於配置最佳化。

排程器追蹤

排程器追蹤可以提供 goroutine 排程器的動態行為見解,並允許除錯負載平衡和可擴充性問題。若要啟用排程器追蹤,請使用環境變數 GODEBUG=schedtrace=1000 執行程式(值表示輸出的週期,以毫秒為單位,在本例中為每秒一次)

$ GODEBUG=schedtrace=1000 ./myserver

然後,程式會在執行期間列印類似下列的輸出

SCHED 1004ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=4 runqueue=8 [0 1 0 3]
SCHED 2005ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=5 runqueue=6 [1 5 4 0]
SCHED 3008ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=4 runqueue=10 [2 2 2 1]

第一個數字(「1004 毫秒」)是自程式啟動以來的時間。Gomaxprocs 是 GOMAXPROCS 的目前值。Idleprocs 是閒置處理器的數量(其餘正在執行 Go 程式碼)。Threads 是排程器建立的工作執行緒總數(執行緒可以有 3 種狀態:執行 Go 程式碼(gomaxprocs-idleprocs)、執行系統呼叫/cgocalls 或閒置)。Idlethreads 是閒置工作執行緒的數量。Runqueue 是可執行 goroutine 的全域佇列長度。方括號中的數字(「[0 1 0 3]」)是可執行 goroutine 的每個處理器佇列長度。全域和區域佇列長度的總和代表可執行 goroutine 的總數。

注意:您可以將任何追蹤器組合為 GODEBUG=gctrace=1,allocfreetrace=1,schedtrace=1000。

注意:還有一個詳細的排程器追蹤,您可以使用 GODEBUG=schedtrace=1000,scheddetail=1 來啟用它。它會列印每個 goroutine、工作執行緒和處理器的詳細資訊。我們不會在此說明其格式,因為它主要對排程器開發人員有用;但您可以在 src/pkg/runtime/proc.c 中找到詳細資訊。

當程式無法隨 GOMAXPROCS 線性擴充,和/或無法消耗 100% 的 CPU 時間時,排程器追蹤很有用。理想情況是所有處理器都忙於執行 Go 程式碼,執行緒數量合理,所有佇列中都有大量工作,且工作合理地平均分配

gomaxprocs=8 idleprocs=0 threads=40 idlethreads=5 runqueue=10 [20 20 20 20 20 20 20 20]

糟糕的情況是以上某項不成立。例如,以下範例示範了工作不足以讓所有處理器保持忙碌

gomaxprocs=8 idleprocs=6 threads=40 idlethreads=30 runqueue=0 [0 2 0 0 0 1 0 0]

注意:使用作業系統提供的工具來測量實際 CPU 使用率,作為最終特徵。在 Unix 系列作業系統上,是 top 指令;在 Windows 上,是工作管理員。

在工作不足的情況下,你可以使用 goroutine 分析器來了解 goroutine 在哪裡封鎖。請注意,只要所有處理器都忙碌,負載不平衡最終並非壞事,只會造成一些適度的負載平衡開銷。

記憶體統計資料

Go 執行時期透過 runtime.ReadMemStats 函式公開粗略的記憶體統計資料。統計資料也透過 net/http/pprof 顯示在 http://myserver:6060/debug/pprof/heap?debug=1 的底部。統計資料在此處 說明。一些有趣的欄位是

  1. HeapAlloc - 目前堆積大小。
  2. HeapSys - 總堆積大小。
  3. HeapObjects - 堆積中的物件總數。
  4. HeapReleased - 釋放到作業系統的記憶體量;執行時期會將 5 分鐘未使用的記憶體釋放到作業系統,你可以使用 runtime/debug.FreeOSMemory 強制執行此程序。
  5. Sys - 從作業系統分配的記憶體總量。
  6. Sys-HeapReleased - 程式的有效記憶體消耗。
  7. StackSys - goroutine 堆疊消耗的記憶體(請注意,有些堆疊是從堆中配置的,並且未在此處計算,不幸的是,沒有辦法取得堆疊的總大小 (https://code.google.com/p/go/issues/detail?id=7468))
  8. MSpanSys/MCacheSys/BuckHashSys/GCSys/OtherSys - 執行時期為各種輔助目的配置的記憶體量;它們通常並不有趣,除非它們太高。
  9. PauseNs - 上次垃圾收集的持續時間。

堆疊傾印器

最後一個可用的工具是堆疊傾印器,它可以將整個堆疊的狀態寫入檔案中,以供日後探索。對於識別記憶體外洩和深入瞭解程式記憶體消耗,它可能很有用。

首先,您需要使用 runtime/debug.WriteHeapDump 函式來寫入傾印。

    f, err := os.Create("heapdump")
    if err != nil { ... }
    debug.WriteHeapDump(f.Fd())

然後,您可以將它呈示為堆疊圖形表示的 dot 檔案,或將它轉換為 hprof 格式。若要將它呈示為 dot 檔案

$ go get github.com/randall77/hprof/dumptodot
$ dumptodot heapdump mybinary > heap.dot

並使用 Graphviz 開啟 heap.dot

若要將它轉換為 hprof 格式

$ go get github.com/randall77/hprof/dumptohprof
$ dumptohprof heapdump heap.hprof
$ jhat heap.hprof

並將您的瀏覽器導航到 http://myserver:7000。

結論

最佳化是一個開放性的問題,有一些簡單的秘訣可讓您提升效能。有時最佳化需要對程式進行完整的重新建構。但我們希望這些工具能成為您工具箱中寶貴的補充,讓您至少可以分析和瞭解發生了什麼事。 剖析 Go 程式 是關於使用 CPU 和記憶體剖析器來最佳化簡單程式的良好教學。


此內容是 Go Wiki 的一部分。