这个坑比较新。周一刚装满,还开着空调。-1-在字节跳动,我们线上服务的所有日志都是通过统一的日志库收集到流式日志服务,落地到ES集群上,配备字节云超(sang)级(xin)强(bing))大(kuang)监控能力,每条paniclog都能触发对值班同学的呼叫。所以我们很多时候不选择手机,只选择飞书↓↓↓但是毕竟是恐慌,大多数情况都会在现场迅速整改,除了少数情况查起来费力不对线路有很大影响,比如这条:Error:invalidmemoryaddressornilpointerdereferenceTraceback:goroutine68532877[running]:...src/encoding/json/encode.go:880+0x59encoding/json.stringEncoder(0xcb9fead550,...)...src/encoding/json/encode.go:298+0xa5encoding/json.Marshal(0x1ecb9a0,...).../path/to/util.SendData(0xca813cd300)注意:对于阅读方便,略作简化。你看,recover就可以抓到(不会挂掉服务),而且出现的频率很低(一天几次甚至没有)。考虑到每天百亿请求的比例,解决它的ROI太大了。低,所以耽误了一段时间,不用担心被P0怪罪。-2-其实我和S之前都关注过这个panic。从上面的Errorlog可以看出错误是在调用json.Marshal时出现的,调用方的代码大概是这样的:funcSendData(...){data:=map[string]interface{}{"code":ctx.ErrorCode,"message":ctx.Message,"step":ctx.StepName,}msg,err:=json.Marshal(data)...}注:实际map有更多的key/value,这里稍微简化了。看到这段代码,第一反应是:这**也能panic吗?找到对应的json库源码(encode.go的第880行,对应下面第5行):func(e*encodeState)string(sstring,escapeHTMLbool){e.WriteByte('"')start:=0fori:=0;i/dev/null===================WARNING:DATARACEWriteat0x00c00011c1e0bygoroutine7:main.main.func1()poc.go:19+0x66(赋值行)Previousreadat0x00c00011c1e0bymaingoroutine:main.main()poc.go:28+0x9d(println行)这个可以看做作为一个真正的锤子。-7-那么为什么并发读写string会出现这种现象呢?这就得从string的底层数据结构说起。go的reflect包中有一个StringHeader类型,它对应了goruntime中string的表示:字符串)和长度。例如,我们可以这样玩StringHeader:s:="hello"p:=*(*reflect.StringHeader)(unsafe.Pointer(&s))fmt.Println(p.Len)对于这样的结构,golang不能保证原子性因此,可能会出现goroutine1刚刚修改了指针(Data),还没有来得及修改长度(Len),而goroutine2却读取到了字符串。所以我们看到了“WHAT”的输出——当s从“F*CK”变成“WHATTHE”时就是这种情况,Data变了,但是Len还没来得及变(还是等于4)。至于“F*CKGOGC”则刚好相反,明显是越界了,但是越界访问的地址还是在进程可以访问的地址空间中。-8-既然问题定位了,解决起来就很简单了。最直接的方法是使用sync.Mutex:func(ctx*Context)SetStep(stepstring){ctx.Mutex.Lock()deferctx.Mutex.Unlock()ctx.StepName=Step}但是Mutex性能不好enough(lockdoesnotscalingwiththenumberofprocessors),对于这种读写冲突概率较小的场景,性能更好的解决方案是将ctx.StepName的类型改为atomic.Value,然后ctx。StepName.Store(step)note:也可以改为*string然后使用atomic.StorePointer实际上,Golang不保证任何单个操作都是原子的,除非使用了atomic包中提供的原语或锁。-9-大结局:周一下午,H同学提交了修复代码并完成发布,这种恐慌就再也没有出现过。总结一下:String对人类和动物来说并不像看上去的那么无害。并发的坑可以求助-race。请记住使用互斥锁或原子锁。最后留个思考的小问题:这个说了很久,恐慌没有完全复现,不过文中已经给出了。工具够多了,你能想到做什么吗?推荐阅读:程序员面试指南:面试官视角踩坑:Go服务内存爆炸TCP:越学越不懂UTF-8:一些看似无用的冷知识【翻译】C程序员应该知道的内存知识(1)欢迎关注