https://software.intel.com/en-us/blogs/2014/05/10/debugging-performance-issues-in-go-programsphp
https://studygolang.com/articles/2729html
假設你手上有個Go語言編寫的程序,你打算提高它的性能。目前有一些工具能夠爲此提供幫助。這些工具能幫你發現包括CPU、IO和內存在內多種類型的熱點。所謂熱點,是指那些爲了能顯著提高性能而值得你去關注的地方。有時候這些工具還能幫助你發現程序中主要的性能瑕疵。舉個例子,你不必每次執行SQL查詢前都對SQL語句進行參數化解析,你能夠將這個準備過程在程序啓動時一次完成。再舉個例子,當前某個算法的複雜度是O(N²),但其實存在一個複雜度是O(N)的解決方案。爲了能發現這些問題,須要理智地檢查你在優化分析器中獲取到的信息。好比上面提到的第一個問題,你會注意到至關長的時間被花費在了對SQL語句的準備上。linux
瞭解針對性能的不一樣邊界因素也是比較重要的。比方說,若是一個程序使用100 Mbps帶寬的網絡進行通訊,而目前已經佔用了超過90 Mbps的帶寬,爲了提高它的性能,你拿這樣的程序也沒啥辦法了。在磁盤IO、內存消耗和計算密集型任務方面,也有相似的邊界因素。git
將這點牢記在心,讓咱們看看有哪些工具能夠用。github
注意:這些工具會彼此互相影響。例如,對內存使用優化分析器會致使針對CPU的優化分析器產生偏差,對goroutine阻塞使用優化分析器會影響調度器跟蹤等等。爲了得到更加精確的信息,請在隔離的環境中使用這些工具。 golang
注意:本文描述的用法基於Go語言發佈的1.3版。web
Go 運行時包含了內建的CPU分析器,它用來展現某個函數耗費了多少CPU百分時間。這裏有三種方式來使用它: 算法
1. 最簡單的是用"go test"的-cpuprofile選項。例以下面的命令: shell
$ go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprof net/http windows
將會分析所給的基準並將結果寫入"cprof"文件中。
而後:
$ go tool pprof --text http.test cprof
將會打印耗費最多CPU時間的函數列表。
這裏有幾種可用的輸出形式,最實用的有 --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 選項來實現可視化的例子:
你可使用--list=funcname來審查單一函數。例如,下面的結果顯示了附加函數中的時間流逝:
. . 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: }
你能夠在這裏找到pprof工具的詳細信息以及上圖中數字的描述。 在3種特殊的情形下分析器不能解開堆棧:GC,System和ExternalCode。GC 表示垃圾回收期間的用時,查看下面的內存分析器和垃圾回收跟蹤器以獲得優化建議。System 表示goroutine調度程序,棧管理代碼和其餘輔助運行時代碼的用時。ExternalCode 表示本地動態庫耗時。 |
![]() Micooz
|
對於你在簡介中看到的東西的解釋,這裏有一些提示和技巧。
若是你看到大量的時間消耗在運行時間,內存分配的函數,那麼暗示程序產生了大量過分的小內存分配工做。此描述將會告訴你這些分配來自哪裏。查看內存分析器部分能夠得到如何優化這種狀況的建議。可考慮對程序進行從新調整以消除頻繁對共享資源的獲取和接入。對此,通常的解決技術方案包括有分片/分區,本地緩存/計量和寫時拷貝。
若是大量的時間消耗在頻道操做,同步。互斥代碼和其餘同步原語或者系統容器,那麼程序極可能正在飽受資源爭奪的痛苦。
若是大量的時間消耗在系統調用的讀/寫,那麼暗示程序產生了大量過分的小塊讀寫。對這種狀況,圍繞系統文件或者網絡鏈接而包裝的Bufio會頗有幫助。
若是大量的時間消耗在GC容器,那麼程序要麼分配了大量的短暫臨時的對象,要麼堆棧的空間很是小以至垃圾回收收集變得很是頻繁。經過查看垃圾收集追蹤器和內存分析器這兩部分能夠獲得一些優化的建議。
舒適提示:當前CPU分析器不能工做於darwin。
舒適提示:在windows系統上你須要安裝Cygwin, Perl和Graphviz才能生成svg/web簡介。
舒適提示:在linux系統上你也能夠嘗試PERF系統分析器。它不能解開Go的棧,但它能夠得到cgo或者SWIG的代碼和kernel內核的快照並解開。因此它對於洞悉本地/kernel內核的性能瓶頸很是有幫助。
內存分析器展現了哪些函數申請了堆內存。你能夠經過熟悉的途徑來收集這些信息,一如使用CPU分析器:和 'go test --memprofile', 以及經過 http://myserver:6060:/debug/pprof/heap的net/http/pprof 或者經過調用runtime/pprof.WriteHeapProfile。
你僅僅能夠可視化描述收集器當前時間內的申請(默認下--inuse_space標識指向pprof),或者自程序啓動以來所有的申請(--alloc_space標識指向pprof)。前者對於在當前活動的程序經過net/http/pprof收集描述頗有幫助,然後者則對在程序後端(不然你將會看到的幾乎都是空的描述)收集描述有幫助。
舒適提示:內存分析器採起抽樣的方式,也就是說,它僅僅從一些內存分配的子集中收集信息。有可能對一個對象的採樣與被採樣對象的大小成比例。你能夠經過使用go test --memprofilerate標識,或者經過程序啓動時 的運行配置中的MemProfileRate變量來改變調整這個採樣的比例。若是比例爲1,則會致使所有申請的信息都會被收集,可是這樣的話將會使得執行變慢。默認的採樣比例是每512KB的內存申請就採樣一次。
你一樣能夠將分配的字節數或者分配的對象數形象化(分別是以--inuse/alloc_space和--inuse/alloc_objects爲標誌)。分析器傾向於在性能分析中對較大的對象採樣。可是須要注意的是大的對象會影響內存消耗和垃圾回收時間,大量的小的內存分配會影響運行速度(某種程度上也會影響垃圾回收時間)。因此最好同時考慮它們。
對象能夠是持續的也能夠是瞬時的。若是你在程序開始的時候須要分配幾個大的持續對象,它們頗有可能能被分析器取樣(由於它們比較大)這些對象會影響內存消耗量和垃圾回收時間,但它們不會影響正常的運行速度(在它們上沒有內存管理操做)。另外一方面,若是你有大量持續期很短的對象,它們幾乎不會表如今曲線中(若是你使用默認的--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。緩衝區 (你能夠預分配而後經過調用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. 不包含任何指針的對象(注意 strings,slices,maps 和 chans 包含隱含指針)不會被垃圾回收器掃描到。好比,1GB 的分片實際上不會影響垃圾回收時間。所以若是你刪除被頻繁使用的對象指針,它會對垃圾回收時間形成影響。一些建議:使用索引替換指針,將對象分割爲其中之一不含指針的兩部分。
6. 使用釋放列表來重用臨時對象,減小內存分配。標準庫包含的 sync.Pool 類型能夠實現垃圾回收期間屢次重用同一個對象。然而須要注意的是,對於任何手動內存管理的方案來講,不正確地使用sync.Pool 會致使 use-after-free bug。
你也可使用Garbage Collector Trace(見後文)來獲取更深層次的內存問題。
阻塞分析器展現了goroutine在等待同步原語(包括計時器通道)被阻塞的位置。你能夠用相似CPU分析器的方法來收集這些信息:經過'go test --blockprofile', net/http/pprof(經由h ttp://myserver:6060:/debug/pprof/block) 或者調用 runtime/pprof.Lookup("block").WriteTo。
值得警示的是,阻塞分析器默認未激活。'go test --blockprofile' 將爲你自動激活它。然而,若是你使用net/http/pprof 或者 runtime/pprof,你就須要手動激活它(不然分析器將不會被載入)。經過調用 runtime.SetBlockProfileRate 來激活阻塞分析器。SetBlockProfileRate 控制着由阻塞分析器報告的goroutine阻塞事件的比率。分析器力求採樣出每指定微秒數內,一個阻塞事件的阻塞平均數。要使分析器記錄每一個阻塞事件,將比率設爲1。
若是一個函數包含了幾個阻塞操做並且並無哪個明顯地佔有阻塞優點,那就在pprof中使用--lines標誌。
注意:並不是全部的阻塞都是不利的。當一個goroutine阻塞時,底層的工做線程就會簡單地轉換到另外一個goroutine。因此Go並行環境下的阻塞 與非並行環境下的mutex的阻塞是有很大不一樣的(例如典型的C++或Java線程庫,當發生阻塞時會引發線程空載和高昂的線程切換)。爲了讓你感覺一 下,咱們來看幾個例子。
在 time.Ticker上發生的阻塞一般是可行的,若是一個goroutine阻塞Ticker超過十秒,你將會在profile中看到有十秒的阻塞,這 是很好的。發生在sync.WaitGroup上的阻塞常常也是能夠的,例如,一個任務須要十秒,等待WaitGroup完成的goroutine會在 profile中生成十秒的阻塞。發生在sync.Cond上的阻塞可好可壞,取決於狀況不一樣。消費者在通道阻塞代表生產者緩慢或不工做。生產者在通道阻塞,代表消費者緩慢,但這一般也是能夠的。在基於通道的信號量發生阻塞,代表了限制在這個信號量上的goroutine的數量。發生在sync.Mutex或sync.RWMutex上的阻塞一般是不利的。你能夠在可視化過程當中,在pprof中使用--ignore標誌來從profile中排除已知的無關阻塞。
goroutine的阻塞會致使兩個消極的後果:
程序與處理器之間不成比例,緣由是缺少工做。調度器追蹤工具能夠幫助肯定這種狀況。
過多的goroutine阻塞/解除阻塞消耗了CPU時間。CPU分析器能夠幫助肯定這種狀況(在系統組件中找)。
這裏是一些一般的建議,能夠幫助減小goroutine阻塞:
在生產者--消費者情景中使用充足的緩衝通道。無緩衝的通道實際上限制了程序的併發可用性。
針對於主要爲讀取的工做量,使用sync.RWMutex而不是sync.Mutex。由於讀取操做在sync.RWMutex中歷來不會阻塞其它的讀取操做。甚至是在實施級別。
在某些狀況下,能夠經過使用copy-on-write技術來徹底移除互斥。若是受保護的數據結構不多被修改,能夠爲它製做一份副本,而後就能夠這樣更新它:
type Config struct { Routes map[string]net.Addr Backends []net.Addr } var config unsafe.Pointer // actual type is *Config
// Worker goroutines use this function to obtain the current config. func CurrentConfig() *Config { return (*Config)(atomic.LoadPointer(&config)) } // Background goroutine periodically creates a new Config object // as sets it as current using this function. func UpdateConfig(cfg *Config) { atomic.StorePointer(&config, unsafe.Pointer(cfg)) }
這種模式能夠防止在更新時阻塞的讀取對它的寫入。
4. 分割是另外一種用於減小共享可變數據結構競爭和阻塞的通用技術。下面是一個展現如何分割哈希表(hashmap)的例子:
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 } }
這種技術並不只限於通道,它還能用於批量更新映射(map)、批量分配等等。
6. 針對freelists,使用sync.Pool代替基於通道的或互斥保護的freelists,由於sync.Pool內部使用智能技術來減小阻塞。
Go協程分析器簡單地提供給你當前進程中全部活躍的Go協程堆棧。它能夠方便地調試負載平衡問題(參考下面的調度器追蹤章節),或調試死鎖。
這個配置僅僅對運行的程序有意義,因此去測試而不是揭露它. 你能夠用net/http/pprof經過http://myserver:6060:/debug/pprof/goroutine來收集配置,並將之形象化爲svg/pdf或經過調用runtime/pprof.Lookup("goroutine").WriteTo形象化。但最有用的方式是在你的瀏覽器中鍵入http://myserver:6060:/debug/pprof/goroutine?debug=2,它將會給出與程序崩潰時相同的符號化的堆棧。
須要注意的是:Go協程「syscall」將會消耗一個OS線程,而其餘的Go協程則不會(除了名爲runtime.LockOSThread的Go協程,不幸的是,它在配置中是不可見的)。一樣須要注意的是在「IO wait」狀態的Go協程一樣不會消耗線程,他們停駐在非阻塞的網絡輪詢器(一般稍後使用epoll/kqueue/GetQueuedCompletionStatus來喚醒Go協程)。
除了性能分析工具之外,還有另外幾種工具可用——追蹤器。它們能夠追蹤垃圾回收,內存分配和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的編號(這是從程序開始後的第九個GC),在括號中的數字("(2)")是參與GC的工做線程的編號。隨後的四個數字("12+1+744+8 us")分別是工做線程完成GC的stop-the-world, sweeping, marking和waiting時間,單位是微秒。接下來的兩個數字("2 -> 10 MB")表示前一個GC事後的存活堆大小和當前GC開始前完整的堆(包括垃圾)的大小。再接下來的三個數字 ("108615 (593983-485368) objects")是堆中的對象總數(包括垃圾)和和分配的內存總數以及空閒內存總數。後面的三個數字("4825/3620/0 sweeps")表示清理階段(對於前一個GC):總共有4825個存儲器容量,3620當即或在後臺清除,0個在stop-the-world階段清除(剩餘的是沒有使用的容量)。再後面的四個數字("0(0) handoff, 6(91) steal")表示在平行的標誌階段的負載平衡:0個切換操做(0個對象被切換)和六個steal 操做(91個對象被竊取)最後的三個數字("16/1/0 yields")表示平行標誌階段的係數:在等候其它線程的過程當中共有十七個yield操做。
GC 是 mark-and-sweep 類型。總的 GC 能夠表示成:
Tgc = Tseq + Tmark + Tsweep
這裏的 Tseq 是中止用戶的 goroutine 和作一些準備活動(一般很小)須要的時間;Tmark 是堆標記時間,標記發生在全部用戶 goroutine 中止時,所以能夠顯著地影響處理的延遲;Tsweep 是堆清除時間,清除一般與正常的程序運行同時發生,因此對延遲來講是不太關鍵的。
標記時間大概能夠表示成:
Tmark = C1*Nlive + C2*MEMlive_ptr + C3*Nlive_ptr
這裏的 Nlive 是垃圾回收過程當中堆中的活動對象的數量,MEMlive_ptr 是帶有指針的活動對象佔據的內存總量,Nlive_ptr 是活動對象中的指針數量。
清除時間大概能夠表示成:
Tsweep = C4*MEMtotal + C5*MEMgarbage
這裏的 MEMtotal 是堆內存的總量,MEMgarbage 是堆中的垃圾總量。
下一次垃圾回收發生在程序被分配了一塊與其當前所用內存成比例的額外內存時。這個比例一般是由 GOGC 的環境變量(默認值是100)控制的。若是 GOGC=100,並且程序使用了 4M 堆內存,當程序使用達到 8M 時,運行時(runtime)就會再次觸發垃圾回收器。這使垃圾回收的消耗與分配的消耗保持線性比例。調整 GOGC,會改變線性常數和使用的額外內存的總量。
只有清除是依賴於堆總量的,且清除與正常的程序運行同時發生。若是你能夠承受額外的內存開銷,設置 GOGC 到以一個較高的值(200, 300, 500,等)是有意義的。例如,GOGC=300 能夠在延遲相同的狀況下減少垃圾回收開銷高達原來的二分之一(但會佔用兩倍大的堆)。
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
...
跟蹤信息包括內存塊地址、大小、類型、執行程序ID和堆棧蹤影。它可能更有助於調試,但也能夠給內存分配優化提供很是詳細的信息。
調度器追蹤能夠提供對 goroutine 調度的動態行爲的內視,而且容許調試負載平衡和可擴展性問題。要啓用調度器追蹤,能夠帶有環境變量 GODEBUG=schedtrace=1000 來運行程序(這個值的意思是輸入的週期,單位 ms,這種狀況下是每秒一次):
$ 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]
注意:你能夠隨意組合追蹤器,如: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 函數提供粗糙的內存統計。這個統計一樣能夠經過 http://myserver:6060/debug/pprof/heap?debug=1 底部的net/http/pprof提供。統計資料,點擊此處。
一些值得關注的地方是:
1. HeapAlloc - 當前堆大小。
2. HeapSys - 總的堆大小。
3. HeapObjects - 堆中對象的總數。
4. HeapReleased - 釋放到操做系統中的內存;若是內存超過五分鐘沒有使用,運行時將會把它釋放到操做系統中,你能夠經過 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函數編寫傾卸器(dump):
f, err := os.Create("heapdump") if err != nil { ... } debug.WriteHeapDump(f.Fd())
而後,你既能夠將堆以圖形化的表現形式保存爲.dot文件,也能夠將它轉換爲hprof格式。爲了將它保存爲.dot文件,你須要執行如下指令:
1
2
|
$ go get github.com
/randall77/hprof/dumptodot
$ dumptodot heapdump mybinary > heap.dot
|
最後,使用Graphviz工具打開heap.dot文件。
爲了將堆轉換成hprof格式,須要執行如下指令:
1
2
3
|
$ go get github.com
/randall77/hprof/dumptohprof
$ dumptohprof heapdump heap.hprof
$ jhat heap.hprof
|
優化是一個開放的問題,你可使用不少簡單的方法來提升性能。然而,有時優化須要對程序進行完整地從新架構。但咱們但願這些工具可以成爲你工具箱中一個有價值的新增成員,至少你可使用它們分析並理解到底發生了什麼。
《剖析Go程序》是一個很好的教程,它講解了如何利用CPU和內存分析器來優化簡單的程序。