- 起因
- 原因調查
- 原因分析
- 問題解決
- 總結
起因
今天在做資料庫資料讀取時, 首先通過多個 goroutine 將從資料庫讀取的資料寫入 channel, 同時通過另一個 goroutine 從 channel 中讀取資料進行分析.
就是這么簡單的一個功能, 在讀取資料的時候不定期的會出如下錯誤:
[signal SIGSEGV: segmentation violation code=0x1 addr=0x7f2227fe004d pc=0x52eb6f]
原因調查
資料庫是 boltdb, 錯誤的位置總是出在 json.Unmarshal 的地方:
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 錯誤.
于是, 就分析了下整個程序, 讀取資料的 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 }
讀取資料的代碼也很簡單, 沒有明顯的問題.
原因分析
讀寫 channel 的代碼就是上面那么簡單, 一眼就能看明白, 為什么會 panic? 我進行了多次實驗, 發現如下現象:
- 每次 panic 的時候, json.Unmarshal 收到的資料不一樣, 也就是 panic 不是發生在固定的資料上
- 發生 panic 的時候, 都是在資料讀取完之后, 也就是上面的 readOneDB 執行完之后
- 如果 channel 的容量小, 很難出現 panic, 如果 channel 的容量大(比如 10000 以上, make(chan []byte, 10000)), 就容易出現 panic
- boltdb 總體資料量(80 萬條)不算小, 如果資料量小的庫, 不會出現 panic
基于上面的分析, 我當時就覺得是不是 db.Close() 之后, 把寫入 channel 的一些資料也釋放了.
問題解決
于是, 我嘗試在寫入 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 }
這樣改造之后, 就再也沒有出現記憶體錯誤了!
總結
golang 的 channel 中寫入資料的時候, 如果寫入的是參考型別, 那么應該寫入的是資料的地址, 而不是完整的資料, 如果該地址對應的資料被 GC 回收的話, 在使用資料的地方就會導致 記憶體錯誤(panic)
這種問題很隱蔽, 因為 GC 的回收時機無法控制, 我們能做的就是在代碼層面保證要用的資料不會被回收.
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/40875.html
標籤:Go
