golang手动管理内存

 

作者:John Graham-Cumming.   原文点击此处。翻译:Lubia Yang(已失效)

前些天我介绍了我们对Lua的使用,implement our new Web Application Firewall

另一种在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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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:程序向应用程序申请的内存

HeapAlloc:堆上目前分配的内存

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)会被重用,但很少归还到操作系统。

解决此问题的一个办法是在程序中手动进行内存管理。例如,

程序可以这样重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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))都可以用类似的手段回收。

 
 

作者:John Graham-Cumming.   原文点击此处。翻译:Lubia Yang(已失效)

前些天我介绍了我们对Lua的使用,implement our new Web Application Firewall

另一种在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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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:程序向应用程序申请的内存

HeapAlloc:堆上目前分配的内存

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)会被重用,但很少归还到操作系统。

解决此问题的一个办法是在程序中手动进行内存管理。例如,

程序可以这样重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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))都可以用类似的手段回收。

猜你喜欢

转载自www.cnblogs.com/williamjie/p/9493104.html