思考(五十七):一处 string 字段竞态问题引发的 crash

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 字段的竞态问题,存在严重的认识不足

以上

发布了129 篇原创文章 · 获赞 73 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/u013272009/article/details/93506820
今日推荐