讓咱們假設你有一golang 程序,想改善其性能。有幾種工具能夠幫咱們完成這個任務。這些工具能夠幫咱們識別程序中的熱點(cpu,io,memory), 熱點便是那些須要咱們集中精力於其上,能顯著改善改善性能的地方。然而,另一種結果也是可能的,工具幫咱們識別出程序裏的多種性能缺陷。好比,每次查詢數據庫,你都準備sql 語句,然而,你能夠在程序啓動時,只准備一次。另外一個例子,一個O(n^2)的算法莫名其妙的溜進,某些存在O(n) 算法的地方。爲了識別出這些狀況,你須要合理檢查程序剖析所看到的結果。好比第一個例子中,有顯著的時間花費在sql 語句準備階段,這就是一個危險的信號。php
瞭解多種關於性能的邊界因素也是重要的。好比,你的程序使用100Mbps網絡鏈路通訊,它已經使用了鏈路90Mbps以上的帶寬,那你的程序就沒有太多性能改善空間。對於磁盤io,內存消耗,計算性任務也存在相似的狀況。這些狀況,銘記在心,接着咱們能夠查看可用的工具。
java
注意:工具之間會相互干擾,好比,精準的內存剖析會誤差cpu剖析,goroutine阻塞剖析影響調度器的追蹤等,隔離地使用工具可以得到更精確的信息。如下闡述基於golang 1.3。linux
cpu 剖析器c++
go運行時內置cpu profiler,它能夠顯示哪些函數消耗了多少的百分比cpu時間,有三種方式能夠訪問它:golang
1.最簡單的是go test 命令-cpuprofile標誌,例如,如下命令:
web
$ go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprof net/http算法
剖析給定的benchmark,把cpu profile 信息寫入‘cprof' 文件
sql
接着:
數據庫
$ go tool pprof --text http.test cprof
數組
打印最熱點的函數列表,有幾種可用輸出格式,最有用的幾個:--text,--web,--list.運行 'go tool pprof' 獲取完整列表
2.net/http/pprof 包,這個方案對於網絡服務應用十分理想,你僅需導入net/http/pprof,就可以使用下面命令profile:
go tool pprof --text mybin http://myserver:6060:/debug/pprof/profile
3.手動profile 採集,須要引入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 選項查閱單個函數,列如如下profile 顯示時間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: }
當剖析器不能解開調用棧時,使用三個特殊條目使用:GC,System,ExternalCode。GC 表示花費在垃圾回收的時間;System表示花費在goroutine 調度器,棧管理及其餘輔助運行時代碼的時間;ExternalCode,表示花費在調用本地動態庫時間。這裏有一些關於如何解釋profile結果的提示:
若是你看到大量時間花費在runtime.mallocgc 函數,程序可能作了過多的小內存分配。profile能告訴你這些分配來自哪裏。
若是大量時間花費在channel,sync.Mutex,其餘的同步原語,或者系統組件,程序可能遭受資源爭用。考慮如下重新組織代碼結構,消除最常訪問的共享資源。經常使用的技術包括分片/分區, 局部緩衝/彙集, 寫時拷貝等。
若是大量時間花費在syscall.Read/Write, 程序可能產生了太多小數據量讀寫操做,可考慮使用bufio 包裹os.File or net.Conn。
若是大量時間花費在GC 組件,要麼是程序分配了太多的臨時對象,要麼是堆內存過小,致使垃圾收集操做運行頻繁。
注意:在當前的darwin 平臺,cpu profiler 不能正確工做 darwin 不能正常工做;window 平臺須要安裝cygwin,perl,graphviz,用來生成svg/web profile;在linux平臺,你也可以使用perf system profiler,它不能解開go stack,但可剖析解開cgo/swig 代碼和內核代碼。
memory profiler
內存剖析器顯示哪些函數分配堆內存,你能夠採集它,使用’go test --memprofile', 或者net/http/ppro經由 http://myserver:6060:/debug/pprof/heap 或者調用 runtime/pprof.WriteHeapProfile。
你能夠僅僅可視化profile 收集過程當中的活躍分配(傳遞 --inuse_space 標誌,默認),或者自程序啓動以來的全部分配(--alloc_space )。
你能夠顯示分配了多少字節或者多少個對象(--inuse/alloc_space or --inuse/alloc_objects )。屢次剖析過程當中,profiler 趨向於採樣更大的對象。瞭解大對象影響內存消耗和gc 時間,大量小分配影響執行速度也在某種程度影響gc時間。對象能夠是持久或臨時的生命週期。若是在程序啓動時,你有幾個大的持久對象分配,它們極可能被採集到。這些對象影響內存消耗和gc時間,但不影響正常的執行速度。另外一方面,若是你有大量短生命週期對象,那麼profile過程當中,它們幾乎不能被呈現。但它們對執行速度有顯著影響,由於它們被分配釋放很頻繁。
通常狀況是,若是你要減小內存消耗,考慮使用--inuse_space 選項的profile;若是是想要改善執行速度,使用--alloc_objects 選項profile。有幾個選項能夠用來控制報告的粒度,--function函數級別(默認),--lines,--files,--adrresses,行級,文件級,指令地址級。
優化一般是應用特定的,如下是一些經常使用建議:
1.合併對象進入更大的對象。例如,使用bytes.Buffer替代*bytes.Buffer,做爲結構體成員,這個能減小內存分配次數,減輕gc壓力。
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.一個合併分配的特例,若是你瞭解使用中slice典型大小,slice的底層數組可使用預分配。
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.那些不包含任何指針的對象(注意:string,slice,map,chan 包含隱式指針),不會被垃圾收集器掃描。好比 1Gb byte slice 不會影響gc time,從活躍使用的對象中移除指針,能對gc time 產生正面影響。一些可能方式:使用索引替代指針,分割對象成兩部分,其中一部分沒有任何指針。
6.使用freelist 重用臨時對象,減小分配次數。標準庫包含的sync.Pool 類型容許在gc 之間屢次重用同一個對象。不過要意識到,就像任何手動內存管理模式同樣,不正確地使用sync.Pool 能夠致使 use-after-free bug。
Blocking Profile
goroutine 阻塞 profiler展現goroutine 阻塞等待同步原語(包括定時器channel),出如今代碼中哪些地方。你可使用‘go test --blockprofile', net/http/pprof 經由 http://myserver:6060:/debug/pprof/block,或者調用 runtime/pprof.Lookup("block").WriteTo
阻塞剖析器默認沒有被開啓,’go test --blockprofile‘ 會爲你自動開啓,可是使用net/http/pprof, runtime/pprof,須要你手動開啓。調用runtime.SetBlockProfileRate,開啓阻塞剖析器,SetBlockProfileRate 控制blocking profile 中阻塞時間的報告粒度。
若是一個函數包含多個阻塞操做,瞭解到那個操做致使阻塞,就變得不太明晰,如此可使用--lines 標誌辨別。
注意並非全部的阻塞都是很差的。當一個goroutine阻塞,底層工做線程會切換到另外一個goroutine。這樣以來,協做的go環境的阻塞與非協做系統互斥器上的阻塞,就有着顯著差別。(c++, java 線程庫裏,阻塞會致使線程空閒,和昂貴的線程上下文切換)
在time.Ticker 上阻塞一般沒什麼問題。若是一個goroutine在一個ticker上阻塞10 s,阻塞剖析中也會看到10s阻塞。在sync.WaitGroup 上阻塞,大多也沒什麼問題。例如,一個任務花費10s,goroutine在waitgroup等待,記帳10s。在sync.Cond 上阻塞是好是壞,取決於具體狀況。消費者阻塞在channel 上,暗示生產者的慢速,或者缺乏能夠作的工做。生產者阻塞在channel 上,暗示消費者的慢速,一般這也不是個問題。阻塞於channel基於的信號量,顯示有多少goroutine被卡在信號量上。阻塞於sync.Mutex, sync,RWMutex, 通常不太好。你可使用--ignore 排除那些不感興趣的阻塞事件。
goroutine的阻塞產生兩種消極後果:
1. 程序不能與處理器線性比例伸縮。
2. 過多的goroutine阻塞與解阻塞,消耗太多cpu時間。
如下是一些提示幫助減小goroutine阻塞:
1. 在匹配生產者,消費者模型代碼中,使用充足緩衝的 buffer channel,無緩衝channel實質上限制了程序的並行度。
2. 在有不少讀取操做, 不多修改數據操做的場景,使用sync.RWMutex 代替 sync.Mutex。讀者之間不會相互阻塞。
3. 某些場景甚至能夠經過使用寫時拷貝技術徹底移除mutex。若是被保護的數據結構修改的 不太頻繁,製造一份拷貝是可行的:
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 } }
這個技術並不限於channel上,能夠用在批量更新map, 批量分配內存。
6. 使用sync.Pool 的freelist ,而非基於channel,或mutex 保護的 freelist。sync.Pool 內部用了一些聰明技術減小阻塞。
Goroutine Profiler
goroutine 剖析器僅是讓你看到進程全部活躍goroutine的當前堆棧,這個對於調試負載均衡及死鎖問題,十分方便。
goroutine profile 僅對運行中的應用程序,顯得合理,因此go test 命令作不到這點。你可使用 net/http/pprof 經由 http://myserver:6060:/debug/pprof/goroutine 或者調用 runtime/pprof.Lookup("goroutine").WriteTo 。可是最有用的方法是在瀏覽器裏鍵入 http://myserver:6060:/debug/pprof/goroutine?debug=2, 你會看到相似程序崩潰時的堆棧追蹤。注意顯示 「syscall" 狀態的goroutine 消費os 線程,其餘goroutine不會。」io wait「 狀態的goroutine 也不消費os 線程,它們停靠在非阻塞的網絡輪詢器上。