Post content
考虑下面的简单 go 程序,其中 bpf.o 编译自任意一个简单的需要 CO-RE relocation 的 kprobe 此处就不展示了: func main() { spec, _ := ebpf.LoadCollectionSpec("bpf.o") coll, _ := ebpf.NewCollection(spec) coll.Close() coll = nil spec = nil runtime.GC() debug.FreeOSMemory() log.Fatal(http.ListenAndServe("localhost:6060", nil)) } 用 ps -e -o pid,rss,comm 很惊讶地发现这个进程在强制 gc 后依然占用了 50+M 的 RSS,泄!明明所有 stack var 都被我 close + nil 了! 用 go tool pprof http://127.0.0.1:6060/debug/pprof/heap?gc=1 看一下前五 heap objects: (pprof) top 5 flat flat% sum% cum cum% 18.17MB 38.00% 38.00% 22.68MB 47.42% github.com/cilium/ebpf/btf.readAndInflateTypes 17MB 35.55% 73.55% 17MB 35.55% github.com/cilium/ebpf/btf.indexTypes 4.50MB 9.42% 82.97% 4.50MB 9.42% github.com/cilium/ebpf/btf.readAndInflateTypes.func2 3MB 6.27% 89.24% 3MB 6.27% bufio.(*Scanner).Text (inline) 2.64MB 5.52% 94.76% 5.64MB 11.79% github.com/cilium/ebpf/btf.readStringTable 😅虽然不知道怎么回事,但貌似是 BTF 读进内存后就没 gc 回去,真是谢谢您了。 虽然 pprof/heap 提供这层信息已经很屌了,然而接下来的路依然不好走,我能想到的办法是用 tree 命令看调用栈,然后对每一层调用都用 list/web 仔细检查源码。 比如对于泄露了 20M 的 github.com/cilium/ebpf/btf.readAndInflateTypes, tree 可以看到其中一层栈是: (pprof) tree github.com/cilium/ebpf/btf.readAndInflateTypes [...] 22.68MB 100% | github.com/cilium/ebpf/btf.LoadKernelSpec 0 0% 47.42% 22.68MB 47.42% | github.com/cilium/ebpf/btf.loadKernelSpec 22.68MB 100% | github.com/cilium/ebpf/btf.loadRawSpec 然后用 weblist 检查 LoadKernelSpec 的源码 (pprof) weblist github.com/cilium/ebpf/btf.LoadKernelSpec 看到源码里 35 . . func LoadKernelSpec() (*Spec, error) { 36 . . kernelBTF.RLock() 37 . . spec := kernelBTF.kernel 38 . . kernelBTF.RUnlock() 39 . . 40 . . if spec == nil { 41 . . kernelBTF.Lock() 42 . . defer kernelBTF.Unlock() 43 . . 44 . . spec = kernelBTF.kernel 45 . . } 46 . . 47 . . if spec != nil { 48 . . return spec.Copy(), nil 49 . . } 50 . . 51 . 45.31MB spec, _, err := loadKernelSpec() 52 . . if err != nil { 53 . . return nil, err 54 . . } 55 . . 56 . . kernelBTF.kernel = spec 57 . . return spec.Copy(), nil 58 . . } 注意到 spec := loadKernelSpec() 这一步 alloc 了 45M 内存并返回,但是在返回之前给全局变量 kernelBTF.kernel = spec 设置了一个引用,正是这个引用导致了之后无法 gc 一大坨内存。喜欢吗 😀 以上步骤还是太苦了,尤其是还要人肉找到全局变量的引用,但凡瞎一点就错过了,任劳任怨的 LLM 此时就站了出来。 如果思路再打开一点,相信 golang gc 没有这么恶性的 bug 😀,一定是 lib 自己内部有全局变量拿了一份引用导致无法 gc,那么可以直接去看 elf 里的 bss/data segment 里长得像 cilium/ebpf/*btf* 的符号 $ objdump -t ./main | grep '\.bss\|\.data' | grep -i 'cilium/ebpf.*btf' [...] 0000000000afda20 g O .bss 0000000000000028 github.com/cilium/ebpf/btf.kernelBTF 也能直接找到这个全局变量。 但总得来说我还是觉得有点蛋疼,不知道有没有更好的方法和工具,请各位 golang experts 再次伸出⚪️手交交我