做者: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))均可以用相似的手段回收。