這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
讓我們假設你有一golang 程式,想改善其效能。有幾種工具可以幫我們完成這個任務。這些工具可以幫我們識別程式中的熱點(cpu,io,memory), 熱點即是那些需要我們集中精力於其上,能顯著改善改善效能的地方。然而,另外一種結果也是可能的,工具幫我們識別出程式裡的多種效能缺陷。比如,每次查詢資料庫,你都準備sql 語句,然而,你可以在程式啟動時,只準備一次。另一個例子,一個O(n^2)的演算法莫名其妙的溜進,某些存在O(n) 演算法的地方。為了識別出這些情況,你需要合理檢查程式剖析所看到的結果。比如第一個例子中,有顯著的時間花費在sql 語句準備階段,這就是一個危險的訊號。
瞭解多種關於效能的邊界因素也是重要的。比如,你的程式使用100Mbps網路鏈路通訊,它已經使用了鏈路90Mbps以上的頻寬,那你的程式就沒有太多效能改善空間。對於磁碟io,記憶體消耗,計算性任務也存在類似的情況。這些情況,銘記在心,接著我們可以查看可用的工具。
注意:工具之間會相互幹擾,比如,精準的記憶體剖析會偏差cpu剖析,goroutine阻塞剖析影響調度器的追蹤等,隔離地使用工具能夠獲得更精確的資訊。以下闡述基於golang 1.3。
cpu 剖析器
go運行時內建cpu profiler,它可以顯示哪些函數消耗了多少的百分比cpu時間,有三種方式可以訪問它:
1.最簡單的是go test 命令-cpuprofile標誌,例如,以下命令:
$ go test -run=none -bench=ClientServerParallel4 -cpuprofile=cprof net/http
剖析給定的benchmark,把cpu profile 資訊寫入‘cprof' 檔案
接著:
$ 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 = 64var m [partCount]Partitionfunc Find(k string) string { idx := hash(k) % partCount part := &m[idx] part.RLock() v := part.m[k] part.RUnlock() return v}
5. 局部緩衝和批次更新可以協助減少不可分區的資料結構上爭用。
const CacheSize = 16type 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 線程,它們停靠在非阻塞的網路輪詢者上。