Go 性能优化 | 青训营

121 阅读6分钟

1 性能优化

前提满足正确可靠、简洁清晰等质量因素。

所以针对Go语言特性,介绍Go相关的性能优化建议。

1.1 Benchmark

性能表现需要实际数据衡量。

Go语言提供了支持基准性能测试的benchmark工具

测试代码:

image.png

终端输入命令: go test -bench=. -benchmem fib_test.go

结果: image.png

第一列:BenchmarkFib10是测试函数名,-8表示GOMAXPROCS(核数)得值为16

第二列:表示一共执行3986155次即b.N的值,

第三列:每次执行花费326ns

第四列:每次申请多大的内存

第五列:每次申请几次内存

1.2 Slice

slice预分配内存

尽可能使用make()初始化切片时提供容量信息

image.png image.png

image.png 指定后它的内存等分配效率越高

原因

切片本质上是一个数组片段的描述:

包括数组指针

片段的长度

片段的容量(不改变内存分配情况下的最大长度)

切片操作并不复制切片指向的元素

创建一个新的切片会复用原来切片的底层数组

type slice struct {
    array unsafe.Pointer
    len int
    cap int
}

image.png

另一个陷阱:大内存未释放

在已有切片的基础上创建切片,不会创建新的底层数组

场景:

  原切片较大,代码在原切片基础上新建小切片
  原底层数组在内存中有引用,得不到释放

可使用copy代替re-slice

image.png

image.png image.png

image.png

1.3 map

map预内存分配

image.png

image.png

image.png

分析:

不断向map中添加元素的操作会触发map的扩容

提前分配好空间可以减少内存拷贝和Rehash的消耗

建议根据实际需求提前预估好需要的空间

1.4 字符串处理

使用strings.Builder

1 image.png 2 image.png 3 image.png

image.png

使用+拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer更快。

分析:

字符串在Go语言中是不可变类型,占用内存大小是固定的

使用+每次都会重新分配内存

strings.Builder,bytes.Buffer底层都是[]byte 数组

内存扩容策略,不需要每次拼接重新分配内存

1.5 空结构体

使用空结构体节省内存

空结构体struct{} 实例不占据任何的内存空间

可作为各种场景下的占位符使用

节省资源
空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符

1.6 atomic包

image.png

image.png

image.png

锁的实现是通过操作系统来实现,属于系统调用

atomic操作是通过硬件实现,效率比锁高

sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量

对于非数值操作,可以使用atomic.Value,能承载一个interface{}

2 性能调优实战

性能调优原则:

要求依靠数据不是猜测

要定位最大瓶颈而不是细枝末节

不要过早优化

不要过度优化

2.1 性能分析工具pprof

说明

希望知道应用在什么地方耗费了多少CPU、Menory

pprof是用于可视化和分析性能分析数据的工具

2.1.1 功能简介

image.png

2.1.2 排查实战

搭建pprof实践项目

GitHub(来自Wolfogre)

github.com/wolfogre/go…

项目提前埋下了一些炸弹代码,产生可观测的性能问题

前置准备:
下载项目代码,能够编译运行
会占用1CPU核心和超过1GB的内存

对于前置准备的代码:

image.png

CPU

image.png image.png

go tool pprof "http://127.0.0.1:6060/debug/pprof/profile?seconds=10"

top

命令:topN

查看占用资源最多的函数

image.png

image.png

image.png

命令:list

根据制定的正则表达式查找代码行

image.png

命令:web

调用关系可视化

image.png

Heap-堆内存

image.png

image.png

go tool pprof -http=:8080 "http://192.168.28.1:139/debug/pprof/heap"

2.1.3 goroutine-协程

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"

image.png

由上到下表示调用顺序

每一块代表一个函数,越长代表占用CPU的事件更长

火焰图是动态的,支持点击块进行分析

image.png

支持搜索,在Source视图下搜索wolf

2.1.4 锁

mutex-锁

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex

2.1.5 block-阻塞

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block

image.png

image.png

两个Block为什么只展示了一个

image.png

第二个阻塞操作是什么

image.png

image.png

2.2 pprof采样过程和原理

CPU

采样对象:函数调用和它们占用的时间

采样率:100次/秒,固定值

采样时间:从手动启动到手动结束

开始采样————>设定信号处理函数————>开启定时器

停止采样————>取消信号处理函数————>关闭定时器

详细过程

操作系统:每10ms向进程发送一次SIGPROF信号

进程:每次接收到SIGPROF会记录调用堆栈

写缓冲:每100ms读取已经记录的调用栈并写入输出流

image.png

2.2.1 Heap-堆内存

采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量

采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录

采样时间:从程序运行开始到采样时

采样指标:alloc_space,alloc_objects,inuse_space,inuse_objects

计算方式:inuse = alloc - free

2.2.2 Goroutine-协程 & ThreadCreate-线程创建

Goroutine记录所有用户发起且在运行中的goroutine(即入口非runtime开头的) runtime.main的调用栈信息

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

Goroutine: Stop The World————>遍历allg切片————>输出创建g的堆栈————>Start The World

ThreadCreate:Stop The World————>遍历allm链表————>输出创建m的堆栈————>Start The World

2.3 性能调优实战-简介

介绍实际业务服务性能优化的案例

对逻辑相对复杂的程序如何进行性能调优

业务服务优化

基础库优化

Go语言优化

2.3.1 业务服务优化

基本概念

服务:能单独部署,承载一定功能的程序

依赖:service A的功能实现依赖Service B的响应结果,称为Service A依赖Service B

调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系

基础库:公共的工具包、中间件

image.png

流程:

建立服务性能评估手段

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

重点优化项改造

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

优化效果验证

2.3.2 Go语言优化

编译器&运行时优化

优化内存分配策略

优化代码编译流程,生成更高效的程序

内部压测验证

推广业务服务落地验证

优点: 接入简单,只需要调整编译配置;通用性强。

image.png