持续创作,加速成长!这是我参与「掘金日新计划 · 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.