这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
本节课讨论如何编写更简洁清晰的代码,学习常用 Go 语言程序优化手段,并熟悉 Go 程序性能分析工具,了解工程中性能优化的原则和流程。
高质量编程
-
高质量编程
正确:各种边界条件是否考虑完备
可靠:异常情况处理,稳定性保证
简洁清晰:易读易维护 -
编程原则
简单性:消除“多余的复杂性”,以简单清晰的逻辑编写代码不理解的代码无法修复改进。
可读性:代码是写给人看的,而不是机器,编写可维护代码的第一步是确保代码可读。
生产力:团队整体工作效率非常重要。 -
编码规范
腾讯代码规范 -
代码格式
使用gofmt和goimports自动格式化代码。 -
注释
注释应该解释代码的作用、如何做的、实现的原因和什么情况会出错。
解释公共函数和变量,代码的实现逻辑,解释为什么这么做(历史背景上下文等等),解释代码运行的限制条件,什么时候可能会报错。 -
命名规范
1、变量命名
简洁胜于冗长。
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写。
例如使用 ServeHTTP 而不是 ServeHttp,使用 XMLHTTPRequest 或者 xmIHTTPRequest。
变量距离其被使用的地方越远,则需要携带越多的上下文信息。
例如全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
2、函数命名
函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的。
例如http包中的Server函数,在使用是http.Server,所以不用写成ServerHTTP。
3、包的命名
只由小写字母组成。不包含大写字母和下划线等字符。
简短并包含一定的上下文信息。例如 schema、task 等。
不要与标准库同名。例如不要使用 sync 或者 strings。
尽量不使用常用变量名作为包名。例如使用 bufio 而不是 buf。
尽量使用单数而不是复数。例如使用 encoding 而不是 encodings。
谨慎地使用缩写。例如使用 fmt 在不破坏上下文的情况下比 format 更加简短
-
控制流程
避免判断if-else嵌套
优先处理错误情况/特殊情况,尽早返回或继续循环
故障问题大多出现在复杂的条件语句和循环语句中 -
错误和异常处理
Go1.13 在 errors 中新增了三个新API和一个新的format 关键字,分别是 errors.Is,errors.As,errors.Unwrap 以及 fmt.Errorf 的 %w。如果项目运行在小于 Go1.13 的版本中,导入golang.org/x/xerrors来使用。
Warp生成一个error的跟踪链,fmt.Errorf将一个错误关联到错误链中。
list, _, err := c.GetBytes( cache.Subkey(a.actionID,"srcfiles"))
if err != nil {
return fmt.Errorf("reading srcfiles list: %w", err)
}
errors.Is判断错误链上所有错误是否含有目标错误
errors.Is(err, fs.ErrNotExist)
errors.Is在错误链上获取特定种类的错误
errors.As(err,&pathError)
-
panic
不建议在业务代码中使用 panic,调用函数不包含 recover 会造成程序崩溃,若问题可以被屏蔽或解决,建议使用error 代替 panic。
当程序启动阶段发生不可逆转的错误时可以在 init 或 main 函数中使用 panic。 -
recover recover 只能在被 defer 的函数中使用。
嵌套无法生效。
只在当前 goroutine 生效。
defer 语句会在函数返回前调用,多个 defer 语句是后进先出。
defer func() {
if e := recover(); e != nil{
if se, ok := e.(scanError); ok {
err = se.err
} else {
panic(e)
}
}
}()
如果需要更多的上下文信息,可以 recover 后在 log中传递参数
defer func() {
if e := recover(); e != nil {
f = nil
err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack( ))
}
}()
性能优化建议
- 测试性能
go test -bench=. -benchmem
go test -run. -v
=== RUN TestBench
6765
--- PASS: TestBench (0.00s)
PASS
ok test/exe1 0.102s
-
切片slice在初始化时通过make提供容量信息。
在只使用大切片部分片段时推荐使用copy(sliceSmall, sliceBig),而不是sliceBig[:2]。 -
map也可以预分配以优化性能
-
拼接字符串最好使用strings.Builder,并在初始化时用builder.Grow初始化容量
用+操作拼接每次都要申请内存
bytes.Builder将[]byte转化成string时重新申请了内存
strings.Builder 直接将底层的[]byte 转换成了字符串类型返回 -
空结构体不占内存,可以作为占位符。
例如在使用map时,如果只用map的key值,可以用struct{}作为value值的类型,即便用bool类型作为value也会占内存。
map=make(map[int]struct{})
-
atomic包
避免并行混乱可以使用加锁操作,但会降低效率,可以使用atomic包。
锁的实现是通过操作系统来实现,属于系统调用。
atomic 操作是通过硬件实现,效率比锁高。
sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量。
对于非数值操作,可以使用 atomic.Value,能承载一个 interfaced。 -
优化性能小结
避免常见的性能陷阱可以保证大部分程序的性能。
普通应用代码,不要一味地追求程序的性能。
越高级的性能优化手段越容易出现问题。
性能调优实战
性能调优原则:
要依靠数据不是猜测。
要定位最大瓶颈而不是细枝末节。
不要过早优化。
不要过度优化。
性能分析工具pprof
- CPU优化
运行go-pprof-practice-master
打开网页localhost:6060/debug/pprof/查看运行情况
也可以命令行输入。
PS D:\Go\go-pprof-practice-master> go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=10
Saved profile in C:\Users\user\pprof\pprof.samples.cpu.001.pb.gz
Type: cpu
Time: Jan 19, 2023 at 5:47pm (CST)
Duration: 10s, Total samples = 5.69s (56.87%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
输入top得到。
(pprof) top
Showing nodes accounting for 5.68s, 99.82% of 5.69s total
Dropped 1 node (cum <= 0.03s)
flat flat% sum% cum cum%
5.68s 99.82% 99.82% 5.69s 100% github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat
0 0% 99.82% 5.69s 100% github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Live
0 0% 99.82% 5.69s 100% main.main
0 0% 99.82% 5.69s 100% runtime.main
其中:
flat是当前函数本身的执行耗时,
flat%是flat 占 CPU 总时间的比例,
sum%是上面每一行的 flat% 总和,
cum指当前函数本身加上其调用函数的总耗时,
cum%是cum 占 CPU 总时间的比例。
显然tiger.(*Tiger).Eat耗时最多。
初步找到耗时最多的项目之后用list关键字进一步定位。
(pprof) list Eat
Total: 5.69s
ROUTINE ======================== github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat in D:\desktop\Go\go-pprof-practice-master\animal\felidae\tiger\tiger.go
5.68s 5.69s (flat, cum) 100% of Total
. . 19:}
. . 20:
. . 21:func (t *Tiger) Eat() {
. . 22: log.Println(t.Name(), "eat")
. . 23: loop := 10000000000
5.68s 5.69s 24: for i := 0; i < loop; i++ {
. . 25: // do nothing
. . 26: }
. . 27:}
. . 28:
. . 29:func (t *Tiger) Drink() {
可以发现是第24行的一个循环耗时最多。
除了网页和命令行还可以用web命令。
- 内存优化
命令行输入
缺少graphviz插件可以官网下载安装,并把bin目录更新到PATH环境变量下。
命令行输入,即安装成功
C:\Users\user>dot -version
dot - graphviz version 7.0.6 (20230106.0513)
如果还不行,就在bin目录下输入dot -c,然后继续操作。
运行代码,命令行输入。
C:\>go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap
Saved profile in C:\Users\user\pprof\pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.006.pb.gz
Serving web UI on http://localhost:8080
会弹出一个网址 http://localhost:8080/ui/ ,通过图形化工具展示占用内存大小的比例
图片,点击VIEW下的source。
mouse.go中的这行代码占用了1.2GB内存,注释之后可以优化内存占用的问题。
在这个网页下:
SAMPLE下的alloc_objects是程序累计申请的对象数,
alloc_space是程序累计申请的内存大小,
inuse_objects是程序当前持有的对象数,
inuse_space是程序当前占用的内存大小。切换不同选项可以看到不同的内存占用情况。
- goroutine协程优化
命令行输入
C:\>go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
Fetching profile over HTTP from http://localhost:6060/debug/pprof/goroutine
Saved profile in C:\Users\user\pprof\pprof.goroutine.001.pb.gz
Serving web UI on http://localhost:8080
弹出一个与内存的类似的网页。点击VIEW里的Flame。
由上到下表示调用顺序。每一块代表一个函数,越长代表占用 CPU 的时间更长。火焰图是动态的,支持点击块进行分析
点击source,可以看到wolf每次申请50个协程,修改之后可以优化goroutine。
- mutex锁
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
- block阻塞(两个)
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
go tool pprof "http://localhost:6060/debug/pprof/block"
- pprof采样过程和原理
CPU:
采样对象: 函数调用和它们占用的时间。
采样率: 100次/秒,固定值。
采样时间: 从手动启动到手动结束。
Heap堆内存:
采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量。
采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录。
采样时间:从程序运行开始到采样时。
采样指标: alloc_space, alloc_objects, inuse_space, inuse_objects。
计算方式: inuse = alloc - free。
Goroutine协程:
记录所有用户发起且在运行中的 goroutine (即入口非runtime开头的)
runtime.main 的调用栈信息
ThreadCreate线程
记录程序创建的所有系统线程的信息
block阻塞操作:
采样阻塞操作的次数和耗时。
采样率:阻塞耗时超过闯值的才会被记录1为每次阻塞均记录。
mutex锁竞争:
采样争抢锁的次数和耗时。
采样率:只记录固定比例的锁操作,1为每次加锁均记录。
性能调优案例
业务服务优化
流程:
建立服务性能评估手段
分析性能数据,定位性能瓶颈
重点优化项改造
优化效果验证
- 建立服务性能评估手段
1、服务性能评估方式:
单独 benchmark 无法满足复杂逻辑分析
不同负载情况下性能表现差异
2、请求流量构造:
不同请求参数覆盖逻辑不同
线上真实流量情况
3、压测范围:
单机器压测
集群压测
4、性能数据采集:
单机性能数据
集群性能数据
-
分析性能数据,定位性能瓶颈
使用库不规范,代码复用率低。
高并发场景优化不足。 -
重点优化项改造
可以比较优化前后输出数值的差异,从而判断对正确性的影响。 -
优化效果验证
重复压测验证。
上线评估优化效果,包括关注服务监控,逐步放量,收集性能数据。 -
基础库优化
分析基础库核心逻辑和性能瓶颈
1. 设计完善改造方案。
2. 数据按需获取。
3. 数据序列化协议优化。
内部压测验证
推广业务服务落地验证
Go 语言优化
编译器&运行时优化:
优化内存分配策略
优化代码编译流程,生成更高效的程序
内部压测验证
推广业务服务落地验证