string 字段多协程竞态
通常写代码比较注意一些数据结构、容器的多协程竞态,比如 slice 、 map
对于 string 字段的多协程竞态,非常容易忽视
这里举例说明,项目中遇到的问题
竞态代码
代码片段1 (协程1 中执行)
func (s *Server) loginOnWindows(p *common.Proto, ch *Channel) (err error) {
req := &stIphoneLoginUserCmd{}
if !req.Unmarshal(p.Body.Data()) {
err = errors.New("stIphoneLoginUserCmd Unmarshal fail")
return
}
// 此处有竞态问题
ch.Account = req.Account
ch.Token = req.LoginTempID
err = s.login(ch, uint32(req.Type), uint32(req.MobileType), req.Account)
return
}
代码片段2 (协程2 中执行)
func (s *Server) dispatchTCP(conn *net.TCPConn, ch *Channel) {
defer func() {
if err := recover(); err != nil {
glog.Errorln("[error] ", err, "\n", string(debug.Stack()))
conn.Close()
}
if ch != nil {
// 这里存在竞态问题
glog.Infof("account: %s start logout ...", ch.Account)
ch.Logout()
// 这里存在竞态问题
glog.Infof("account: %s dispatch goroutine exit", ch.Account)
}
}()
// 这里存在竞态问题
glog.Infof("account: %s start dispatch tcp goroutine", ch.Account)
// 其他代码略
//
}
竞态分析
原因很简单:
- 一个协程中对 ch.Account 字段赋值
- 一个协程中对 ch.Account 读取
代码列好,问题马上出来了
难点在于:
- 这种竞态问题太容易忽视了,排查问题时极可能忽略掉 ch.Account 字段
- panic 现场提示,容易带偏。见下面说明
如何复现
目前只有在极端压测中必现
表现为:
- 捕获并打印异常
- 提示
runtime error: invalid memory address or nil pointer dereference
- 提示
- 进程消失
- 捕获异常后,应继续打印 ch.Account 字段,造成 2 次 panic
由于提示是 nil 引用,因此很容易被带偏,通常思路会是这样:
- 编译器语法上 string 字段不可能为空,比如你使用 string字段!=nil 是编译不过的
- 因此会认为 ch.Account 字段不可能为空,所以会觉得堆栈信息不可思议
- 于是更多的判断会认为是哪里内存写坏掉了
其他说明
从首次发现,到实际解决该问题,至少 2 个月(当然都是在极端压测中出现)
可以见对于 string 字段的竞态问题,存在严重的认识不足
以上