zoukankan      html  css  js  c++  java
  • golang channel 的一次内存错误

    起因

    今天在做数据库数据读取时, 首先通过多个 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
    ", 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? 我进行了多次实验, 发现如下现象:

    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 的一些数据也释放了.

    问题解决

    于是, 我尝试在写入 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 的回收时机无法控制, 我们能做的就是在代码层面保证要用的数据不会被回收.

  • 相关阅读:
    Java的Regex --正则表达式
    Java的包装类
    类的始祖Object
    abstract和interface关键字介绍
    内部类
    Accumulation Degree [换根dp,二次扫描]
    牛客练习赛61 [口胡]
    CF1334G Substring Search [bitset,乱搞]
    CF1175F The Number of Subpermutations [哈希,乱搞]
    CF793G Oleg and chess [线段树优化建边,扫描线,最大流]
  • 原文地址:https://www.cnblogs.com/wang_yb/p/12334696.html
Copyright © 2011-2022 走看看