上一篇 HTTP json 转 gRPC 中,我们提到了一起 Golang runtime panic,这篇就稍微展开介绍的
报错
单测的时候,运行倒是正常,跑压测的时候,很快就会出现 Golang runtime panic
比如:
1 | runtime: marked free object in span 0x7fef96df8658, elemsize=24 freeindex=83 (bad use of unsafe.Pointer? try -d=checkptr) |
这个是 Golang 的 GC 在 sweep 阶段,发现有一个内存块,标记被使用,也就是有一个指针指向这个内存块,但是,这个内存块同时也是被标记 free 的,不应该被使用的
简单说,就是有内存指针指向了 free 内存块,是个非法指针
原因
虽然 panic 的时候,会把所有 goroutine 的调用栈,都全部打印出来
但是,由于 panic 发生在 sweep 阶段,显然已经不是非法指针写入的时候了,调用栈并没有太多分析价值了
不过,好在我们是 demo 演示场景,代码本身比较简单,经过一番折腾,主要是 review 代码,发现坑在 copyHeaderMapToGo
中的这段:
1 | len = value.length(); |
这里是将 C++ 侧保存的 header map,copy 到 Golang 提前准备好的内存中
其中涉及到两个操作:
为 Golang string 的
p
指针和n
长度赋值将字符串内容 copy 到
go_buf
内存块中
问题就出现在第一步,因为 value 有可能为空,那么 go_buf
有可能就已经是非法的了,超出了预定的总长度
刚好,我们压测的这个 grpc 场景,Golang 的 grpc server 总是会返回
1 | grpc-status: 0 |
grpc-message
这个 trailer 的 value 总是为空,所以,压测场景很容易就 panic 了
知道原因,修复方案也简单,当 len 为 0 时,我们直接跳过即可
这是已经合并的 PR: https://github.com/envoyproxy/envoy/pull/35930
骚操作
在 C 里面,修改 Golang 指针的值,是 Golang 不建议的玩法,可以说是实锤的骚操作
不过,我们这么玩也是理论上安全的,只是需要非常小心,玩不好就踩坑了
至于,为什么我们选择这种骚操作,主要还是在保证内存安全的前提下,还提供不错的性能,具体可以看之前的 Envoy Go 扩展之内存安全
最开始实现的时候,只考虑了 Golang string 的使用场景,因为长度为 0,那么指针地址就算是非法,也是会被忽略的
但是,到了 GC 环节,GC 是不看类型的,只看指针引用,所以就悲剧了
彩蛋
顺便在 copyHeaderMapToGo
中还发现了一处优化点:
1 | auto value = std::string(header.value().getStringView()); |
这里的 std::string
是没有必要的,因为反正都要 copy 到 Golang 里面,C++ 侧再 copy 一次是没必要的了
于是,又有了这个 PR,简单场景压测了一把,Golang 的 header 初始化,可以有 10%+ 的提升,~400ns
当然,还有一些 Golang 侧的优化空间,后面有空再继续搞了