0%

cgo 骚操作踩坑了

上一篇 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
2
3
4
5
len = value.length();
go_strs[i].n = len;
go_strs[i].p = go_buf;
memcpy(go_buf, value.data(), len); // NOLINT(safe-memcpy)
go_buf += len;

这里是将 C++ 侧保存的 header map,copy 到 Golang 提前准备好的内存中

其中涉及到两个操作:

  1. 为 Golang string 的 p 指针和 n 长度赋值

  2. 将字符串内容 copy 到 go_buf 内存块中

问题就出现在第一步,因为 value 有可能为空,那么 go_buf 有可能就已经是非法的了,超出了预定的总长度

刚好,我们压测的这个 grpc 场景,Golang 的 grpc server 总是会返回

1
2
grpc-status: 0
grpc-messages

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 侧的优化空间,后面有空再继续搞了