golang channel 的一次內存錯誤

原由

今天在作數據庫數據讀取時, 首先經過多個 goroutine 將從數據庫讀取的數據寫入 channel, 同時經過另外一個 goroutine 從 channel 中讀取數據進行分析.golang

就是這麼簡單的一個功能, 在讀取數據的時候不按期的會出以下錯誤:數據庫

[signal SIGSEGV: segmentation violation code=0x1 addr=0x7f2227fe004d pc=0x52eb6f]

緣由調查

數據庫是 boltdb, 錯誤的位置老是出在 json.Unmarshal 的地方:json

1  for v := range outCh {
2    var data OmsData
3    if err := json.Unmarshal(v, &data); err != nil {
4      log.Fatalf("json unmarshal error: %v\n", err)
5    }
6  }

outCh 中就是從數據庫讀取的數據. 剛開始覺得是數據中的數據有錯誤, 後來發現 err 也捕獲不到, 每次都是 panic 錯誤.code

因而, 就分析了下整個過程, 讀取數據的 goroutine 代碼大體以下:內存

1  func readOneDB(db *bolt.DB, outCh chan []byte) {
 2    defer db.Close()
 3
 4    // 獲取 db 中的全部 bucket
 5    bucketNames := getAllBucketNames(db)
 6
 7    err := db.View(func(tx *bolt.Tx) error {
 8
 9      for _, bName := range bucketNames {
10
11        bucket := tx.Bucket([]byte(bName))
12
13        bucket.ForEach(func(_ []byte, v []byte) error {
14          // 把 bucket 中的value 寫入 channel
15          outCh <- v
16          return nil
17        })
18      }
19
20      return nil
21    })
22
23    if err != nil {
24      log.Fatal(err)
25    }
26  }

讀取數據的代碼也很簡單, 沒有明顯的問題.get

緣由分析

讀寫 channel 的代碼就是上面那麼簡單, 一眼就能看明白, 爲何會 panic? 我進行了屢次實驗, 發現以下現象:io

  1. 每次 panic 的時候, json.Unmarshal 收到的數據不同, 也就是 panic 不是發生在固定的數據上
  2. 發生 panic 的時候, 都是在數據讀取完以後, 也就是上面的 readOneDB 執行完以後
  3. 若是 channel 的容量小, 很難出現 panic, 若是 channel 的容量大(好比 10000 以上, make(chan []byte, 10000)), 就容易出現 panic
  4. boltdb 整體數據量(80 萬條)不算小, 若是數據量小的庫, 不會出現 panic

基於上面的分析, 我當時就以爲是否是 db.Close() 以後, 把寫入 channel 的一些數據也釋放了.class

問題解決

因而, 我嘗試在寫入 channel 以前, 把數據複製一份, 改造 readOneDB 以下:引用

1  func readOneDB(db *bolt.DB, outCh chan []byte) {
 2    defer db.Close()
 3
 4    bucketNames := getAllBucketNames(db)
 5
 6    err := db.View(func(tx *bolt.Tx) error {
 7
 8      for _, bName := range bucketNames {
 9
10        bucket := tx.Bucket([]byte(bName))
11
12        bucket.ForEach(func(_ []byte, v []byte) error {
13          // ** 改造的部分 **
14          // 改造的方式就是把 bucket 中的數據copy一份放入channel
15          // 而不是像以前那樣, 直接把 v 放入 channel
16          nb := make([]byte, len(v))
17          copy(nb, v)
18          outCh <- nb
19          return nil
20        })
21      }
22
23      return nil
24    })
25
26    if err != nil {
27      log.Fatal(err)
28    }
29  }

這樣改造以後, 就再也沒有出現內存錯誤了!channel

總結

golang 的 channel 中寫入數據的時候, 若是寫入的是引用類型, 那麼應該寫入的是數據的地址, 而不是完整的數據, 若是該地址對應的數據被 GC 回收的話, 在使用數據的地方就會致使 內存錯誤(panic)

這種問題很隱蔽, 由於 GC 的回收時機沒法控制, 咱們能作的就是在代碼層面保證要用的數據不會被回收.

相關文章
相關標籤/搜索