緣起
最近 Go 1.15 發佈了,我也第一時間更新了這個版本,畢竟對 Go 的穩定性仍是有一些信心的,因而直接在公司上了生產。git
結果,上線幾分鐘,就出現了 OOM,因而 pprof 了一下 heap,而後趕忙回滾,發現某塊本應該在一次請求結束時被釋放的內存,被保留了下來並且一直在增加,如圖(圖中的 linkBufferNode):github
此次上線的變動只有 Go 版本的升級,沒有任何其它變更,因而在本地開始測試,發如今本地也能百分百復現。golang
排查過程
看了 Go 1.15 的 Release Note,發現有倆高度疑似的東西:算法
去除了一些 GC Data,使得 binary size 減小了 5%;微信
新的內存分配算法。app
因而改 runtime,關閉新的內存分配算法,切換回舊的,等等一頓操做猛如虎下來,發現問題仍是沒解決,現象仍然存在。less
GODEBUG="allocfreetrace=1
最終直覺告訴我,這個問題可能和 Go 1.15 中 sync.Map 的改動有關(別問我爲啥,真的是直覺,我也說不出來)。this
示例代碼
爲了方便講解,我寫了一個最小可復現的代碼,以下:google
package main
import (
"sync"
)
var sm sync.Map
func insertKeys() {
keys := make([]interface{}, 0, 10)
// Store some keys
for i := 0; i < 10; i++ {
v := make([]int, 1000)
keys = append(keys, &v)
sm.Store(keys[i], struct{}{})
}
// delete some keys, but not all keys
for i, k := range keys {
if i%2 == 0 {
continue
}
sm.Delete(k)
}
}
func shutdown() {
sm.Range(func(key, value interface{}) bool {
// do something to key
return true
})
}
func main() {
insertKeys()
// do something ...
shutdown()
}
Go 1.15 中 sync.Map 改動
LoadAndDelete
爲何我確認是這個改動致使的呢?很簡單:我在本地把這個改動 revert 掉了,問題就沒了,好了關機下班……
固然沒這麼簡單,知其然要知其因此然,因而開始看到底改了哪塊……(此處省略 100000 字)
最終發現,關鍵代碼是這段:
// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if ok {
return e.delete()
}
return nil, false
}
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
m.LoadAndDelete(key)
}
func (e *entry) delete() (value interface{}, ok bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == nil || p == expunged {
return nil, false
}
if atomic.CompareAndSwapPointer(&e.p, p, nil) {
return *(*interface{})(p), true
}
}
}
在這段代碼中,會發如今 Delete 的時候,並無真正刪除掉 key,而是從 key 中取出了 entry,而後把 entry 設爲 nil……
因此,在咱們場景中,咱們把一個鏈接做爲 key 放了進去,因而和這個鏈接相關的好比 buffer 的內存就永遠沒法釋放了……
那麼爲何在 Go 1.14 中沒有問題呢?如下是 Go 1.14 的代碼:
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
e.delete()
}
}
在 Go 1.14 中,若是 key 在 dirty 中,是會被刪除的;而湊巧,咱們其實 「誤用」 了 sync.Map,在咱們的使用過程當中沒有讀操做,致使全部的 key 其實都在 dirty 裏面,因此當調用 Delete 的時候是會被真正刪除的。
要注意,不管哪一個版本的 Go,一旦 key 升級到了 read 中,都是永遠不會被刪除的。
總結
在 Go <= 1.15 版本中,sync.Map 中的 key 是不會被刪除的,若是在 Key 中放了一個大的對象,或者關聯有內存,就會致使內存泄漏。
針對這個問題,我已經向 Go 官方提出了 Issue,目前尚不清楚是 by-design 仍是 bug:https://github.com/golang/go/issues/40999