golang map的并发读写导致panic
前言
golang在官方的 FAQ 提到过map不是线程安全的,如果有并发场景需要自己加锁,或者使用sync包里的Map。
这本是众所周知的问题,但是本文的重点是记录一个压测过程中进程panic问题,panic的报错信息是map的并发读写和并发写的情况,但是一波分析之后,原因并不出在map上,而是一个slice的操作问题。
压测背景
压测的服务是一个http服务,用的一个github上的go-restful库,没有使用gin这些框架。
panic报错信息:
fatal error: concurrent map writes
goroutine 518470 [running]:
runtime.throw(0x2e063c3, 0x15)
/usr/local/go/src/runtime/panic.go:617 +0x72 fp=0xc01c8251a8 sp=0xc01c825178 pc=0xef8962
runtime.mapassign_faststr(0x2ab5d20, 0xc01c8c1aa0, 0x2dea8f5, 0x6, 0x2)
/usr/local/go/src/runtime/map_faststr.go:291 +0x40f fp=0xc01c825210 sp=0xc01c8251a8 pc=0xede0bf
github.com/emicklei/go-restful.(*Request).SetAttribute(...)
/root/go/pkg/mod/github.com/emicklei/go-restful@v2.12.0+incompatible/request.go:107
github.com/go-chassis/go-chassis/server/restful.Invocation2HTTPRequest(0xc020183e60, 0xc01c8c19e0)
...
net/http.(*conn).serve(0xc0201f2e60, 0x31e02e0, 0xc01ca3f180)
/usr/local/go/src/net/http/server.go:1878 +0x851 fp=0xc01c825fc8 sp=0xc01c825d20 pc=0x11c5f81
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc01c825fd0 sp=0xc01c825fc8 pc=0xf28eb1
created by net/http.(*Server).Serve
/usr/local/go/src/net/http/server.go:2884 +0x2f4
goroutine 518462 [running]:
runtime.throw(0x2e063c3, 0x15)
/usr/local/go/src/runtime/panic.go:617 +0x72 fp=0xc01ff4b1a8 sp=0xc01ff4b178 pc=0xef8962
runtime.mapassign_faststr(0x2ab5d20, 0xc01c8c1aa0, 0x2dea8f5, 0x6, 0x2)
/usr/local/go/src/runtime/map_faststr.go:291 +0x40f fp=0xc01ff4b210 sp=0xc01ff4b1a8 pc=0xede0bf
github.com/emicklei/go-restful.(*Request).SetAttribute(...)
/root/go/pkg/mod/github.com/emicklei/go-restful@v2.12.0+incompatible/request.go:107
github.com/go-chassis/go-chassis/server/restful.Invocation2HTTPRequest(0xc020334d80, 0xc01c8c19e0)
...
net/http.(*conn).serve(0xc0201bc3c0, 0x31e02e0, 0xc020016ec0)
/usr/local/go/src/net/http/server.go:1878 +0x851 fp=0xc01ff4bfc8 sp=0xc01ff4bd20 pc=0x11c5f81
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc01ff4bfd0 sp=0xc01ff4bfc8 pc=0xf28eb1
created by net/http.(*Server).Serve
/usr/local/go/src/net/http/server.go:2884 +0x2f4
分析
从日志上可以明显的看到确实是有两个运行的状态的协程同时操作了同一个map,导致map双写,然后panic
runtime.mapassign_faststr(0x2ab5d20, 0xc01c8c1aa0, 0x2dea8f5, 0x6, 0x2)
但是问题在于,这个map是go-restful库对每一个请求都新建的一个结构体对象,当请求到来的时候http会为每一个请求创建一个协程,所以每个map都是在同一个协程里创建,正常来说不会出现并发的问题,但是寄存器地址明显显示是同一个map,说明问题不在go-restful这个库,继续往下看对栈信息。
继续往下可以看到 Invocation2HTTPRequest 这个函数调用,第二个参数是一样的,离事实更近一步了
Invocation2HTTPRequest(0xc020183e60, 0xc01c8c19e0)
Invocation2HTTPRequest(0xc020334d80, 0xc01c8c19e0)
对go-chassis这个库不熟悉的朋友对这里的 Invocation2HTTPRequest 函数调用应该不太熟悉,跟gin类似,
go-chassis这个库会对每一个请求生成一个handler链,一个请求过来,都会复制条默认的handler链,然后把自己的请求信息作为最后一个handler,追加到默认的handler链上。
想到这里,翻一翻go-chassis这个链路的实现:
...
bs := NewBaseServer(inv.Ctx)
bs.Req = req
bs.Resp = resp
//create a new chain for each resource handler
c := &handler.Chain{}
*c = *originChain
c.AddHandler(newHandler(handleFunc, bs, opts))
...
newHandler(handleFunc, bs, opts)
所做的事情就是为每个请求包装一个handler,然后追加到共享变量 originChain
的拷贝对象中去。
看到了一丝怪异, *c = *originChain
这个拷贝动作仿佛有点问题,点开 handler.Chain{}
结构体:
// Chain struct for service and handlers
type Chain struct {
ServiceType string
Name string
Handlers []Handler
}
看到 Handlers
这个切片,果然,问题就是出在这里了。
可以还原一下当时的场景:
A、B请求同时走到 *c = *originChain
这时候,两个hanlder链点拷贝体中Hnadlers还指向同一个slice地址,指向的底层数组还是同一个,此时两个协程都进行append操作,在一种情况下,存在后一个append操作把前一个append操作覆盖的现象。
因为slice的增长是有规律的,当cap小于1024,那么每次增长,cap都变成之前的2倍,当大于1024,那么每次增长就只是1.25倍。当一个slice的cap大于len的时候,底层数组有空余的地址可以写入即将append的那个元素。那么这时候并发append就会有 DATA RACE
的情况发生。
回头继续看压测的情况,当时handler链是从0开始append,压测中的originChain会append三次,那么slice的变化就是:(len: 1, cap: 1) --> (len: 2, cap: 2) --> (len: 3, cap: 4)
, 第四个位置就有可能发生并发写。假设A请求的append操作在B请求之后,A覆盖B,两条复制的handler链就会变成:
A请求handler链: [hanlder1, handler2, handler3, handlerA]
B请求handler链: [hanlder1, handler2, handler3, handlerA]
而最后一个handler中刚好就是有日志中打印的map,A、B协程并发写,程序panic,报错日志中其他panic是map的并发读写,根因必然也是如此了。
结论
slice共享变量的拷贝和append操作线程不安全,导致map被多个协程操作,引发panic。
回顾整个过程,golang的map并发读写造成的原因可能有很多,但是并发问题一定是有变量被共享了,多个协程一起操作,只要基于这个原则,顺着堆栈,根据代码找到泄漏的地方就可以。
- Java多线程之Runable与Thread
- 关于 devbridge-autocomplete 插件多选操作的实现方法
- node-sass 安装失败的解决措施
- JavaMelody监控SQL
- 关于jboss的线程问题+java.lang.outofmemoryError
- 《了不起的 nodejs》中 TwitterWeb 案例 bug 解决
- java.lang.ClassNotFoundException与java.lang.NoClassDefFoundError的区别
- 【java开发系列】—— Tomcat编译报错
- java.lang.NoClassDefFoundError: org/aopalliance/aop/Advice
- 《像计算机科学家一样思考Java》—— 读后总结
- 记录安装oracle的那些事(三)之oracle Database R2安装
- Elasticsearch Javascript API增删改查
- Oracle二三事之 Oracle SPARC SuperCluster的九大技术优势
- 两个 viewports 的故事-第二部分
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- python如何快速生成时间戳
- 从python读取sql的实例方法
- PHP+iframe模拟Ajax上传文件功能示例
- Centos7 Yum安装PHP7.2流程教程详解
- PHP session垃圾回收机制实例分析
- thinkphp5框架调用其它控制器方法 实现自定义跳转界面功能示例
- Python常用库Numpy进行矩阵运算详解
- PHP设计模式之建造者模式(Builder)原理与用法案例详解
- PHP大文件切割上传并带进度条功能示例
- PHP设计模式之观察者模式入门与应用案例详解
- Python文件夹批处理操作代码实例
- ThinkPHP框架结合Ajax实现用户名校验功能示例
- Laravel框架Eloquent ORM新增数据、自定义时间戳及批量赋值用法详解
- PHP使用HTML5 FileApi实现Ajax上传文件功能示例
- python3.7调试的实例方法