这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记
前言
最近学到了如何写出高性能和可靠的 Go 代码,虽然对这么方面的经验还很浅
但还是想着写篇文章来对这部分知识做一个简单的梳理
什么是性能问题
这个问题看起来没什么价值,但其实有不小的门道
以我的拙见,我认为性能问题可以从两个视角来看
一个是用户(服务调用者)视角,使用起来会感到卡顿(效率低下)
一个是商家(服务提供者)视角,启动时资源分配异常(资源浪费)
至此,我简单的将性能问题分为时间上的问题和空间上的问题
具体来说,服务调用的时间过长(时间问题)和资源分配异常(空间问题)会造成性能问题
如何定位性能问题
Golang标准库中的基准测试(Benchmark)
作为 Golang 的新手,我的第一个想法自然是 golang标准库 中的 testing, 它提供了基准测试 Benchmark,可以进行函数级别的测试,这样其实已经可以满足很多场景的需求
我们引用学习中发现的一个简单小demo看下效果
被测试代码
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i+100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
测试代码
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
执行结果
goos: windows
goarch: amd64
cpu: Intel(R) Core(TM) i5-10300H CPU @ 2.50GHz
BenchmarkSelect
BenchmarkSelect-8 73104758 16.41 ns/op 0 B/op 0 allocs/op
PASS
ok command-line-arguments 3.581s
可以看到结果中有一些性能相关的参数,至此,我们可以对代码的性能有一个粗略的了解
问题
- 可以定位到出现问题的链路入口,若想进一步定位,需要写新的测试,能否直接查看整个链路的情况?
- 测试终究是测试,和实际运行环境还是不同的,能否查看到真正的运行环境下的性能监控?
- 能否使用可视化界面查看,更加直观?
Golang标准库中的性能检测工具 pprof
pprof 是 Go 提供的能 可视化分析 性能数据 的工具
你没看错,你想要的 Go 都有,没有可以自己写(狗头)
pprof同时图形化显示性能报告和命令行按需查询性能数据,可以说非常友好
本文主要简单整理一下 pprof 的原理和排查问题使用的主要属性
主要指标
监控的指标主要分布在CPU占用,Heap堆栈内存占用,以及阻塞等待,协程间内存等
| 类型 | 描述 |
|---|---|
| allocs | 内存分配 |
| blocks | 阻塞操作 |
| cmdline | 显示程序启动命令及参数 |
| goroutine | 当前所有协程的堆栈信息 |
| heap | 堆上内存使用 |
| mutex | 锁争用 |
| profile | CPU 占用 |
| threadcreate | 系统线程创建 |
| trace | 程序运行跟踪信息 |
常用指令
以下指令需要在使用
go tool pprof "http://localhost:6060/debug/pprof/具体指标项"指令对属性进行采样之后才能使用详情见安装部署以及简单的使用
同时,新版本的pprof集成了火焰图,还可以使用高级的web页面代替静态文件的方式查看性能报告
只需要改为
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/具体指标项"即可
- topN: 可以查看当前采样指标排在前N位的函数信息
- list XXX: XXX标识具体的函数名,可以查看XXX函数中当前采样指标最异常的代码的片段,并标识出问题最大的代码位置
- web:需要配合 graphviz 插件使用,可以生成可视化文件,调用相应组件打开
原理简析
检测CPU占用
- 采样对象:函数调用和占用时间
- 采样率:100次/秒。固定值
- 采样时间:手动启动到手动结束
- 过程:
- 操作系统(进程会提前向操作系统注册定时器,结束后注销)
- 每10ms 向进程发送SIGPROF信号
- 进程
- 每次接收到SIGPROF会记录调用堆栈
- 写缓冲
- 每100ms 读取已经记录的调用栈写入输出流
- 操作系统(进程会提前向操作系统注册定时器,结束后注销)
检测 Heap-堆内存 占用
- 采样程序通过内存分配器在对上分配和释放的内存,记录分配/释放的大小和数量
- 采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
- 采样时间:从程序运行开始到采样时间
- 采样指标:alloc_space、alloc_objects、inuse_space、inuse_objects
- 计算方式:inuse = alloc - free
检测 Goroutine-协程 和 ThreadCreate-创建
- Goroutinue
- 记录所有用户发起且在运行中的goroutine(入口非runtime开头的)runtime.main的调用栈信息
- ThreadCreate
- 记录程序创建所有系统线程的信息
检测 Block-阻塞 和 Mutex-锁
- 阻塞操作
- 采样阻塞操作的次数和耗时
- 采样率:阻塞好事超过阈值的才会被记录,1为每次阻塞均记录
- 锁竞争
- 采样争抢锁的次数和耗时
- 采样率:指记录固定比例的锁操作,1为每次加锁均记录
企业案例学习
是老师留下的作业,但是学习的过程中确实受到了震撼,特此记录一下自己当时的小结
看了Uber的文章,有一个案例是从对GOGC处理做了优化
他们发现GO的GC在很多服务中开销占比不小(文中的实例大概占20以上) 而触发GC的方式主要有两种:一种是定时触发;另一种是堆内存进行检测,如果内存压力过大就会触发GC
同时,他们发现他们的主要服务器中,内存和CPU空间的占比(5:1)要比一般的GO服务要大(一般是2:1或1:1),所以他们有信心通过更高效的利用多出来的内存,来减少第二种GC的触发次数,从而提高程序的性能
最后他们通过编写自动化组件的动态调配GOGC的参数,从而动态的分配内存,实现了他们的思路
因为这个优化是业务无关的,所以几乎他们公司绝大多数的服务都可以使用这个优化,最后他们在 30 项关键服务任务中节省下了 70K 内核,简直是非常NB