Golang FlameGraph(火焰图)

混江龙づ霸主 提交于 2019-12-24 14:48:03

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

简介

初学golang(一个月多),之前主要用其他语言,如有问题欢迎指出。

安装

go get github.com/uber/go-torch
# 再安装 brendangregg/FlameGraph 
export PATH=$PATH:/absolute/path/FlameGraph-master
# 还需要安装一个graphviz用来画内存图
yum install graphviz

代码修改

import "net/http"
import _ "net/http/pprof"
func main() {
    // 主函数中添加
    go func() {
		http.HandleFunc("/program/html", htmlHandler) // 用来查看自定义的内容
		log.Println(http.ListenAndServe("0.0.0.0:8080", nil))
	}()
}

使用

# 用 -u 分析CPU使用情况
./go-torch -u http://127.0.0.1:8080
# 用 -alloc_space 来分析内存的临时分配情况
./go-torch -alloc_space http://127.0.0.1:8080/debug/pprof/heap --colors=mem
# 用 -inuse_space 来分析程序常驻内存的占用情况;
./go-torch -inuse_space http://127.0.0.1:8080/debug/pprof/heap --colors=mem
# 画出内存分配图
go tool pprof -alloc_space -cum -svg http://127.0.0.1:8080/debug/pprof/heap > heap.svg

查看

使用浏览器查看svg文件,程序运行中,可以登录 http://127.0.0.1:10086/debug/pprof/ 查看程序实时状态

在此基础上,可以通过配置handle来实现自定义的内容查看,可以添加Html格式的输出,优化显示效果

func writeBuf(buffer *bytes.Buffer, format string, a ...interface{}) {
	(*buffer).WriteString(fmt.Sprintf(format, a...))
}
func htmlHandler(w http.ResponseWriter, req *http.Request) {
	io.WriteString(w, statusHtml())
}
// 访问 localhost:8080/program/html 可以看到一个表格,一秒钟刷新一次
func statusHtml() string {
	var buf bytes.Buffer
	buf.WriteString("<html><meta http-equiv=\"refresh\" content=\"1\">" +
		"<body><h2>netflow-decoder status count</h2>" +
		"<table width=\"500px\" border=\"1\" cellpadding=\"5\" cellspacing=\"1\">" +
		"<tr><th>NAME</th><th>TOTAL</th><th>SPEED</th></tr>")
	writeBuf(&buf, "<tr><td>UDP</td><td>%d</td><td>%d</td></tr>",
		total, speed)
	...
	buf.WriteString("</table></body></html>")
	return buf.String()
} 

火焰图效果

输入图片说明

火焰图自下而上是函数的调用关系,底下的一个方块是入口,对应其上面的方块是他直接或者间接调用到的,长度是运行时所占用的CPU时长,颜色没有特别的意义

pprof内存分配图效果

输入图片说明

从上到下是调用关系,如箭头所示,表示给每个函数【累计】分配了多少内存,包括它自己占用多少以及向下调用时分配了多少。从这个就可以看出程序中哪个地方最消耗内存,最底下没有名字的方块是这个函数内,每次向系统申请内存的大小

实际图片是svg格式的,可以无限方法,这里只是看个大概(人为打码)。

调优实践

先说一下结论吧,性能限制主要是IO相关的,比如网络数据收发、磁盘读写等,在程序复杂度并没有那么高的情况下,调优只是锦上添花,主要可以帮助自己更好的了解这个语言。 以下调优的部分主要是针对项目中,从github上引入的部分代码

CPU使用调优

结果图

先来看看前后的对比图:

输入图片说明

调优前,两个蓝色方框中的函数分别是StringDefaultRead,前者的作用是把二进制表示的数值转为对应大小的字符串([]byte -> int -> string),后者是将二进制读到指定的位置,这两个函数占用了40%+的时间。优化后如下:

输入图片说明

由于对前面两个部分的优化,占用的时间已经大大缩小,从70%左右下降到40%+。后面没有做处理的网络读写net.Write占了很多时间

调优过程

首先针对字符串处理部分,从第一个火焰图点击StringDefault可以看到细节,如下图:

输入图片说明

里面有很多的readnewobject,从函数功能上我们可以知道,转换一次字符串并不需要这么麻烦,来看看代码是这么写的:

// 原来的写法,以双字节为例
// 先申请临时变量,将字节数组转为buffer(多余且费时费内存),再读取到临时变量中,再进行类型转换
var n uint16
binary.Read(bytes.NewBuffer(b), binary.BigEndian, &n)
return strconv.Itoa(int(n)) 
// 精简后:直接讲字节数组转为对应长度的int,再转为字符串即可
return strconv.Itoa(int(binary.BigEndian.Uint16(b)))

同理,针对第一个火焰图的Read,定位到代码如下:

// 逐字节读取 binary.Read(每次新建一个临时变量,并读取一个字节,总共需要分成n次读取)
// 目的是为了将每个直接按大端解析,
n := recordSize
for n > 0 {
    var field uint8
    if err := binary.Read(buffer, binary.BigEndian, &field); err != nil {
        return 0, err
    }
    Fields = append(Fields, field)
    n -= 1
}
 
// 然而实际上单字节内不管是网络数据还是内存中的数据都是一样的,大小端主要是针对多字节的情况,比如int类型的四个字节。
// 换成一次拷贝用 buffer.Next(n) 函数,直接把n个直接拷贝到对应位置
 
Fields = buffer.Next(recordSize)

这两个简单的修改就提升了很多性能。。由此可见,在从github上抄代码时(¬_¬),特别是一些不知名的代码还是要自己审阅一遍。。

最终让程序性能得到重大提升的,是对最后net.Write的优化。这个方法也很简单,原来是每条消息发一次包,改成拼接多条短消息,再发一个大包,大包的长度不要超过一个以太帧,本文使用UDP是不超过1450,预留了一点空间,反正也放不下一条消息。

内存使用调优

内存调优主要是使用上面那个pprof图,观察流程是否合理,是否可以简化,以及每个函数的内存分配情况,具体过程不像上面那么清洗,都是小修小补,故直接总结一些可能不够可靠的经验:

  1. 减少不必要的临时变量,函数的参数如果比较长则应该传递指针
  2. 在字节流处理中,原来经常出现使用 bytes.NewBuffer(buffer) 作为参数的情况,这种用法是为了使用 bytes.Buffer 的一系列函数,但是需要重新申请一次空间,其实这样会多申请一个bytes.Buffer对象,如果操作比较简单,可以直接对buffer数组进行,不用转换。还有就是 string 的转换也会申请空间,比如把 []byte 转 string ,做个简单的处理又转成 []byte 发送出去 ,可以尽量去掉中间的过程
  3. 如果已知切片大小,直接make出指定长度,否则频繁的 grow 占用资源
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!