性能优化前提:正确可靠,简洁清晰
性能调优原则:
- 依靠性能而非猜测
- 定位最大瓶颈而非细枝末节
- 不要过早优化,预期将要出现性能问题时再优化
- 不要过度优化,避免后期功能修改不兼容
常见方法
Slice
- 尽量在
make()初始化时提供足够的内存容量信息- 减少内存分配和复制:切片本质是一个数组片段,容量不足时会创建一个新数组并复制原有元素(或复用底层数组)
- 使用
copy代替切片的切片- 切片的切片复用底层数组,即使原切片不再使用也不会释放,容易造成大内存未释放问题
map
- 尽量预分配足够空间,减少
map扩容和rehash
string
- 大量字符串拼接使用
strings.Builder,底层使用[]byte- 尽量通过
Grow预分配长度
- 尽量通过
struct
- 使用空结构体
struct{}作为占位符,空结构体本身不占据任何内存空间,可用于实现set(map[T]struct{})
多线程
- 使用
atomic原子类代替锁,通过硬件实现,效率更高 sync.Mutex用意是保护一段逻辑,而不是保护一个变量
pprof
可视化性能分析工具,包括工具、采样、分析、展示等模块
[!note] 火焰图:自上而下表示调用顺序,横向宽度表示耗时
引入 net/http/pprof 和 net/http 模块后,在代码中开启服务器,这里在 6060 端口开启,可通过 localhost:6060/debug/pprof/ 访问
性能排查
go tool pprof -http=:端口 "数据地址",其中数据地址即前面 6060 的具体页项,依赖于 graphviz(注意添加环境变量)
如将堆内存的分析报告映射到 8080 端口,使用
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap“
控制台访问 go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10" 采集 10s 内 CPU 使用情况
top,topN:查看占用资源最多的函数flat,flat%:当前函数本身占用sum%:当前行与前面行的flat%总和cum,cum%:当前函数与函数调用的其他函数总和
list 函数名:进入函数,查看占用最大的部分
web:创建可视化调用关系图,需要依赖Graphviz工具(实际生成一个 svg 图)
采样过程及原理
- CPU:使用系统定时信号采样(SIGPROF 信号)
- 采样对象:函数调用及其占用时间
- 采样率:100 次/秒,固定
- 采样时间:从调用开始到采样结束
- Heap 堆内存:通过内存分配器记录分配、释放大小和数量,因此记录不到栈内存
- 采样率:默认 512K 记录一次
- 采样时间:从程序运行开始到采样时
- 采样指标:
alloc_space,alloc_objects,inuse_space,inuse_objects,其中inuse=alloc-free
- Goroutine 协程:
Stop the world- 遍历allg切片 - 输出g堆栈 -Start the world- 记录用户发起且在运行中的(即非 runtime 的)调用堆栈
- 记录
runtime.main的调用堆栈
- ThreadCreate 线程:
Stop the world- 遍历allm链表 - 输出m堆栈 -Start the world- 记录程序创建的所有系统线程信息
- Block 阻塞、Mutex 锁竞争:阻塞(或锁竞争) - 上报 Profile 调用栈和耗时 - 采样 - 遍历记录 - 统计次数和超时
- 采样:操作的次数和超时
- 采样率:
- 阻塞记录耗时超过阈值的操作
- 采样率记录固定比例的锁操作
调优实战
业务服务优化
服务:能单独部署,承载一定功能的程序
调用链路:支持一个接口请求的相关服务集合及依赖关系
基础库:公共工具包,中间件等
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 调用库不规范
- 日志
- 对比高峰与低峰数据
- 重点优化项改造
- 正确性判断:比较优化前后的输出
- 优化效果验证
- 进一步优化:规范上游服务调用,分析链路
基础库优化
- 分析核心逻辑和性能瓶颈
- 内部压测验证
- 推广业务服务落地验证
Go 语言优化
接入简单(调整编译配置),通用性强
- 优化内存分配策略
- 优化编译流程
- 内部压测验证
- 推广业务服务落地验证