问题
对持续性能分析,需要通过 dwarf 文件获取函数名,行号,文件名,内联函数名等信息。
在实践中发现,一个 16 core 32 GB 的机器大概能处理 50 个 agent。
性能消耗过大。
内存 oom
原先,对 dwarf 文件的假设是 缓存率很高。
因此,会将以下数据,缓存在内存中:
- line entry。(用于查找行号)
- abstract program。(用于查找内联函数函数名)
然而, line entry 十分庞大,非常容易让程序 oom。
(作为例子,一个 100mb 的 go 可执行,含有 300w line entry)
火焰图分析
在不缓存 lineEntry 时,程序火焰图如下:
- findLineInfo 23%(查找行号)
- buildAbstraceSubProgram 27% (构建内联函数表)
- zlib.Read 17% (解压缩 dwarf 数据)
- dwarf.SeekPC 5% (查找 pc 对应的 complication unit)
为 dwarf 文件构建索引
实际上,dwarf 文件缓存率低,因此不适合将 line entry 和 abstract program 缓存在内存中。
因此,我们在磁盘上,为 dwarf 文件构建索引,通过 mmap 的方式使用,避免了每次读取所需的 cpu 开销。
比如,abstract program 本质是一个 map[uint32]string。
key 为 dwarf offset, value 为函数名。
所以我们在磁盘上的文件格式为:
// list of abstract subProgram program form:
// dwarf offset uint32
// offset of name data uint32
// sorted by dwarf offset
//
// list of abstract subprogram name:
// length of name uint8
// name string
//
// offset of list of abstract subProgram program
// offset of list of abstract subprogram name
通过 offset 之差, 可以计算出 abstract 的数量:
const (
abstractSubprogramEntrySize = 4 + 4
)
ix.numAbstractProgram = (ix.abstractProgramDataOffset - ix.abstractProgramIndexOffset) / abstractSubprogramEntrySize
然后,就可以通过二分搜索,在文件上实现类似 map 的功能。
- abstractProgramIndex: 通过 mmap 形式读取的 []byte,包含 dwarf offset, 以及对应函数名在 abstraceProgramData 上的偏移量。
- abstraceProgramData: 通过 mmap 形式读取的 []byte, 包含函数名长度以及函数名。
func (ix *Index) GetAbstractName(off uint32) (string, error) {
n := sort.Search(int(ix.numAbstractProgram), func(i int) bool {
addr := binary.BigEndian.Uint32(ix.abstractProgramIndex[i*abstractSubprogramEntrySize:])
return addr > off
})
if n == 0 || binary.BigEndian.Uint32(ix.abstractProgramIndex[(n-1)*abstractSubprogramEntrySize:]) != uint32(off) {
return "", errors.New("failed to find abstract name")
}
i := n - 1
o := binary.BigEndian.Uint32(ix.abstractProgramIndex[i*abstractSubprogramEntrySize+4:])
nameLen := int(ix.abstractProgramData[o])
return string(ix.abstractProgramData[o+1 : int(o+1)+nameLen]), nil
}
提前解压缩 dwarf 文件
每次读取 dwarf 文件,都需要进行解压缩,这导致了许多的 cpu 开销。
因此,我们直接将生成 dwarf 所需的所有数据,从 elf 文件中读取,然后直接写入磁盘。
预排序地址
通过将 addr 提前排序,我们可以复用 dwarf.Reader 来查找 complication unit。
r := f.debugData.Reader()
for _, addr := range addrs {
var found bool
for _, pcs := range ranges {
if pcs[0] <= addr && addr < pcs[1] {
found = true
break
}
}
if !found {
// new complication unit
cu, err = r.SeekPC(addr)
ranges, _ = f.debugData.Ranges(cu)
}
结果
优化后,我们几乎将服务处理性能提升了十倍,也就是一台 16 core 的机器可以处理 300 个 agent 的数据。