Go语言性能调优笔记| 青训营笔记

221 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第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

可以看到结果中有一些性能相关的参数,至此,我们可以对代码的性能有一个粗略的了解

问题

  1. 可以定位到出现问题的链路入口,若想进一步定位,需要写新的测试,能否直接查看整个链路的情况?
  2. 测试终究是测试,和实际运行环境还是不同的,能否查看到真正的运行环境下的性能监控?
  3. 能否使用可视化界面查看,更加直观?

Golang标准库中的性能检测工具 pprof

pprof 是 Go 提供的能 可视化分析 性能数据 的工具

你没看错,你想要的 Go 都有,没有可以自己写(狗头)

pprof同时图形化显示性能报告和命令行按需查询性能数据,可以说非常友好

安装部署以及简单的使用

本文主要简单整理一下 pprof 的原理和排查问题使用的主要属性

主要指标

监控的指标主要分布在CPU占用,Heap堆栈内存占用,以及阻塞等待,协程间内存等

类型描述
allocs内存分配
blocks阻塞操作
cmdline显示程序启动命令及参数
goroutine当前所有协程的堆栈信息
heap堆上内存使用
mutex锁争用
profileCPU 占用
threadcreate系统线程创建
trace程序运行跟踪信息

常用指令

以下指令需要在使用 go tool pprof "http://localhost:6060/debug/pprof/具体指标项" 指令对属性进行采样之后才能使用

详情见安装部署以及简单的使用

同时,新版本的pprof集成了火焰图,还可以使用高级的web页面代替静态文件的方式查看性能报告

只需要改为 go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/具体指标项" 即可

  1. topN: 可以查看当前采样指标排在前N位的函数信息
  2. list XXX: XXX标识具体的函数名,可以查看XXX函数中当前采样指标最异常的代码的片段,并标识出问题最大的代码位置
  3. 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