Go 高质量编程与性能调优 | 青训营笔记

116 阅读10分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

本节课讨论如何编写更简洁清晰的代码,学习常用 Go 语言程序优化手段,并熟悉 Go 程序性能分析工具,了解工程中性能优化的原则和流程。

高质量编程

  1. 高质量编程
    正确:各种边界条件是否考虑完备
    可靠:异常情况处理,稳定性保证
    简洁清晰:易读易维护

  2. 编程原则
    简单性:消除“多余的复杂性”,以简单清晰的逻辑编写代码不理解的代码无法修复改进。
    可读性:代码是写给人看的,而不是机器,编写可维护代码的第一步是确保代码可读。
    生产力:团队整体工作效率非常重要。

  3. 编码规范
    腾讯代码规范

  4. 代码格式
    使用gofmt和goimports自动格式化代码。

  5. 注释
    注释应该解释代码的作用、如何做的、实现的原因和什么情况会出错。
    解释公共函数和变量,代码的实现逻辑,解释为什么这么做(历史背景上下文等等),解释代码运行的限制条件,什么时候可能会报错。

  6. 命名规范

命名笑话.png 1、变量命名
简洁胜于冗长。
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写。
例如使用 ServeHTTP 而不是 ServeHttp,使用 XMLHTTPRequest 或者 xmIHTTPRequest。
变量距离其被使用的地方越远,则需要携带越多的上下文信息。
例如全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义

2、函数命名
函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的。
例如http包中的Server函数,在使用是http.Server,所以不用写成ServerHTTP。

3、包的命名
只由小写字母组成。不包含大写字母和下划线等字符。
简短并包含一定的上下文信息。例如 schema、task 等。
不要与标准库同名。例如不要使用 sync 或者 strings。
尽量不使用常用变量名作为包名。例如使用 bufio 而不是 buf。
尽量使用单数而不是复数。例如使用 encoding 而不是 encodings。
谨慎地使用缩写。例如使用 fmt 在不破坏上下文的情况下比 format 更加简短

  1. 控制流程
    避免判断if-else嵌套
    优先处理错误情况/特殊情况,尽早返回或继续循环
    故障问题大多出现在复杂的条件语句和循环语句中

  2. 错误和异常处理
    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)
  1. panic
    不建议在业务代码中使用 panic,调用函数不包含 recover 会造成程序崩溃,若问题可以被屏蔽或解决,建议使用error 代替 panic。
    当程序启动阶段发生不可逆转的错误时可以在 init 或 main 函数中使用 panic。

  2. 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( ))
	}
}()

性能优化建议

  1. 测试性能
go test -bench=. -benchmem

性能.png

go test -run. -v
=== RUN   TestBench
6765
--- PASS: TestBench (0.00s)
PASS
ok      test/exe1 0.102s
  1. 切片slice在初始化时通过make提供容量信息。
    在只使用大切片部分片段时推荐使用copy(sliceSmall, sliceBig),而不是sliceBig[:2]。

  2. map也可以预分配以优化性能

  3. 拼接字符串最好使用strings.Builder,并在初始化时用builder.Grow初始化容量
    用+操作拼接每次都要申请内存
    bytes.Builder将[]byte转化成string时重新申请了内存
    strings.Builder 直接将底层的[]byte 转换成了字符串类型返回

  4. 空结构体不占内存,可以作为占位符。
    例如在使用map时,如果只用map的key值,可以用struct{}作为value值的类型,即便用bool类型作为value也会占内存。

map=make(map[int]struct{})
  1. atomic包
    避免并行混乱可以使用加锁操作,但会降低效率,可以使用atomic包。
    锁的实现是通过操作系统来实现,属于系统调用。
    atomic 操作是通过硬件实现,效率比锁高。
    sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量。
    对于非数值操作,可以使用 atomic.Value,能承载一个 interfaced。

  2. 优化性能小结
    避免常见的性能陷阱可以保证大部分程序的性能。
    普通应用代码,不要一味地追求程序的性能。
    越高级的性能优化手段越容易出现问题。

性能调优实战

性能调优原则:
要依靠数据不是猜测。
要定位最大瓶颈而不是细枝末节。
不要过早优化。
不要过度优化。

性能分析工具pprof

pprof.png

  1. 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命令。

  1. 内存优化
    命令行输入
    缺少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。
内存.png mouse.go中的这行代码占用了1.2GB内存,注释之后可以优化内存占用的问题。

在这个网页下:
SAMPLE下的alloc_objects是程序累计申请的对象数,
alloc_space是程序累计申请的内存大小,
inuse_objects是程序当前持有的对象数,
inuse_space是程序当前占用的内存大小。切换不同选项可以看到不同的内存占用情况。

  1. 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 的时间更长。火焰图是动态的,支持点击块进行分析
协程.png 点击source,可以看到wolf每次申请50个协程,修改之后可以优化goroutine。 协程2.png

  1. mutex锁
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
  1. block阻塞(两个)
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
go tool pprof "http://localhost:6060/debug/pprof/block"
  1. 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. 建立服务性能评估手段

1、服务性能评估方式:
单独 benchmark 无法满足复杂逻辑分析
不同负载情况下性能表现差异
2、请求流量构造:
不同请求参数覆盖逻辑不同
线上真实流量情况
3、压测范围:
单机器压测
集群压测
4、性能数据采集:
单机性能数据
集群性能数据

  1. 分析性能数据,定位性能瓶颈
    使用库不规范,代码复用率低。
    高并发场景优化不足。

  2. 重点优化项改造
    可以比较优化前后输出数值的差异,从而判断对正确性的影响。

  3. 优化效果验证
    重复压测验证。
    上线评估优化效果,包括关注服务监控,逐步放量,收集性能数据。

  4. 基础库优化

分析基础库核心逻辑和性能瓶颈

1. 设计完善改造方案。
2. 数据按需获取。
3. 数据序列化协议优化。

内部压测验证
推广业务服务落地验证

Go 语言优化

编译器&运行时优化:
优化内存分配策略
优化代码编译流程,生成更高效的程序
内部压测验证
推广业务服务落地验证