jfr to pprof 性能优化

最近需要将 jfr 转换成 pprof,发现转换太耗费性能了,遂做一些性能优化。

将一份 jfr 文件转成 lock,memory, cpu, wall 四种类型的 pprof 的开销:

初始性能:

bash 复制代码
Benchmark-8            2         833094945 ns/op        461013108 B/op   8881948 allocs/op

实现

代码中需要通过 java 的 class name 得到 filename,我们给它加上一层缓存。

go 复制代码
func (b *pprofBuilder) getFileName(s string) string {
   k := s
   if res, ok := b.classNameCache[k]; ok {
      return res
   }
   if i := strings.Index(s, "$"); i != -1 {
      s = s[:i]
   }
   res := s + ".java"
   b.classNameCache[k] = res
   return res
}

有一块需要将 jvm 的 name 抓换成类名, 比如 [C 需要转换成 char[]。

我们给 dimension 加上 cache。

go 复制代码
var dimensions = []string{"", "[]", "[][]", "[][][]"}

func getDimension(dimension int) string {
   if dimension < len(dimensions) {
      return dimensions[dimension]
   }
   return strings.Repeat("[]", dimension)
}

对切片进行复用:

css 复制代码
b.results = b.results[:0]

成果:

arduino 复制代码
// with classname cache and dimension cache and results cache
// Benchmark-8           10         668776975 ns/op        369239214 B/op   6147112 allocs/op

我们原先通过 classname + "." + methodName + args 的形式拼接成 java name。

转换成通过 bytes.buffer 尝试去减小分配:

css 复制代码
b.b.Reset()
b.b.WriteString(className)
b.b.WriteString(".")
b.b.WriteString(f.Method.Name.String)
if f.Method.Descriptor != nil {
   if args := b.parseArgs(f.Method.Descriptor.String); args != "()" {
      b.b.WriteString(args)
   }
}
name = b.b.String()

成果:

bash 复制代码
// use bytes.buffer to write it
// Benchmark-8           10         641657292 ns/op        338757497 B/op   5566944 allocs/op

java 中带有 args,我们需要做一层转换

arduino 复制代码
(ZJ)V -> (boolean, long)

同样的也做一层 cache:

bash 复制代码
Benchmark-8           10         493109508 ns/op        315108003 B/op   4101661 allocs/op

在生成 args 的时候需要使用字符串拼接:

vbnet 复制代码
 "(" + strings.Join(results, ",") + ")"

这里有两次内存分配,我们可以去除 + 带来的一次内存分配:

go 复制代码
func (b *pprofBuilder) Join(elems []string, sep string) string {
   switch len(elems) {
   case 0:
      return ""
   case 1:
      return elems[0]
   }
   n := len(sep) * (len(elems) - 1)
   for i := 0; i < len(elems); i++ {
      n += len(elems[i])
   }

   n += 2
   b.b.Reset()
   b.b.Grow(n)
   b.b.WriteString("(")
   b.b.WriteString(elems[0])
   for _, s := range elems[1:] {
      b.b.WriteString(sep)
      b.b.WriteString(s)
   }
   b.b.WriteString(")")
   return b.b.String()
}

但是由于 argscache 的存在,对性能影响不大,去除掉避免让代码逻辑变得复杂:

bash 复制代码
Benchmark-8           10         498650422 ns/op        312121734 B/op   4101807 allocs/op

我们之前使用 bytes.buffer 去拼接字符串,然后用于 map 中的 key:

ini 复制代码
name  = b.b.String()
if result, ok := b.functionTable[name]; ok {
   return result
}
 b.functionTable = name

事实上我们在 map 中寻找的时候,只需要一个临时的 string 去计算一个 hash 就行了,因此可以改写成这样:

ini 复制代码
name  = BytesToString(b.b.Bytes())
if result, ok := b.functionTable[name]; ok {
   return result
}
name  = b.b.String()
 b.functionTable = name

完成后:

bash 复制代码
Benchmark-8           10         438528324 ns/op        239495232 B/op   2975963 allocs/op

原先我们使用 func name + line number 作为 location 的 key:

go 复制代码
line64 := int64(line)
key := fun.Name + ":" + strconv.FormatInt(line64, 10)
if l, ok := b.locationTable[key]; ok {

但如果我们假设 line number 不会大于 10000 的话,那么 key 可以改为 uint64:

go 复制代码
key := fun.ID*10000 + uint64(line)
if l, ok := b.locationTable[key]; ok {
   return l
}

结果:

bash 复制代码
Benchmark-8           10         204552827 ns/op        34932547 B/op     459624 allocs/op

对于每一个调用栈, 我们要生成一个 key 用于确认唯一一个调用栈,跟生成 function key 时候的一样,我们也使用 unsafe 去获取 hash。 同时, 给 java 的 pid 套一层 cache:

go 复制代码
func (b *pprofBuilder) getPid(JavaThreadID int64) string {
   return b.pidCache[JavaThreadID]
}

结果:

bash 复制代码
Benchmark-8           10         161653003 ns/op         3604637 B/op      25385 allocs/op

随后,我们合并 github.com/grafana/jfr... 库的这个 mr:

减少内存分配:github.com/grafana/jfr...

总结

通过将内存分配减少为原先的 1%,我们成功的优化了 80% 的 cpu 消耗。

对比优化前后的内存分配次数火焰图,jfr to pprof 的部分几乎完全抹除了,剩下的部分是读取 jfr 文件本身。

对于这部分,有一个优化思路是将 api 变换成流式读取,这样可以去复用中间的结构体。

相关推荐
哎呦没1 小时前
英语知识网站开发:Spring Boot框架技巧
spring boot·后端·性能优化
顽疲1 小时前
springboot vue 开源 会员收银系统 (9) 库存管理 结算时扣库存
vue.js·spring boot·后端
2401_857600952 小时前
英语知识在线教学:Spring Boot网站构建
spring boot·后端·mfc
Object~2 小时前
【第十课】Rust并发编程(一)
开发语言·后端·rust
techdashen2 小时前
Go与黑客(第一部分)
开发语言·后端·golang
黄俊懿3 小时前
【深入理解SpringCloud微服务】Sentinel功能详解
后端·spring·spring cloud·微服务·中间件·架构·sentinel
运维&陈同学3 小时前
【zookeeper04】消息队列与微服务之zookeeper客户端访问
linux·后端·微服务·zookeeper·云原生·消息队列·云计算
2401_854391084 小时前
企业OA管理系统:Spring Boot技术架构与应用
spring boot·后端·架构
潜洋4 小时前
Spring Boot教程之七: Spring Boot –注释
java·spring boot·后端·注释
不能只会打代码4 小时前
深入讲解Spring Boot和Spring Cloud,外加图书管理系统实战!
spring boot·后端·spring cloud