sarama踩坑记录——rebalance时读取数据

224 阅读2分钟

背景

为了批量操作数据,在流式读取数据基础上将数据写入至batch中并批量的对数据进行处理,这个过程中为了防止数据丢失,使用for select 的方式代替for range对数据进行读取,发现服务会不定期的发生空指针异常导致panic。

为什么会这样呢?

分析

根据对sarama源码的分析, 我们发现了如下内容:

ConsumeClaim must start a consumer loop of ConsumerGroupClaim's Messages().Once the Messages() channel is closed, the Handler must finish its processing loop and exit.

sarama 推荐使用for range channel处理数据,但这并不代表不能使用for select 的方式处理数据,(尽管for select 会因为通道的关闭而产生空指针异常),在这个场景我们会产生多个问题:

  1. 为什么从关闭的通道读取数据会读取空指针
  2. 为什么for range不会出现这类问题
  3. 如果在for select 的场景避免因通道关闭而产生panic

为什么从关闭的通道读取数据会读取空指针

不妨回忆下从通道读取/写入数据会有哪些异常情况,这里前人之述备矣直接说结论了:分别会出现阻塞、panic(仅在写入场景可能发生)、零值几种情况,其中对于无缓冲或缓冲区为空的情况,读取数据会出现阻塞,因为我们期待读到数据,可能有其他协程在一段时间后会往通道中写入数据。

而当通道关闭时,我们很确定通道中不会再有新数据(因这个时候再写入就会panic),那么当关闭的通道还有数据时,我们可以正常的读取,但当通道的数据不再存在时,我们既不能阻塞(因为不会有新数据写入,所以等待无意义),也不能直接panic(因为作为读取通道的一方,并不应该判断通道中是否还有数据),所以只能返回一个零值。

但这样我们去使用for select 时就会造成迅速的读到一个零值而不是阻塞,直到case <- time.After()生效。

为什么for range不会出现这类问题

回答这个问题,本质上要知道 for range在做什么,源码面前,没有秘密,不过这里面不是go语言源码,而是go编译器源码

 // The loop we generate:
  //   for {
  //           index_temp, ok_temp = <-range
  //           if !ok_temp {
  //                   break
  //           }
  //           index = index_temp
  //           original body
  //   }

这里面可以看到每次读取时都从通道中获取了两个值,通过对ok_temp的判断,及时的break

如果在for select 的场景避免因通道关闭而产生panic

通过对源码的查看我们也知道了如何解决当下的问题

for {
    select {
        case msg, ok := <- ch:
            if !ok {
                break
            }
        case <- time.After(time.Second):
    }
}