聽說過對 Go map 做 GC 嗎?
在 Golang 中的 map 結構,在刪除鍵值對的時候,並不會真正的刪除,而是標記。那麼隨着鍵值對越來越多,會不會造成大量內存浪費?
首先答案是會的,很有可能導致 OOM,而且針對這個還有一個討論:https://github.com/golang/go/issues/20135。大致的意思就是在很大的 map 中,delete 操作沒有真正釋放內存而可能導致內存 OOM。
所以一般的做法:就是 重建 map。而 go-zero 中內置了 safemap 的容器組件。safemap 在一定程度上可以避免這種情況發生。
那首先我們看看 go 原生提供的 map 是怎麼刪除的?
原生 map 刪除
1 package main
2
3 func main() {
4 m := make(map[int]string, 9)
5 m[1] = "hello"
6 m[2] = "world"
7 m[3] = "go"
8
9 v, ok := m[1]
10 _, _ = fn(v, ok)
11
12 delete(m, 1)
13 }
14
15 func fn(v string, ok bool) (string, bool) {
16 return v, ok
17 }
測試代碼如上,我們可以通過 go tool compile -S -N -l testmap.go | grep "CALL" :
0x0071 00113 (test/testmap.go:4) CALL runtime.makemap(SB)
0x0099 00153 (test/testmap.go:5) CALL runtime.mapassign_fast64(SB)
0x00ea 00234 (test/testmap.go:6) CALL runtime.mapassign_fast64(SB)
0x013b 00315 (test/testmap.go:7) CALL runtime.mapassign_fast64(SB)
0x0194 00404 (test/testmap.go:9) CALL runtime.mapaccess2_fast64(SB)
0x01f1 00497 (test/testmap.go:10) CALL "".fn(SB)
0x0214 00532 (test/testmap.go:12) CALL runtime.mapdelete_fast64(SB)
0x0230 00560 (test/testmap.go:7) CALL runtime.gcWriteBarrier(SB)
0x0241 00577 (test/testmap.go:6) CALL runtime.gcWriteBarrier(SB)
0x0252 00594 (test/testmap.go:5) CALL runtime.gcWriteBarrier(SB)
0x025c 00604 (test/testmap.go:3) CALL runtime.morestack_noctxt(SB)
執行第 12 行的 delete,實際執行的是 runtime.mapdelete_fast64。
這些函數的參數類型是具體的 int64,mapdelete_fast64 跟原始的 delete 操作一樣的,所以我們來看看 mapdelete。
mapdelete
長圖預警!!!
大致代碼分析如上,具體代碼就留給大家去閱讀了。其實大致過程:
-
寫保護,防止併發寫
-
查詢要刪除的
key是否存在 -
存在則對其標誌做刪除標記
-
count--
所以你在大面積刪除 key ,實際 map 存儲的 key 是不會刪除的,只是標記當前的 key 狀態爲 empty。
其實出發點,和 mysql 的標記刪除類似,防止後續會有相同的 key 插入,省去了擴縮容的操作。
但是這個對有些場景是不妥的,如果開發者在未來時間內都不會再插入相同的 key ,很可能會導致 OOM。
所以針對以上情況,go-zero 開發了 safemap 。下面我們看看 safemap 是如何避免這個問題的?
safemap
直接從操作 safemap 中分析爲什麼要這麼設計:
-
預設一個 刪除閾值,如果觸發會放到一個新預設好的
newmap中 -
兩個
map是一個整體,所以key只能留一份
所以爲什麼要設置兩個 map 就很清楚了:
-
dirtyOld作爲存儲主體,如果delete操作達到閾值,則會觸發遷移。 -
dirtyNew作爲暫存體,會在到達閾值時,存放部分key/value
所以在遷移操作時,我們需要做的就是:將原先的 dirtyOld 清空,存儲的 key/value 通過 for-range 重新存儲到 dirtyNew,然後將 dirtyNew 指向 dirtyOld。
可能會有疑問:不是說
key/value沒有刪除嗎,只是標記了tophash=empty其實在
for-range過程中,會過濾掉tophash <= emptyOne的 key
這樣就實現了不需要的 key 不會被加入到 dirtyNew,進而不會影響 dirtyOld。
這其實也就是垃圾回收的年老代和新生代的概念。
更多實現細節,可以查看源碼!
項目地址
https://github.com/tal-tech/go-zero
歡迎使用 go-zero 並 star 支持我們!
微信交流羣
關注『微服務實踐』公衆號並點擊 交流羣 獲取社區羣二維碼。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/3Wg3GWHyYEps3Qvm-MoENg