Go Web服务出现这异常,没有被recover住,线上web服务直接崩溃了

353 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情


事情缘由

就在前几天,线上生产环境发生了本年度第二次p0级事故,整个事故持续了接近2小时才恢复。

事后查看日志,发现的异常如下:

fatal error: concurrent map read and map write

goroutine 6657 [running]:
runtime.throw(0x19a1fde, 0x21)
	/usr/local/go/src/runtime/panic.go:1117 +0x72 fp=0xc01708d090 sp=0xc01708d060 pc=0x438f12
runtime.mapaccess2_fast64(0x16ef640, 0xc002a4cf60, 0x5e79, 0xc002a4cf60, 0x0)
	/usr/local/go/src/runtime/map_fast64.go:61 +0x1ac fp=0xc01708d0b8 sp=0xc01708d090 pc=0x41382c
xxxxxx/xxxbiz.(*XXXXBIZ).getPaidWallpaperLiveResolutionInfo(0x2569560, 0x1b9c9a0, 0xc00b946000, 0xc002bfc300, 0x5d, 0x5e, 0x0, 0x0, 0x259a250)
	/data/build/app/xxx/xxx/xxxbiz/xxxbiz_live.go:206 +0x10e fp=0xc01708d1e0 sp=0xc01708d0b8 pc=0x12877ee
```

这个是在一堆日志的崩溃中找到的关键一段日志

至此,已经非常明朗,出现了map并发读写。

为什么没有被recover住

大家都知道,在golang中,一般的panic是可以被recover住的,我们线上服务用的是gin框架,用了一个Recover中间件来recover panic,但上面的这个panic显然没有被recover住

查看源代码go/src/runtime/map_fast64.go:61中具体内容

if h.flags&hashWriting != 0 {
		throw("concurrent map read and map write")
	}

这个61行的意思是,检测到map有写,直接thorw一个异常。

而我们的revoer是只能recover 是正常panic的语句的异常,所以这个throw是不被被recover的。

\

为什么跑了2年的代码,现在才出来这个错误

原来的代码就有问题,只不过,并发写map的那部份代码一直没有被执行到,导致跑了一年多没有问题。

最近在dao层的sql加了一个状态过滤,使得写map的代码分支符合执行条件,被执行了。这个坑被2年前那个童鞋埋的。2年后才触发。

我整理一下伪代码,大概如下

func getInfo(wids int[]) (map,error){
    cacheMap:=cacheUtil.GetCache(ctx)
    if cacheMap==nil{
      cacheMap=dao.GetFromDb(wids)
      return cacheMap,nil
   }
   excludeIds:=make([]int64,0)
   for _,wid :=range wids{
       if _,ok:=cacheMap[wid];!ok{
           excludeIds=append(excludeIds,wid)
       }
   }
  if len(excludeIds)==0{
       return cacheMap,nil
   }
   //此处开始写了,只要逻辑走到这里了,必须崩溃
   // 之前没有崩溃,是因为逻辑一直没有走到下面来
   appendMap:=dao.GetFromDb(excludeIds)
   for wid,v:=range appendMap {
	cacheMap[wid]=v
   }
    return cacheMap,nil
}

如何避免这类错误

  • 完善的单测,不是说写一个单测run起来就完事,而是要把主要核心逻辑都要测到

  • 在编写代码时,要时刻review自己的代码有没有并发问题

还有哪些错误不能被recover

其实可以把golang源码下载下来,搜索throw关键字,就知道哪些崩溃触发条件了

  • 并发读写map

  • lock

  • 死锁

  • 未加锁,就解锁

END

并不是所有的错误都能Recover,有的错误是叫崩溃,由throw关键字引发,不能被recover.