对 dwarf 文件的优化

173 阅读2分钟

问题

对持续性能分析,需要通过 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)

image.png

为 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 的数据。

image.png