前言

golang在官方的 FAQ 提到过map不是线程安全的,如果有并发场景需要自己加锁,或者使用sync包里的Map。

这本是众所周知的问题,但是本文的重点是记录一个压测过程中进程panic问题,panic的报错信息是map的并发读写和并发写的情况,但是一波分析之后,原因并不出在map上,而是一个slice的操作问题。

压测背景

压测的服务是一个http服务,用的一个github上的go-restful库,没有使用gin这些框架。

panic报错信息:

分析

从日志上可以明显的看到确实是有两个运行的状态的协程同时操作了同一个map,导致map双写,然后panic

runtime.mapassign_faststr(0x2ab5d20, 0xc01c8c1aa0, 0x2dea8f5, 0x6, 0x2)

但是问题在于,这个map是go-restful库对每一个请求都新建的一个结构体对象,当请求到来的时候http会为每一个请求创建一个协程,所以每个map都是在同一个协程里创建,正常来说不会出现并发的问题,但是寄存器地址明显显示是同一个map,说明问题不在go-restful这个库,继续往下看对栈信息。

继续往下可以看到 Invocation2HTTPRequest 这个函数调用,第二个参数是一样的,离事实更近一步了

对go-chassis这个库不熟悉的朋友对这里的 Invocation2HTTPRequest 函数调用应该不太熟悉,跟gin类似,

go-chassis这个库会对每一个请求生成一个handler链,一个请求过来,都会复制条默认的handler链,然后把自己的请求信息作为最后一个handler,追加到默认的handler链上。

想到这里,翻一翻go-chassis这个链路的实现:

newHandler(handleFunc, bs, opts)originChain
*c = *originChainhandler.Chain{}
Handlers

可以还原一下当时的场景:

*c = *originChain
DATA RACE
(len: 1, cap: 1) --> (len: 2, cap: 2) --> (len: 3, cap: 4)

而最后一个handler中刚好就是有日志中打印的map,A、B协程并发写,程序panic,报错日志中其他panic是map的并发读写,根因必然也是如此了。

结论

slice共享变量的拷贝和append操作线程不安全,导致map被多个协程操作,引发panic。

回顾整个过程,golang的map并发读写造成的原因可能有很多,但是并发问题一定是有变量被共享了,多个协程一起操作,只要基于这个原则,顺着堆栈,根据代码找到泄漏的地方就可以。