GO性能调优 | 青训营笔记

108 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

高质量编程

  • 正确可靠
  • 简洁清晰

要求:

  • 简单性
  • 可读性
  • 生产力

编码规范

  • 公共符号始终要注释(特殊:不需要注释实现接口的方法)
  • 采用gofmt自动格式化代码
  • 注释:解释代码作用、如何做、实现原因、什么情况下出错

命名规范

  • 变量:简洁,缩略词全大写(开头特殊)、变量距离使用的地方越远,需携带更多上下文信息
  • 形参:使用具有特定含义的
  • 函数名:不携带包名的上下文信息、短
  • 包名:只包含小写字母、短+包含上下文信息、避免重名

控制流程

  • 避免嵌套
  • 尽量保持正常代码路径为最小缩进

错误和异常处理

  • 优先使用errors.New来创建匿名变量来直接表示简单错误
  • 若有格式化要求,使用fmt.Errorf
  • 错误的Wrap和Unwrap
  • 判断特殊错误,使用errors.Is/As
  • 不建议在业务代码中使用panic
  • recover只能在被defer的函数中使用、嵌套无法生效、只在当前goroutine有效、defer后进先出

性能优化

go test -bench=. -benchmen
go test -run=. -v

slice

切片的本质是一个数组片段的描述(数组指针、片段长度、片段容量),切片操作并不复制切片指向的元素、创建新切片会复用原来切片的底层数组

  • 尽可能使用make初始化容量信息
  • 在已有切片基础上创建切片,不会创建新的底层数组,可能会导致大内存未释放/原底层数组在内存中有引用,得不到释放(可以使用copy替代re-slice)

map

  • 预分配内存,向map中添加元素会触发map的扩容,预分配可以减少内存拷贝和rehash的消耗

string

  • 字符串在Go中是不可变类型,占用内存大小固定,
  • 使用+每次会重新分配内存,
  • strings.Builder/Buffer底层是[]byte数组,内部维护了扩容策略
  • 使用strings.Builder/Buffer拼接字符串,builder较快,因为buf转化为字符串时重新申请了一块空间,builder直接将底层的[]byte转换成了字符串类型返回
  • 提前申请空间buf.Grow()

空结构体

  • 不占用内存空间
  • 作为占位符使用
  • 用来通过map实现set

atomic包

多线程安全

  • 锁通过os实现,系统调用,消耗高
  • atomic硬件实现,效率高

性能调优

原则

  • 依靠数据而不是猜测
  • 定位最大瓶颈,而不是细枝末节
  • 不要过早优化
  • 不要过度优化

go test

-bench=. # 进行性能测试,“.”是正则匹配,匹配了所有的测试函数
-benchmem # 打印出申请内存的次数。一般用于简单的性能测试,不会导出数据文件。
-blockprofile block.out # 将协程的阻塞数据写入特定的文件(block.out)。如果-c,则写成二进制文件。
-cpuprofile cpu.out # 将协程的CPU使用数据写入特定的文件(cpu.out)。如果-c,则写成二进制文件。
-memprofile mem.out # 将协程的内存申请数据写入特定的文件(mem.out)。如果-c,则写成二进制文件。
-mutexprofile mutex.out # 将协程的互斥数据写入特定的文件(mutex.out)。如果-c,则写成二进制文件。
-trace trace.out # 将执行调用链写入特定文件(trace.out)。

pprof

功能

分析、展示、工具、采样

使用

github.com/wolfogre/go… http://localhost:6060/debug/pprof/ http://localhost:port/debug/pprof/profile,这个地址会收集30秒内服务的运行情况,这个结果会返回一个profile文件

  • 有过滤策略

环境配置

学习链接:blog.wolfogre.com/posts/go-pp…

package main

import (
	// 略
	_ "net/http/pprof" // 会自动注册 handler 到 http server,方便通过 http 接口获取程序运行采样报告
	// 略
)

func main() {
	// 略

	runtime.GOMAXPROCS(1) // 限制 CPU 使用数,避免过载
	runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪
	runtime.SetBlockProfileRate(1) // 开启对阻塞操作的跟踪

	go func() {
		// 启动一个 http server,注意 pprof 相关的 handler 已经自动注册过了
		if err := http.ListenAndServe(":6060", nil); err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	}()

	// 略
}

cpu

  • 采样对象:函数调用和它们占用的时间
  • 采样率:100次/s,固定值
  • 采样时间:从手动启动到手动结束
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10" # 默认30s
go tool pprof cpu.out

top命令 展示cpu占用较高的调用

flat:当前函数本身的执行时间
flat%:flat占CPU总时间的比例
sum%:从上往下每一行的flat%总和
cum:当前函数本身加上其调用函数的总耗时
cum%:cum占CPU总时间的比例

list Eat(函数名)命令,产看具体代码位置
web命令
traces 显示栈信息

heap

使用web需要先安装graphviz: windowns:www.graphviz.org/download/

并且设置默认使用浏览器打开.svg文件

  • 采样率:512KB依次,可在运行开头修改
  • 采样时间:从程序运行开始到采样时
  • 采样指标:alloc_space...
  • 计算方式:inuse = alloc - free

基本排查

sudo apt install graphviz # 安装graphviz
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap" # 查看堆内存
top视图和source视图

go tool pprof "http://localhost:6060/debug/pprof/heap
top  list

排查频繁内存回收

# 程序启动前设置环境变量
go build # 先gobuild
GODEBUG=gctrace=1 ./go-pprof-practice | grep gc # ./go-pprof-practice是二进制文件

# 内存的申请与释放频度需要一段时间统计,保证程序运行了几分钟再运行
go tool pprof http://localhost:6060/debug/pprof/allocs
top list web

tips:golang对象使用堆内存还是栈内存,由编译器进行逃逸分析,若对象不会逃逸(申请的内存较小),便可以使用栈内存,不涉及GC

goroutine

协程可能泄露(协程失控),进而导致内存泄露

  • 由上到下表示调用顺序
  • 记录所有用户发起且在运行中的goroutine
  • 每一块代表一个函数,越长代表占用CPU时间更长
  • 火焰图是动态的,支持点击块进行分析
  • 支持搜索,在source视图下搜索wolf
  • 火焰图的横向长度表示cum,相比下面超出的一截代表flat
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
traces 显示栈信息,找占用最大的一行中最底下的

go tool pprof http://localhost:6060/debug/pprof/goroutine
top list web

  • 采样争抢锁的次数和耗时
  • 采样率:记录固定比例的锁操作,可以设置为每次都记录
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"

go tool pprof http://localhost:6060/debug/pprof/mutex
top list web

阻塞block

  • 采样阻塞操作的次数和耗时
  • 超阈值才记录,可以设置为每次都记录
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"

go tool pprof http://localhost:6060/debug/pprof/block
top list web

ThreadCreate

  • 记录程序创建的所有系统线程信息

总结

go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10" # 默认30s
top # 查看资源占用函数
list funcName # 展示问题代码片段

tips:heap第二步,用traces查看

业务服务优化

  • 建立服务性能评估手段
  • 分析性能数据,定位性能瓶颈
  • 重点优化项改造
  • 优化效果验证

建立服务性能评估手段

  • 服务性能评估方式
  • 请求流量构造
  • 压测范围
  • 性能数据采集

分析性能数据,定位性能瓶颈

  • 第三方库
  • 高并发场景

业务服务优化

工具:Ediff

  • 正确性是基础
  • 响应数据diff:线上请求数据录制,新旧逻辑接口数据diff

优化压测验证

  • 重复压测

进一步,服务整体链路分析

  • 规范上游服务调用接口,明确场景需求
  • 分析链路,通过业务流程优化提升服务性能

基础库优化

  • 分析基础库核心逻辑和性能瓶颈
  • 内部压测验证
  • 推广业务服务落地验证

编译器 & 运行时优化