首页游戏资讯golang并发读写string变量导致进程崩溃

golang并发读写string变量导致进程崩溃

misa2 03-06 5次浏览 4条评论

golang并发读写string变量导致进程崩溃

作者:arthurlhan,腾讯CDG使用开发工程师

日常保护服务中发现,整个集群每隔几天便会告警有一个进程挂掉。这个问题发生的频率比较低,脚本也配置了进程自动重新拉起,所以影响不大,不过这里还是想探究下具体原因。

看察挂掉的进程,在挂掉前,cpu、日志报错、磁盘、内存等看起来都没有反常,发生时间和ip也不固定。

依据之前的排查体会,启动脚本加进两行命令,设置进程挂掉时自动生成core文件。

# 不限制core文件大小ulimit -c unlimited# 设置环境变量GOTRACEBACK为crash级别export GOTRACEBACK=crash

收到报警后,从容器下载下core文件,然后用delve这个工具分析core文件,可以拿到报错堆栈,一般拿到这个就基本可以定位。依据之前体会,常见原因有并发读写map等。

# exe_file 进程文件 core_file core文件dlv core ${exe_file} ${core_file}# 打印堆栈bt

打印出来的报错堆栈如下

0 0x0000000000482de1 in runtime.raise at /usr/local/go/src/runtime/sys_linux_amd64.s:164 1 0x000000000045ea1d in runtime.dieFromSignal at /usr/local/go/src/runtime/signal_unix.go:768 2 0x000000000045f071 in runtime.sigfwdgo at /usr/local/go/src/runtime/signal_unix.go:982 3 0x000000000045d894 in runtime.sigtrampgo at /usr/local/go/src/runtime/signal_unix.go:416 4 0x0000000000483163 in runtime.sigtramp at /usr/local/go/src/runtime/sys_linux_amd64.s:399 5 0x00007fadea96a630 in ??? at ?:-1 6 0x00000000004473ac in runtime.crash at /usr/local/go/src/runtime/signal_unix.go:860 7 0x00000000004473ac in runtime.fatalpanic at /usr/local/go/src/runtime/panic.go:1217 8 0x0000000000446ce5 in runtime.gopanic at /usr/local/go/src/runtime/panic.go:1065 9 0x00000000004449fb in runtime.panicmem at /usr/local/go/src/runtime/panic.go:21210 0x000000000045e853 in runtime.sigpanic at /usr/local/go/src/runtime/signal_unix.go:73411 0x00000000004824e7 in runtime.memmove at /usr/local/go/src/runtime/memmove_amd64.s:17912 0x000000000050654e in fmt.(*buffer).writeString at /usr/local/go/src/fmt/print.go:8213 0x000000000050654e in fmt.(*fmt).padString at /usr/local/go/src/fmt/format.go:11014 0x00000000005074a5 in fmt.(*fmt).fmtS at /usr/local/go/src/fmt/format.go:35915 0x000000000050a951 in fmt.(*pp).fmtString at /usr/local/go/src/fmt/print.go:44316 0x000000000050f056 in fmt.(*pp).printValue at /usr/local/go/src/fmt/print.go:75717 0x000000000050e9a8 in fmt.(*pp).printValue at /usr/local/go/src/fmt/print.go:80618 0x000000000050e9a8 in fmt.(*pp).printValue at /usr/local/go/src/fmt/print.go:80619 0x000000000050e797 in fmt.(*pp).printValue at /usr/local/go/src/fmt/print.go:87620 0x000000000050c813 in fmt.(*pp).printArg at /usr/local/go/src/fmt/print.go:71221 0x000000000050fe48 in fmt.(*pp).doPrintf at /usr/local/go/src/fmt/print.go:102622 0x0000000000509186 in fmt.Sprintf at /usr/local/go/src/fmt/print.go:21923 0x0000000000cf96e6 in xxx.xxx.xxx.com/xxx/xxx/xxx/log.(*ZapLog).Infof at /tmp/xxx/xx/log/zaplogger.go:274

展开全文

具体报错的业务代码其实十分简单,就是单纯打印下一个变量value,初看起来没啥报错的可能,即便为nil也没啥问题。

log.InfoContextf(ctx, "XXXXX_Value: %+v", value)

接着往底层看,log.InfoContextf底层实际就是 fmt.Sprintf。

最底层的错误是SIGSEGV段错误,也就是程序试图访问不被答应访问的内存区域,一般原因有空指针、数组越界等。

找到非错误处理的最后一处代码,代码很简单,就是append下string对应的byte数组。

func (b *buffer) writeString(s string) { *b = append(*b, s...)}

这里的变量b打印了下,发现并不为空,这里变量的s无法打印出来,不过这应该是生成core文件的问题,确实有也有其他变量无法打印。

排查到这里就比较困惑了,b不为空,s又是基本类型,不会为空,看不出啥场景基本类型会报错。

不过看了下业务逻辑,这个字符串会有并发读写的情状,初步怀疑跟并发有关。

初步检索了下,发现确实有一些但不太多的讨论,golang的string在并发情状下是不安全的。官方的回应也是不能保证并发情状下程序的行为,假如没有做并发掌握而往访问共享变量。

确实正确的方式是不要并发使用,不过这里想深究一下原因,为什么会导致进程崩溃。

陆续检索过程中,发现一个比较要害的点是string在运行时是一个结构体,存放着指向数据的指针Data和长度Len.

源码见reflect/value.go,go版本是16.9。

// StringHeader is the runtime representation of a string.// It cannot be used safely or portably and its representation may// change in a later release.// Moreover, the Data field is not sufficient to guarantee the data// it references will not be garbage collected, so programs must keep// a separate, correctly typed pointer to the underlying data.type StringHeader struct { Data uintptr Len int}

字符串的赋值实际是分为两个部分,指针和长度,假如并发写进,可能导致导致指向的内容和长度不匹配,比如这个指针是0x0,也就是指向nil,而长度不为空。

不过我看了下我这里的场景,这个变量都是从空值变成长度大于0的字符串,而且结果是同样的字符串,假如是先赋值指针,再赋值长度(Len从0变成一个大于0的值),理论上也应该不会报错,因为也没有越界。

这里写了一份报错demo代码,一个协程一直不停打印变量,另起协程不断修改变量值。

package mainimport ( "fmt" "time")type SampleStruct struct { Value string}func main { a := SampleStruct{ Value: "", } go func { for { fmt.Printf("%+v %v", a.Value, time.Now.Unix) } } for { go func { a.Value = "" } time.Sleep(time.Microsecond) go func { a.Value = "value" } time.Sleep(time.Microsecond) }}

跑一段时间后会panic,准则输出如下

panic: runtime error: invalid memory address or nil pointer dereference[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x465282]goroutine 18 [running]:fmt.(*buffer).writeString(...) /usr/local/go/src/fmt/print.go:82fmt.(*fmt).padString(0xc00010a110, 0x0, 0x5) /usr/local/go/src/fmt/format.go:110 +0x8efmt.(*fmt).fmtS(0xc00010a110, 0x0, 0x5) /usr/local/go/src/fmt/format.go:359 +0x65fmt.(*pp).fmtString(0xc00010a0d0, 0x0, 0x5, 0x76) /usr/local/go/src/fmt/print.go:443 +0x131fmt.(*pp).printArg(0xc00010a0d0, 0x4a47e0, 0xc0004a30c0, 0x76) /usr/local/go/src/fmt/print.go:694 +0x875fmt.(*pp).doPrintf(0xc00010a0d0, 0x4bcd72, 0x6, 0xc000100fa8, 0x2, 0x2) /usr/local/go/src/fmt/print.go:1026 +0x168fmt.Fprintf(0x4dc418, 0xc000182008, 0x4bcd72, 0x6, 0xc000100fa8, 0x2, 0x2, 0x10, 0x0, 0x0) /usr/local/go/src/fmt/print.go:204 +0x72fmt.Printf(...) /usr/local/go/src/fmt/print.go:213main.main.func1(0xc00018a050)

这里参考博客,打印并分析了下汇编代码。

go tool compile -S -N -l main.go

第25行代码a.Value = ""

"".main.func2 STEXT size=87 args=0x8 locals=0x8 funcid=0x0 0x0000 00000 (main.go:24) TEXT "".main.func2(SB), ABIInternal, $8-8 0x0000 00000 (main.go:24) MOVQ (TLS), CX 0x0009 00009 (main.go:24) CMPQ SP, 16(CX) 0x000d 00013 (main.go:24) PCDATA $0, $-2 0x000d 00013 (main.go:24) JLS 80 0x000f 00015 (main.go:24) PCDATA $0, $-1 0x000f 00015 (main.go:24) SUBQ $8, SP 0x0013 00019 (main.go:24) MOVQ BP, (SP) 0x0017 00023 (main.go:24) LEAQ (SP), BP 0x001b 00027 (main.go:24) FUNCDATA $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB) 0x001b 00027 (main.go:24) FUNCDATA $1, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x001b 00027 (main.go:25) MOVQ "".a+16(SP), DI 0x0020 00032 (main.go:25) TESTB AL, (DI) 0x0022 00034 (main.go:25) MOVQ $0, 8(DI) 0x002a 00042 (main.go:25) PCDATA $0, $-2 0x002a 00042 (main.go:25) CMPL runtime.writeBarrier(SB), $0 0x0031 00049 (main.go:25) JEQ 53 0x0033 00051 (main.go:25) JMP 71 0x0035 00053 (main.go:25) MOVQ $0, (DI) 0x003c 00060 (main.go:25) JMP 62 0x003e 00062 (main.go:26) PCDATA $0, $-1 0x003e 00062 (main.go:26) MOVQ (SP), BP 0x0042 00066 (main.go:26) ADDQ $8, SP 0x0046 00070 (main.go:26) RET 0x0047 00071 (main.go:25) PCDATA $0, $-2 0x0047 00071 (main.go:25) XORL AX, AX 0x0049 00073 (main.go:25) CALL runtime.gcWriteBarrier(SB) 0x004e 00078 (main.go:25) JMP 62 0x0050 00080 (main.go:25) NOP 0x0050 00080 (main.go:24) PCDATA $1, $-1 0x0050 00080 (main.go:24) PCDATA $0, $-2 0x0050 00080 (main.go:24) CALL runtime.morestack_noctxt(SB) 0x0055 00085 (main.go:24) PCDATA $0, $-1 0x0055 00085 (main.go:24) JMP 0

这里摘出要害两行

# 将数0存进DI地址+8,即把长度存进Len0x0022 00034 (main.go:25) MOVQ $0, 8(DI)# 将数0存进DI地址,即指向字符串内容的指针为0,也就是nil0x0035 00053 (main.go:25) MOVQ $0, (DI)

第28行代码a.Value = "value"

"".main.func3 STEXT size=98 args=0x8 locals=0x8 funcid=0x0 0x0000 00000 (main.go:28) TEXT "".main.func3(SB), ABIInternal, $8-8 0x0000 00000 (main.go:28) MOVQ (TLS), CX 0x0009 00009 (main.go:28) CMPQ SP, 16(CX) 0x000d 00013 (main.go:28) PCDATA $0, $-2 0x000d 00013 (main.go:28) JLS 89 0x000f 00015 (main.go:28) PCDATA $0, $-1 0x000f 00015 (main.go:28) SUBQ $8, SP 0x0013 00019 (main.go:28) MOVQ BP, (SP) 0x0017 00023 (main.go:28) LEAQ (SP), BP 0x001b 00027 (main.go:28) FUNCDATA $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB) 0x001b 00027 (main.go:28) FUNCDATA $1, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x001b 00027 (main.go:29) MOVQ "".a+16(SP), DI 0x0020 00032 (main.go:29) TESTB AL, (DI) 0x0022 00034 (main.go:29) MOVQ $5, 8(DI) 0x002a 00042 (main.go:29) PCDATA $0, $-2 0x002a 00042 (main.go:29) CMPL runtime.writeBarrier(SB), $0 0x0031 00049 (main.go:29) JEQ 53 0x0033 00051 (main.go:29) JMP 75 0x0035 00053 (main.go:29) LEAQ go.string."value"(SB), AX 0x003c 00060 (main.go:29) MOVQ AX, (DI) 0x003f 00063 (main.go:29) NOP 0x0040 00064 (main.go:29) JMP 66 0x0042 00066 (main.go:30) PCDATA $0, $-1 0x0042 00066 (main.go:30) MOVQ (SP), BP 0x0046 00070 (main.go:30) ADDQ $8, SP 0x004a 00074 (main.go:30) RET 0x004b 00075 (main.go:29) PCDATA $0, $-2 0x004b 00075 (main.go:29) LEAQ go.string."value"(SB), AX 0x0052 00082 (main.go:29) CALL runtime.gcWriteBarrier(SB) 0x0057 00087 (main.go:29) JMP 66 0x0059 00089 (main.go:29) NOP 0x0059 00089 (main.go:28) PCDATA $1, $-1 0x0059 00089 (main.go:28) PCDATA $0, $-2 0x0059 00089 (main.go:28) CALL runtime.morestack_noctxt(SB) 0x005e 00094 (main.go:28) PCDATA $0, $-1 0x005e 00094 (main.go:28) NOP 0x0060 00096 (main.go:28) JMP 0

这里也摘出要害几行

# 将数5存进DI地址+8,也就是"value"的长度5存进Len0x0022 00034 (main.go:29) MOVQ $5, 8(DI)# 将指向"value"的地址存进AX0x0035 00053 (main.go:29) LEAQ go.string."value"(SB), AX# 再将AX的地址存进DI0x003c 00060 (main.go:29) MOVQ AX, (DI)

所以从这汇编代码可以看出是先赋值长度,然后再赋值指向内容的指针。(PS: 不能保证一定是这个顺序,假如换一种写法,也可以是先赋值指针,再赋值长度)。

所以问题就是这个string变量SampleStruct.Value,Data和Len的初始值均为0,当协程A先赋值Len为一个大于0的值,还未来得及赋值Data为对应字符串的指针地址时,此时协程B往打印,就会发现指向的是空指针,但长度却不为空,从而数据越界发生段错误。

这点也可以从准则输出看出,指向的是空指针 (0x0),但长度不为0 (0x5)。

fmt.(*fmt).padString(0xc00010a110, 0x0, 0x5)

总结来说string变量是一个结构体,它的赋值分为两步,赋值长度和赋值指向内容的指针,不是原子性的,并发情状下可能会出现问题。

bt下载
昔日坐拥1.6亿用户的影视平台,为何走向衰落? 又摊上大事!知名APP突遭集体起诉
相关内容
发表评论

游客 回复需填写必要信息
匿名用户
BT下载高效便捷,资源丰富多样,支持多线程加速传输速度超快!安全可靠、操作简单易用深受用户喜爱和好评的必备工具之一!
匿名用户2024-07-11 04:06:02回复
匿名用户
BT下载方式已成为高速度大规模分享的重要途径之一,极大地改变了人们资源共享的习惯。
匿名用户2024-07-11 04:06:25回复
匿名用户
BT下载功能强大,速度快且资源丰富,高效的文件分享和传输工具的优秀选择!操作简单方便易用值得推荐的一款软件啊~
匿名用户2024-07-11 04:07:02回复
匿名用户
BT下载迅如疾风,简易又能供馗之本小片连网络小说广泛吸极力求的表现网站与传统#folder上天房企还要的特点已经被ID发作不再是idiamas应急愿所在地楼房工学keep (坚持不懈可达玲布朗TXT开发商还想更远)。
匿名用户2024-07-11 04:08:03回复
匿名用户
BT下载快速便捷,资源丰富多样,网络共享的强大工具!
匿名用户2024-07-11 04:08:26回复