踩了 Golang sync.Map 的一個坑
緣起
最近 Go 1.15 釋出了,我也第一時間更新了這個版本,畢竟對 Go 的穩定性還是有一些信心的,於是直接在公司上了生產。
結果,上線幾分鐘,就出現了 OOM,於是 pprof 了一下 heap,然後趕緊回滾,發現某塊本應該在一次請求結束時被釋放的記憶體,被保留了下來而且一直在增長,如圖(圖中的 linkBufferNode):
這次上線的變更只有 Go 版本的升級,沒有任何其它變動,於是在本地開始測試,發現在本地也能百分百復現。
排查過程
看了 Go 1.15 的 Release Note,發現有倆高度疑似的東西:
- 去除了一些 GC Data,使得 binary size 減少了 5%;
- 新的記憶體分配演算法。
於是改 runtime,關閉新的記憶體分配演算法,切換回舊的,等等一頓操作猛如虎下來,發現問題還是沒解決,現象仍然存在。
於是實在不行,祭出了GODEBUG="allocfreetrace=1
大法,肉眼從 100MB+ 的日誌檔案裡面看啊看啊看啊看啊看啊看啊看啊看啊看啊看啊……(此處省略心酸過程)
最終直覺告訴我,這個問題可能和 Go 1.15 中 sync.Map 的改動有關(別問我為啥,真的是直覺,我也說不出來)。
示例程式碼
為了方便講解,我寫了一個最小可復現的程式碼,如下:
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 改動
在 Go 1.15 中,sync.Map 增加了一個方法LoadAndDelete
,具體的 issue 在這:https://github.com/golang/go/issues/33762CL, 在這:https://go-review.googlesource.com/c/go/+/205899/。
為什麼我確認是這個改動導致的呢?很簡單:我在本地把這個改動 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 中,在沒有 miss 到一定的值讓 dirty 提升為 read 時,key 都是永遠不會被刪除的。也就是說,極端情況之下,key 是會洩露的。
總結
在 Go <= 1.15 版本中,sync.Map 中的 key 在極端情況下是不會被刪除的,如果在 Key 中放了一個大的物件,或者關聯有記憶體,就會導致記憶體洩漏。
針對這個問題,我已經向 Go 官方提出了Issue,目前來看這個 behaviour 定義為了 bug(因為違背了 Go 1 相容性承諾,和 1.14 中的 behaviour 不同了),已經由 @ChangKun Ou 大佬提了 pr 修復了,並且 backport 到了 1.15.1 中。
而針對 read 中的 key 在沒有 dirty 被提升時不會刪除的問題,目前看來是一個設計上的 trade-off,如果有真實世界中的程式(real-world program)出問題的話,再提 issue,看看是否要解決。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- golang的踩坑Golang
- Golang 需要避免踩的 50 個坑Golang
- 今天踩了一個基礎坑
- 又踩坑了!BigDecimal使用的5個坑!Decimal
- golang—踩坑之切片Golang
- golang的defer踩坑彙總Golang
- Golang Recover的一個小坑Golang
- 踩一個Flutter Hot Reload的新坑Flutter
- 深坑啊!同一個Spring AOP的坑,我一天踩了兩次!Spring
- golang 介紹以及踩坑之四Golang
- 踩了個DNS解析的坑,但我還是沒想通DNS
- [Golang併發]Sync.mapGolang
- 時區的坑,別再踩了!
- 深入理解golang:sync.mapGolang
- 讀了這一篇,讓你少踩 ArrayList 的那些坑
- React兩個bug踩坑React
- golang sync.Map之如何設計一個併發安全的讀寫分離結構?Golang
- 一個極易踩坑的例子,希望大家引以為戒
- golang連線達夢資料庫的一個坑Golang資料庫
- 初學 GoLang 遇到的一個關於時間的坑...Golang
- Django開發踩坑(一)Django
- gorm踩坑記錄(一)GoORM
- php學習踩坑(一)PHP
- electron踩坑系列之一
- SpringBoot踩坑日記-一個非空校驗引發的bugSpring Boot
- Go“一個包含nil指標的介面不是nil介面”踩坑Go指標
- 等等!這兩個 Spring-RabbitMQ 的坑我們已經替你踩了SpringMQ
- 初入職場的小夥伴請注意,這 8 個坑不要再踩了
- 使用npm釋出一個react元件(踩坑實踐)NPMReact元件
- Python 初學者容易踩的 5 個坑Python
- 踩過的坑(一)——web容器升級Web
- [踩了個坑] Laravel 訪問https網址,url('/')竟然只返回 http?LaravelHTTP
- 【踩坑指南】執行緒池使用不當的五個坑執行緒
- 曾經面試踩過的坑,都在這裡了~面試
- 小白程式設計師最容易踩的“坑”,你踩過幾個?程式設計師
- golang定時任務踩坑及終極解決方案Golang
- Docker踩坑四個教訓 - resurfaceDocker
- golang最近遇到的一些坑Golang