golang 核心開發人員 Dmitry Vyukov(1.1 調度器作者) 關於效能剖析

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

讓我們假設你有一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 線程,它們停靠在非阻塞的網路輪詢者上。 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.