这是我参与「第三届青训营-后端场」笔记创作活动的第3篇笔记。
0.课前准备
1.高质量编程
1.1 什么是高质量
- 各种边界条件考虑完备,具有正确性
- 对异常情况进行处理,保证稳定性
- 易读易维护
1.2 编程原则
- 简单性:以简单清晰的逻辑编写代码
- 可读性:善用注释
- 生产力:代码风格统一、功能模块划分妥当
1.3 编码规范
1.3.1 代码格式
-
gofmt:官方工具,自动格式化go语言代码为官方统一风格,常见IDE都支持方便的配置
-
goimports:官方工具,相当于gofmt加上依赖包管理(自动增删依赖的包引用,将依赖包按字母排序并分类)
1.3.2 注释: 好的代码有很多注释,坏的代码需要很多注释
注释应该做的:
- 体现代码作用:一般公共变量、公开对外的api需要注释。若函数名已经可以体现函数作用,可不加注释。
- 说明代码实现流程:相对复杂的程序,不明显的功能、代码行需要加注释说明代码在干什么。代码更改时需要维护注释。
- 解释代码为什么有效:解释外部因素,证明算法有效性,说明某一行的意义。
- 标注代码什么情况会出错:提示代码的限制条件以及输入需要进行什么处理。
1.3.3 命名规范
好的命名就像一个好笑话,如果你必须解释它,那就不好笑了。 1.3.3.1 变量命名
- 尽可能简洁,但不应该减少变量名中的重要信息
- 大小写得当
- 变量距离被使用的地方越远,越需要变量名携带更多信息
1.3.3.2 函数命名
- 函数名不用携带包名的上下文信息
- 尽可能简短
- 当返回类型容易被误会时,应加入返回类型信息
1.3.3.3 包名命名
- 只由小写字母组成,不包含大写字母和下划线等字符
- 简短且包含一定上下文信息
- 不要与标准库同名
- 尽量不用常用变量名作包名
- 尽量使用英文单数而不是复数
- 谨慎的使用缩写,缩写应该在不丢失信息时使用
1.3.4 控制流程
减少循环分支的嵌套,优先处理错误/特殊情况,尽早返回,尽量减小正常代码路径的缩进数。
1.3.4 错误和异常处理
简单错误:仅出现一次且不需要被捕获的错误,使用 error.New("simple error")创建匿名变量直接表示。如果有格式化的需求,使用fmt.Errorf。
复杂错误:使用fmt.Errorf的%w将错误嵌套进错误链。
if err != nil {
return fmt.Errorf("error is %w",err)
}
错误判定:使用errors.Is(err, 特定错误)判断err是否为特定错误。不同于==,errors.Is可以判定错误链上的所有错误是否含有特定错误。使用errors.As(err, &pathError)可以获取特定种类的错误,pathError.Path为错误的路径。
panic比error更严重,出现时可能意味着程序将不能正常工作。
recover可用于记录panic的上下文信息,其必须在defer函数中使用。
1.3 性能优化
1.3.1 Benchmark
使用go test -bench=. -benchmem查看结果。每行结果都包括
测试函数名-cpu内核使用个数 函数被执行总次数 每次执行用时 每次申请内存大小 每次申请内存次数
-
Slice优化
-
预先分配内存会减少分配次数和总大小(原理同C++的vector扩容)。
-
如果一开始有一个大切片,将其部分为小切片赋值,此时不会创建新的切片,而是小切片引用大切片的部分。此时大切片在内存中有引用,得不到释放,导致内存占用过大。因此应使用
copy(small, big[start:end])函数进行赋值。 -
map优化
-
预先分配内存(原理同C++的unorder_map扩容)。
-
string优化
-
go中字符串是不可变类型,占用内存大小是固定的,每次
+都会重新分配内存,string.Builder和bytes.Buffer底层都是[]byte数组,每次扩容内存而不是字符串,因此效率更高。返回时,bytes.Buffer重新申请了一块空间来读取buffer并转化为字符串,而string.Builder直接将底层的[]byte转化为字符串并返回。因此string.Builder性能最优。 -
string.Builder和bytes.Buffer都支持内存的预分配。 -
空结构体
-
空结构体不占据任何空间,可作为占位符使用(实现set)
-
atomic包
-
atomic操作通过硬件实现,性能比加锁高。锁是通过操作系统来实现的,属于系统调用。锁应该用于保护一段逻辑而不是用于保护一个变量。 对于非数值操作,可以使用atomic.Value,能承载一个interface{}。
2.性能调优实战
2.1 性能调优原则
- 要依靠数据而不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化(此时的优化以后可能用不上)
- 不要过度优化(需要保证优化方法能兼容后续版本) 2.2 性能调优工具
pprof是用于可视化和分析性能、分析数据的工具,能观察在什么地方耗费了多少cpu和内存。
2.2.1 pprof-功能简介
2.2.2 pprof-排查实战
1.一定要用go mod init github.com/wolfogre/go-pprof-practice指令构建go.mod!!!无论是否执行go mod tidy,你的go.mod文件中应该只有2行文本(如下图)。
2.运行实验代码go build以及./go-pprof-practice-master,或使用go run main.go,两种方法在资源管理器中的进程名称会不同,但都会排在最上面(因为此时这个程序消耗资源很多)。
3.浏览器打开http://localhost:6060/debug/pprof/可以查看可用的程序运行采样数据。
4.终端输入gnome-system-monitor可查看任务管理器。可以看到此程序cpu和内存都是最高的。
5.终端输入go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"等待出现输入行出现(pprof)标识。
6.输入命令:top查看占用资源最多的函数。结果含义如下:
flat: 当前函数本身的执行耗时
flat%: flat占CPU总时间的比例
sum%: 上面每一行的flat%总和
cum: 当前函数本身加上其调用函数的总耗时
cum%: cum占CPU总时间的比例
因此flat==cum代表当前函数没有调用其它函数,flat==0代表函数中只有其它函数的调用。可以看到tiger的Eat函数占用了大量资源。
7.输入命令:list Eat查看细节。list后的参数是正则表达式形式。
可以看到函数中的for循环占用了大量的flat和cum时间。
8.使用sudo apt install graphviz安装可视化功能后就能在(pprof)处输入web进行可视化。
9.注释掉23-26行的循环代码重新运行。则cpu占用率下降,但内存占用仍然很高。
10.使用go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"查看内存情况。该命令能直接打开可视化图,可以看到mouse的Steal占用了大量内存。
11.可视化图菜单的View中包含很多有用的图和表。其中VIEW的Source提供了类似第6步list的可视化。如果程序是使用go run main.go运行的,则可能加载不出来,需要使用go build运行可执行文件才能看到此视图。
12.找到内存占用的原因
将48-51行代码注释掉并重新运行。
13.重新运行程序并打开可视化。菜单栏的SAMPLE中有下拉菜单,四个选项分别为alloc_objects(程序累计申请对象数)、alloc_space(程序累积申请的内存大小)、inuse_objects(程序当前持有的对象数)、inuse_space(程序当前占用的内存大小)。
14.点击alloc_space可以看到dog的run函数累积申请内存最大。
因为是累积量,所以这个数字会越来越大,但我们的视图是截取的某一时刻。可以通过重新输入打开可视化界面命令的方法更新数据,以更好的进行观察。之后我们注释掉第43行重新运行代码。
15.goroutine泄漏也会导致内存泄漏。在第3步打开的网页中可以看到此程序有100+个goroutine,因此接下来查看goroutine相关的信息。输入指令go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"查看信息。
16.点击VIEW的Flame Graph(火焰图,将调用关系平铺)可以更直观的观察数据。火焰图从上到下表示调用顺序,每一个小块代表一个函数,小块的长度代表占用cpu的时间。火焰图支持简单交互,鼠标移动之小块上或点击都能得到额外信息。此时看到wolf的Drink函数占用cpu最长(因为其协程数过多)。
VIEW的Top视图按协程数降序排列可以看到下图结果,其中每个Drink都会调用Sleep函数等待30秒。
在VIEW的Source中搜索Drink查看细节如下:
注释掉第34行代码,刷新第3步的界面,可以看到协程数降低。同时注意到锁(mutex)的数量为1。
17.输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"查看锁信息。
18.按之前的方法找到锁时间最长的地方(wolf的Howl),注释掉第54-60行并重新运行程序。这一步由于Howl调用的Unlock函数占据主要时间,所以可视化结果中可能仅显示Unlock的部分(可视化界面会过滤资源占用少的函数,无法显示与搜索),使用go tool pprof "http://localhost:6060/debug/pprof/mutex"+top+list即可搜索到目标函数。
19.刷新第三步视图,还有2个block,这不一定有问题,但我们可以检查一下。于是输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"查看阻塞信息。
20.Cat的Pee会阻塞,注释掉第38行,再次运行。此时只剩0-1个阻塞。实验结束。
2.2.2 pprof-采样过程和原理
2.2.2.1 cpu采样
- 采样对象:函数调用和它们占用的时间
- 采样率:100次/秒,固定值
- 采样时间:从手动开启到手动结束
- 采样过程:pprof进程会在操作系统上启动一个计时器,操作系统每10ns会向进程发送一次SIGPROF信号,pprof接到信号后会记录当前的调用堆栈。pprof每100ns将记录的信息写入缓冲区。进程结束时取消操作系统的定时器,并将写入的缓冲区转化为文件方便后续处理。
2.2.2.2 堆内存采样
- 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和次数。
- 采样率:每分配512KB记录一次,可在运行开头修改,设为1代表每次分配都记录。
- 采样时间:从程序运行开始到采样时。
- 采样指标:alloc_objects、alloc_space、inuse_objects、inuse_space。
- 计算方式:inuse = alloc - free
2.2.2.3 协程与线程
- Goroutine:记录所有用户发起且在运行中的goroutine(即入口不是runtime开头的)runtime.main的调用栈信息。
- Threadreate:记录程序创建的所有系统线程的信息。
- 遍历记录协程或线程的表,记录它们的调用信息。
2.2.2.4 阻塞和锁
- 阻塞:采样阻塞操作的次数和耗时。耗时超过阈值才会被记录,阈值可设定,设为1代表每次阻塞都记录。
- 锁竞争:采样争抢锁的次数和耗时。通过随机数来记录一定比例的锁操作。若设定1则记录每次加锁。
2.3 性能调优案例
2.3.1业务服务优化
基本概念:
- 服务:能单独部署,承载一定功能的程序。
- 依赖:A的功能实现依赖B的响应结果,则称A依赖B。
- 调用链路:能支持一个接口请求的
相关服务集合及其相互之间的依赖关系 - 基础库:公共的工具包、中间件
流程:
-
建立服务性能评估手段:
服务性能评估方式:单独benchmark无法满足复杂逻辑分析;不同负载情况下性能表现差异 请求流量构造:不同请求参数覆盖逻辑不同;线上真实流量情况 压测范围:单机器压测;集群压测 性能数据采集:单极性能数据;集群性能数据 -
分析性能数据,定位性能瓶颈
使用库不规范:json库、日志库 高并发场景优化不足:用异步优化同步 -
重点优化项的改造
正确性是基础:和优化前的结果做对比,分析正确率是否下降 -
优化效果验证
重复压测验证 上线评估优化效果:关注服务监控;逐步放量;收集性能数据
整体链路分析:
- 规范上游服务调用接口,明确场景需求
- 分析链路,通过业务优化提升服务性能
2.3.2基础库优化
AB实验SDK的优化:
- 分析基础库核心逻辑和性能瓶颈:设计完善方案、数据按需获取、数据序列号协议优化
- 内部压测验证
- 推广业务服务落地验证
2.3.3go语言优化
编译器&运行时优化:
- 优化内存分配策略
- 优化代码编译流程,生成更高效的程序
- 内部压测验证
- 推广业务服务落地验证
优点:
- 接入简单,只需要调整编译配置
- 通用性强
4.课后作业(copy整理的官方答案)
1.其他语言的编码规范是否与Go语言有相通之处?
可以了解开源项目的编码规范,比如Google开源项目风格指南
google.github.io/styleguide/
中文版:
https://zh-google-styleguide.readthedocs.io/en/latest/
2.有没有方式能够自动化对代码进行检测?
Go 语言有代码检查工具,可以和CI进行集成
https://github.com/golangci/golangci-lint
3.查看看Go的源码github.com/golang/go/t…
可以先从sync包和net包入手
https://github.com/golang/go/tree/master/src/sync
https://github.com/golang/go/tree/master/src/net
4.使用 Go 进行并发编程时有哪些性能陷阱或者优化手段?
可以了解Effective Go的并发编程章节 https://go.dev/doc/effective_go#concurrency
以及 https://github.com/geektutu/high-performance-go 的并发编程部分
5.了解现实的性能优化的案例
https://eng.uber.com/category/oss-projects/oss-go/
https://eng.uber.com/how-we-saved-70k-cores-across-30-mission-critical-services/
6.Go 语言每个版本在性能上有哪些重要的优化点?
每次更新有文档说明
https://go.dev/doc/devel/release