Golang FlameGraph(火焰图)

简介

初学golang(一个月多),以前主要用其余语言,若有问题欢迎指出。html

安装

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/ 查看程序实时状态git

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

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时长,颜色没有特别的意义golang

pprof内存分配图效果

输入图片说明

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

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

调优实践

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

CPU使用调优

结果图

先来看看先后的对比图:网络

输入图片说明

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

输入图片说明

因为对前面两个部分的优化,占用的时间已经大大缩小,从70%左右降低到40%+。后面没有作处理的网络读写net.Write占了不少时间svg

调优过程

首先针对字符串处理部分,从第一个火焰图点击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 占用资源