這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
許多人在剛開始接觸 Go 語言時,經常會有的疑惑就是“為什麼一個 Hello world 會佔用如此之多的記憶體?”。Understanding Go Lang Memory Usage 很好的解釋了這個問題。不過“簡介”就是“簡介”,更加深入的內容恐怕要讀者自己去探索了。另外,文章寫到最後,作者飄了,估計引起了一些公憤,於是又自己給自己補刀,左一刀,右一刀……
————翻譯分隔線————
理解 Go 語言的記憶體使用量
2014年12月22日,星期一
溫馨提示:這僅是關於 Go 語言記憶體的簡介,俗話說不入虎穴、焉得虎子,讀者可以進行更加深入的探索。
大多數 Go 開發人員都會嘗試像這樣簡單的 hello world 程式:
package mainimport ("fmt""time")func main() {fmt.Println("hi")time.Sleep(30 * time.Second)}
然後他們就完全崩潰了。
f!*%!$%@# 138 G?!這個筆記本也只有 16 G 記憶體!
虛擬記憶體 vs 常駐記憶體
Go 管理記憶體的方式可能與你以前使用的方式不太一樣。它會在一開始就保留一大塊 VIRT,而 RSS 與實際記憶體用量接近。
RSS 和 VIRT 之間有什麼區別呢?
VIRT 或者虛擬位址空間大小是程式映射並可以訪問的記憶體數量。
RSS 或者常駐大小是實際使用的記憶體數量。
如果你對 Go 到底是怎麼實現的感興趣,來看看這個:
https://github.com/golang/go/blob/master/src/runtime/malloc1.go
// 在 64 位元裝置中,從單一的連續保留地址中分配。 // 當前來說 128 GB (MaxMem) 應當是足夠了。 // 實際上我們保留了 136 GB(因為最終位映射會使用 8 GB)
務必注意,如果你使用 32 位的架構,記憶體保留機制是完全不同的。
垃圾收集
現在我們已經清楚了常駐記憶體和共用記憶體的區別,可以來談談 Go 進行垃圾收集的機制,以便瞭解我們的程式是如何工作的。
設想你正在編寫一個長期啟動並執行後台服務,就讓它是一個 web 應用服務或者某些更複雜的東西。通常來說,在整個運行周期都會需要分配記憶體。瞭解如何處理這些記憶體是必要的。
通常,每 2 分鐘會執行一次垃圾收集。如果某個片段持續 5 分鐘都沒有被使用,回收器會將其釋放。
因此,如果你認為記憶體使用量會降低,那麼 7 分鐘之後再去確認吧。
需要注意的是,當前 gc 是非壓縮的,也就是說如果你在某個頁面有一個位元組正在使用,回收器會拒絕釋放這個頁面。
最後,也是最重要的,Go 1.3 的 goroutine 棧有 8k/pop 的空間不會被釋放,它們隨後會被重用。不用擔心,Go 在 GC 的部分還有很大的改進空間。因此,如果你的代碼會產生大量的 goroutine,並且 RES 居高不下的話,這可能就是原因。
好了,現在我們已經知道了程式從外部看到的樣子以及期望 GC 做的工作。
分析記憶體使用量
現在通過一個小例子來看看如何瞭解記憶體使用量。在這個例子中,我們將分配 10 組 100 MB的記憶體。
然後會用多種方式來瞭解記憶體的使用。
一個方法是通過 runtime 包的 ReadMemStats 函數。
另一個方法是通過 pprof 包提供的 web 介面。這允許我們遠程獲得程式的 pprof 資料,稍候會詳細解釋。
還有一種方法是我們必須介紹的是 Dave Cheney 提到的,使用 gctrace 調試環境變數。
注意:這些都在 64 位元 Linux 下的 Go 1.4 環境下完成。
package mainimport ( "log" "net/http" _ "net/http/pprof" "runtime" "sync")func bigBytes() *[]byte { s := make([]byte, 100000000) return &s}func main() { var wg sync.WaitGroup go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() var mem runtime.MemStats runtime.ReadMemStats(&mem) log.Println(mem.Alloc) log.Println(mem.TotalAlloc) log.Println(mem.HeapAlloc) log.Println(mem.HeapSys) for i := 0; i < 10; i++ { s := bigBytes() if s == nil { log.Println("oh noes") } } runtime.ReadMemStats(&mem) log.Println(mem.Alloc) log.Println(mem.TotalAlloc) log.Println(mem.HeapAlloc) log.Println(mem.HeapSys) wg.Add(1) wg.Wait()}
在使用 pprof 查看記憶體的時候,通常會用到兩個選項。
一個選項是“-alloc_space”,用於告訴你已經分配了多少記憶體。
另一個是“-inuse_space”,用於獲得正在使用的記憶體的數量。
可以運行 pprof 並將其指向我們內建的 web 服務來獲得最高的記憶體消耗。
並且還可以使用 list 來瞭解那裡使用了這些記憶體:
使用
vagrant@vagrant-ubuntu-raring-64:~/blahdo$ go tool pprof -inuse_spaceblahdo http://localhost:6060/debug/pprof/heapFetching profile from http://localhost:6060/debug/pprof/heapSaved profile in/home/vagrant/pprof/pprof.blahdo.localhost:6060.inuse_objects.inuse_space.025.pb.gzEntering interactive mode (type "help" for commands)(pprof) top5190.75MB of 191.25MB total (99.74%)Dropped 3 nodes (cum <= 0.96MB) flat flat% sum% cum cum% 190.75MB 99.74% 99.74% 190.75MB 99.74% main.main 0 0% 99.74% 190.75MB 99.74% runtime.goexit 0 0% 99.74% 190.75MB 99.74% runtime.main(pprof) quit
分配
vagrant@vagrant-ubuntu-raring-64:~/blahdo$ go tool pprof -alloc_spaceblahdo http://localhost:6060/debug/pprof/heapFetching profile from http://localhost:6060/debug/pprof/heapSaved profile in/home/vagrant/pprof/pprof.blahdo.localhost:6060.alloc_objects.alloc_space.027.pb.gzEntering interactive mode (type "help" for commands)(pprof) top5572.25MB of 572.75MB total (99.91%)Dropped 3 nodes (cum <= 2.86MB) flat flat% sum% cum cum% 572.25MB 99.91% 99.91% 572.25MB 99.91% main.main 0 0% 99.91% 572.25MB 99.91% runtime.goexit 0 0% 99.91% 572.25MB 99.91% runtime.main
熱門排行榜已經相當不錯了,不過更好的是 list 命令,可以在上下文中看到消耗是如何影響程式的其他部分的。
(pprof) listTotal: 572.75MBROUTINE ======================== main.main in/home/vagrant/blahdo/main.go 572.25MB 572.25MB (flat, cum) 99.91% of Total . . 23: var mem runtime.MemStats . . 24: runtime.ReadMemStats(&mem) . . 25: log.Println(mem.Alloc) . . 26: . . 27: for i := 0; i < 10; i++ { 572.25MB 572.25MB 28: s := bigBytes() . . 29: if s == nil { . . 30: log.Println("oh noes") . . 31: } . . 32: } . . 33:
聰明的讀者可能已經發現在上面的記憶體使用量報告中,存在一些差異。為什麼會這樣呢?
讓我們來看看進程:
vagrant@vagrant-ubuntu-raring-64:~$ ps aux | grep blahdovagrant 4817 0.2 10.7 699732 330524 pts/1 Sl+ 00:13 0:00 ./blahdo
現在來看看日誌輸出:
./vagrant@vagrant-ubuntu-raring-64:~/blahdo$ ./blahdo2014/12/23 00:19:37 2796722014/12/23 00:19:37 3361522014/12/23 00:19:37 2796722014/12/23 00:19:37 8192002014/12/23 00:19:37 3002099202014/12/23 00:19:37 10004209682014/12/23 00:19:37 3002099202014/12/23 00:19:37 500776960
最後,來看看使用 gctrace 的效果:
vagrant@vagrant-ubuntu-raring-64:~/blahdo$ GODEBUG=gctrace=1 ./blahdogc1(1): 1+0+95+0 us, 0 -> 0 MB, 21 (21-0) objects, 2 goroutines, 15/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yieldsgc2(1): 0+0+81+0 us, 0 -> 0 MB, 52 (53-1) objects, 3 goroutines, 20/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yieldsgc3(1): 0+0+77+0 us, 0 -> 0 MB, 151 (169-18) objects, 4 goroutines, 25/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yieldsgc4(1): 0+0+110+0 us, 0 -> 0 MB, 325 (393-68) objects, 4 goroutines, 33/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yieldsgc5(1): 0+0+138+0 us, 0 -> 0 MB, 351 (458-107) objects, 4 goroutines, 40/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields2014/12/23 02:27:14 2779602014/12/23 02:27:14 3326802014/12/23 02:27:14 2779602014/12/23 02:27:14 884736gc6(1): 1+0+181+0 us, 0 -> 95 MB, 599 (757-158) objects, 6 goroutines, 52/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yieldsgc7(1): 1+0+454+19 us, 95 -> 286 MB, 438 (759-321) objects, 6 goroutines, 52/0/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yieldsgc8(1): 1+0+167+0 us, 190 -> 477 MB, 440 (762-322) objects, 6 goroutines, 54/1/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yieldsgc9(1): 2+0+191+0 us, 190 -> 477 MB, 440 (765-325) objects, 6 goroutines, 54/1/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields2014/12/23 02:27:14 3002068642014/12/23 02:27:14 10004170402014/12/23 02:27:14 3002068642014/12/23 02:27:14 500842496GC forcedgc10(1): 3+0+1120+22 us, 190 -> 286 MB, 455 (789-334) objects, 6 goroutines, 54/31/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yieldsscvg0: inuse: 96, idle: 381, sys: 477, released: 0, consumed: 477 (MB)GC forcedgc11(1): 2+0+270+0 us, 95 -> 95 MB, 438 (789-351) objects, 6 goroutines, 54/39/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yieldsscvg1: 0 MB releasedscvg1: inuse: 96, idle: 381, sys: 477, released: 0, consumed: 477 (MB)GC forcedgc12(1): 85+0+353+1 us, 95 -> 95 MB, 438 (789-351) objects, 6 goroutines, 54/37/0 sweeps, 0(0) handoff, 0(0) steal, 0/0/0 yields
由於大多數營運工具是站在作業系統的角度來對待你的程式,那麼瞭解程式內部實際發生了什麼就變得尤為重要了。
更多選項可以參考 runtime 包。
摘錄如下:
- RES – 將會顯示在當前時刻進程的記憶體用量,但可能不包含任何尚未換入或已經換出的頁面。
- mem.Alloc – 已經被配並仍在使用的位元組數
- mem.TotalAlloc – 從開始運行到現在分配的記憶體總數
- mem.HeapAlloc – 堆當前的用量
- mem.HeapSys – 包含堆當前和已經被釋放但尚未歸還作業系統的用量
更進一步說,明白 pprof 僅僅是擷取了樣本,而不是真正的值,是非常重要的。
通常在處理這個情況的時候,不要聚焦於數字本身,而著眼於解決問題。
我們堅信要測量一切,但是同時覺得“現代”營運工具是相當糟糕的,並聚焦於問題的影響,而不是真正的問題。
如果你的車不能發動了,你可能認為這是個問題,但它不是。這甚至不是郵箱空了的表象。真正的問題在於你沒有給郵箱加油,但你關注的是最初的問題導致的一系列的結果。
如果對於 Go 程式你只關注來自 ps 的的 RES 值,它可能告訴你這裡出問題了,除非你更進一步挖掘,否則沒有任何線索可以解決這個問題。我們希望能更正它。
更正:
最後一段未進行編輯。它並不是用來貶低營運或 devops 人員。其目的在於展示應用層級的度量和系統層級的度量。我們已經意識到這裡的表達有誤,並且對此道歉。我們只是覺得已有的“營運”工具沒有為開發人員提供充分的資訊來修複他們的問題。
我們同時認為當前應用層級的度量工具仍然匱乏。
營運人員扮演著至關重要的角色,對於他們的工作我們至誠的感謝。事實上,是開發人員糟糕的代碼把事情搞麻煩了,這也是我們正在著手解決的問題。
最終更正
與其讓營運人員擁有超過 300 個圖表包括表格、計數器、點線圖和長條圖。作為編寫軟體的我們,應當更加關注找到真正的問題,並提出真正的解決方案。