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))都可以用類似的手段回收。

 

 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM