做者:John Graham-Cumming. 原文點擊此處。翻譯:Lubia Yang 程序員
前些天我介紹了咱們對Lua的使用,implement our new Web Application Firewall. golang
另外一種在CloudFlare (做者的公司)變得很是流行的語言是Golang。在過去,我寫了一篇 how we use Go來介紹相似Railgun的網絡服務的編寫。 編程
用Golang這樣帶GC的語言編寫長期運行的網絡服務有一個很大的挑戰,那就是內存管理。 數組
爲了理解Golang的內存管理有必要對run-time源碼進行深挖。有兩個進程區分應用程序再也不使用的內存,當它們看起來不會再使用,就把它們歸還到操做系統(在Golang源碼裏稱爲scavenging )。 安全
這裏有一個簡單的程序製造了大量的垃圾(garbage),每秒鐘建立一個 5,000,000 到 10,000,000 bytes 的數組。程序維持了20個這樣的數組,其餘的則被丟棄。程序這樣設計是爲了模擬一種很是常見的狀況:隨着時間的推移,程序中的不一樣部分申請了內存,有一些被保留,但大部分再也不重複使用。在Go語言網絡編程中,用goroutines 來處理網絡鏈接和網絡請求時(network connections or requests),一般goroutines都會申請一塊內存(好比slice來存儲收到的數據)而後就再也不使用它們了。隨着時間的推移,會有大量的內存被網絡鏈接(network connections)使用,鏈接累積的垃圾come and gone。 網絡
package main import ( "fmt" "math/rand" "runtime" "time" ) func makeBuffer() []byte { return make([]byte, rand.Intn(5000000)+5000000) } func main() { pool := make([][]byte, 20) var m runtime.MemStats makes := 0 for { b := makeBuffer() makes += 1 i := rand.Intn(len(pool)) pool[i] = b time.Sleep(time.Second) bytes := 0 for i := 0; i < len(pool); i++ { if pool[i] != nil { bytes += len(pool[i]) } } runtime.ReadMemStats(&m) fmt.Printf("%d,%d,%d,%d,%d,%d\n", m.HeapSys, bytes, m.HeapAlloc, m.HeapIdle, m.HeapReleased, makes) } }
程序使用 runtime.ReadMemStats函數來獲取堆的使用信息。它打印了四個值, 函數
HeapSys:程序嚮應用程序申請的內存 oop
HeapAlloc:堆上目前分配的內存 lua
HeapIdle:堆上目前沒有使用的內存 操作系統
HeapReleased:回收到操做系統的內存
GC在Golang中運行的很頻繁(參見GOGC環境變量(GOGC environment variable )來理解怎樣控制垃圾回收操做),所以在運行中因爲一些內存被標記爲」未使用「,堆上的內存大小會發生變化:這會致使HeapAlloc和HeapIdle發生變化。Golang中的scavenger 會釋放那些超過5分鐘仍然沒有再使用的內存,所以HeapReleased不會常常變化。
下面這張圖是上面的程序運行了10分鐘之後的狀況:
(在這張和後續的圖中,左軸以是以byte爲單位的內存大小,右軸是程序執行次數)
紅線展現了pool中byte buffers的數量。20個 buffers 很快達到150,000,000 bytes。最上方的藍色線表示程序從操做系統申請的內存。穩定在375,000,000 bytes。所以程序申請了2.5倍它所需的空間!
當GC發生時,HeapIdle和HeapAlloc發生跳變。橘色的線是makeBuffer()發送的次數。
這種過分的內存申請是有GC的程序的通病,參見這篇paper
Quantifying the Performance of Garbage Collection vs. Explicit Memory Management
程序不斷執行,idle memory(即HeapIdle)會被重用,但不多歸還到操做系統。
解決此問題的一個辦法是在程序中手動進行內存管理。例如,
程序能夠這樣重寫:
package main import ( "fmt" "math/rand" "runtime" "time" ) func makeBuffer() []byte { return make([]byte, rand.Intn(5000000)+5000000) } func main() { pool := make([][]byte, 20) buffer := make(chan []byte, 5) var m runtime.MemStats makes := 0 for { var b []byte select { case b = <-buffer: default: makes += 1 b = makeBuffer() } i := rand.Intn(len(pool)) if pool[i] != nil { select { case buffer <- pool[i]: pool[i] = nil default: } } pool[i] = b time.Sleep(time.Second) bytes := 0 for i := 0; i < len(pool); i++ { if pool[i] != nil { bytes += len(pool[i]) } } runtime.ReadMemStats(&m) fmt.Printf("%d,%d,%d,%d,%d,%d\n", m.HeapSys, bytes, m.HeapAlloc, m.HeapIdle, m.HeapReleased, makes) } }
下面這張圖是上面的程序運行了10分鐘之後的狀況:
這張圖展現了徹底不一樣的狀況。實際使用的buffer幾乎等於從操做系統中申請的內存。同時GC幾乎沒有工做可作。堆上只有不多的HeapIdle最終須要歸還到操做系統。
這段程序中內存回收機制的關鍵操做就是一個緩衝的channel ——buffer,在上面的代碼中,buffer是一個能夠存儲5個[]byte slice的容器。當程序須要空間時,首先會使用select從buffer中讀取:
select {
case b = <- buffer:
default :
makes += 1
b = makeBuffer()
}
這永遠不會阻塞由於若是channel中有數據,就會被讀出,若是channel是空的(意味着接收會阻塞),則會建立一個。
使用相似的非阻塞機制將slice回收到buffer:
select {
case buffer <- pool[i]:
pool[i] = nil
default:
}
若是buffer 這個channel滿了,則以上的寫入過程會阻塞,這種狀況下default觸發。這種簡單的機制能夠用於安全的建立一個共享池,甚至可經過channel傳遞實現多個goroutines之間的完美、安全共享。
在咱們的實際項目中運用了類似的技術,實際使用中(簡單版本)的回收器(recycler )展現在下面,有一個goroutine 處理buffers的構造並在多個goroutine之間共享。get(獲取一個新buffer)和give(回收一個buffer到pool)這兩個channel被全部goroutines使用。
回收器對收回的buffer保持鏈接,並按期的丟棄那些過於陳舊可能不會再使用的buffer(在示例代碼中這個週期是一分鐘)。這讓程序能夠自動應對爆發性的buffers需求。
package main import ( "container/list" "fmt" "math/rand" "runtime" "time" ) var makes int var frees int func makeBuffer() []byte { makes += 1 return make([]byte, rand.Intn(5000000)+5000000) } type queued struct { when time.Time slice []byte } func makeRecycler() (get, give chan []byte) { get = make(chan []byte) give = make(chan []byte) go func() { q := new(list.List) for { if q.Len() == 0 { q.PushFront(queued{when: time.Now(), slice: makeBuffer()}) } e := q.Front() timeout := time.NewTimer(time.Minute) select { case b := <-give: timeout.Stop() q.PushFront(queued{when: time.Now(), slice: b}) case get <- e.Value.(queued).slice: timeout.Stop() q.Remove(e) case <-timeout.C: e := q.Front() for e != nil { n := e.Next() if time.Since(e.Value.(queued).when) > time.Minute { q.Remove(e) e.Value = nil } e = n } } } }() return } func main() { pool := make([][]byte, 20) get, give := makeRecycler() var m runtime.MemStats for { b := <-get i := rand.Intn(len(pool)) if pool[i] != nil { give <- pool[i] } pool[i] = b time.Sleep(time.Second) bytes := 0 for i := 0; i < len(pool); i++ { if pool[i] != nil { bytes += len(pool[i]) } } runtime.ReadMemStats(&m) fmt.Printf("%d,%d,%d,%d,%d,%d,%d\n", m.HeapSys, bytes, m.HeapAlloc m.HeapIdle, m.HeapReleased, makes, frees) } }
執行程序10分鐘,圖像會相似於第二幅:
這些技術能夠用於程序員知道某些內存能夠被重用,而不用藉助於GC,能夠顯著的減小程序的內存使用,同時可使用在其餘數據類型而不只是[]byte slice,任意類型的Go type(用戶定義的或許不行(user-defined or not))均可以用相似的手段回收。