<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          踩了 Golang sync.Map 的一個(gè)坑

          共 788字,需瀏覽 2分鐘

           ·

          2020-08-26 12:21

          緣起

          最近 Go 1.15 發(fā)布了,我也第一時(shí)間更新了這個(gè)版本,畢竟對(duì) Go 的穩(wěn)定性還是有一些信心的,于是直接在公司上了生產(chǎn)。

          結(jié)果,上線幾分鐘,就出現(xiàn)了 OOM,于是 pprof 了一下 heap,然后趕緊回滾,發(fā)現(xiàn)某塊本應(yīng)該在一次請(qǐng)求結(jié)束時(shí)被釋放的內(nèi)存,被保留了下來(lái)而且一直在增長(zhǎng),如圖(圖中的 linkBufferNode):



          這次上線的變更只有 Go 版本的升級(jí),沒(méi)有任何其它變動(dòng),于是在本地開(kāi)始測(cè)試,發(fā)現(xiàn)在本地也能百分百?gòu)?fù)現(xiàn)。

          排查過(guò)程

          看了 Go 1.15 的 Release Note,發(fā)現(xiàn)有倆高度疑似的東西:

          1. 去除了一些 GC Data,使得 binary size 減少了 5%;

          2. 新的內(nèi)存分配算法。

          于是改 runtime,關(guān)閉新的內(nèi)存分配算法,切換回舊的,等等一頓操作猛如虎下來(lái),發(fā)現(xiàn)問(wèn)題還是沒(méi)解決,現(xiàn)象仍然存在。



          于是實(shí)在不行,祭出了GODEBUG="allocfreetrace=1大法,肉眼從 100MB+ 的日志文件里面看啊看啊看啊看啊看啊看啊看啊看啊看啊看啊……(此處省略心酸過(guò)程)

          最終直覺(jué)告訴我,這個(gè)問(wèn)題可能和 Go 1.15 中 sync.Map 的改動(dòng)有關(guān)(別問(wèn)我為啥,真的是直覺(jué),我也說(shuō)不出來(lái))。

          示例代碼

          為了方便講解,我寫(xiě)了一個(gè)最小可復(fù)現(xiàn)的代碼,如下:

          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 改動(dòng)

          在 Go 1.15 中,sync.Map 增加了一個(gè)方法LoadAndDelete,具體的 issue 在這:https://github.com/golang/go/issues/33762CL, 在這:https://go-review.googlesource.com/c/go/+/205899/。

          為什么我確認(rèn)是這個(gè)改動(dòng)導(dǎo)致的呢?很簡(jiǎn)單:我在本地把這個(gè)改動(dòng) revert 掉了,問(wèn)題就沒(méi)了,好了關(guān)機(jī)下班……

          當(dāng)然沒(méi)這么簡(jiǎn)單,知其然要知其所以然,于是開(kāi)始看到底改了哪塊……(此處省略 100000 字)

          最終發(fā)現(xiàn),關(guān)鍵代碼是這段:

          // 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
          }
          }
          }

          在這段代碼中,會(huì)發(fā)現(xiàn)在 Delete 的時(shí)候,并沒(méi)有真正刪除掉 key,而是從 key 中取出了 entry,然后把 entry 設(shè)為 nil……

          所以,在我們場(chǎng)景中,我們把一個(gè)連接作為 key 放了進(jìn)去,于是和這個(gè)連接相關(guān)的比如 buffer 的內(nèi)存就永遠(yuǎn)無(wú)法釋放了……

          那么為什么在 Go 1.14 中沒(méi)有問(wèn)題呢?以下是 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 中,是會(huì)被刪除的;而湊巧,我們其實(shí) “誤用” 了 sync.Map,在我們的使用過(guò)程中沒(méi)有讀操作,導(dǎo)致所有的 key 其實(shí)都在 dirty 里面,所以當(dāng)調(diào)用 Delete 的時(shí)候是會(huì)被真正刪除的。

          要注意,無(wú)論哪個(gè)版本的 Go,一旦 key 升級(jí)到了 read 中,都是永遠(yuǎn)不會(huì)被刪除的。

          總結(jié)

          在 Go <= 1.15 版本中,sync.Map 中的 key 是不會(huì)被刪除的,如果在 Key 中放了一個(gè)大的對(duì)象,或者關(guān)聯(lián)有內(nèi)存,就會(huì)導(dǎo)致內(nèi)存泄漏。

          針對(duì)這個(gè)問(wèn)題,我已經(jīng)向 Go 官方提出了 Issue,目前尚不清楚是 by-design 還是 bug:https://github.com/golang/go/issues/40999


          瀏覽 62
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  乱伦中文字幕网 | 看免费操逼视频 | 国产主播在线观看 | 99精品视频国产 | 国产精品MV视频 |